paperlint 7.0.0 → 7.2.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 +3 -0
- package/.github/workflows/deadlines.yml +100 -0
- package/README.md +24 -11
- package/dist/adapters/hotcrp/deadlines.d.ts +8 -0
- package/dist/adapters/hotcrp/deadlines.d.ts.map +1 -0
- package/dist/adapters/hotcrp/deadlines.io.d.ts +6 -0
- package/dist/adapters/hotcrp/deadlines.io.d.ts.map +1 -0
- package/dist/adapters/hotcrp/deadlines.io.js +26 -0
- package/dist/adapters/hotcrp/deadlines.io.js.map +1 -0
- package/dist/adapters/hotcrp/deadlines.js +134 -0
- package/dist/adapters/hotcrp/deadlines.js.map +1 -0
- package/dist/adapters/hotcrp/index.d.ts +2 -0
- package/dist/adapters/hotcrp/index.d.ts.map +1 -1
- package/dist/adapters/hotcrp/index.js +2 -0
- package/dist/adapters/hotcrp/index.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +42 -7
- package/dist/cli.js.map +1 -1
- package/dist/cycle-rules.d.ts +24 -0
- package/dist/cycle-rules.d.ts.map +1 -0
- package/dist/cycle-rules.js +212 -0
- package/dist/cycle-rules.js.map +1 -0
- package/dist/domain/body-pages.d.ts +29 -0
- package/dist/domain/body-pages.d.ts.map +1 -1
- package/dist/domain/body-pages.js +25 -0
- package/dist/domain/body-pages.js.map +1 -1
- package/dist/domain/cycle.d.ts +148 -0
- package/dist/domain/cycle.d.ts.map +1 -0
- package/dist/domain/cycle.js +281 -0
- package/dist/domain/cycle.js.map +1 -0
- package/dist/domain/deadline.d.ts +128 -0
- package/dist/domain/deadline.d.ts.map +1 -0
- package/dist/domain/deadline.js +281 -0
- package/dist/domain/deadline.js.map +1 -0
- package/dist/eslint-rules/cycle-stage.d.ts +39 -0
- package/dist/eslint-rules/cycle-stage.d.ts.map +1 -0
- package/dist/eslint-rules/cycle-stage.js +119 -0
- package/dist/eslint-rules/cycle-stage.js.map +1 -0
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +2 -0
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/paper-includes.d.ts +3 -0
- package/dist/paper-includes.d.ts.map +1 -1
- package/dist/paper-includes.js +17 -2
- package/dist/paper-includes.js.map +1 -1
- package/dist/paper-settings.d.ts +7 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +61 -8
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/venue-deadlines.d.ts +14 -0
- package/dist/ports/venue-deadlines.d.ts.map +1 -0
- package/dist/ports/venue-deadlines.js +2 -0
- package/dist/ports/venue-deadlines.js.map +1 -0
- package/dist/presets.d.ts +22 -3
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +26 -17
- package/dist/presets.js.map +1 -1
- package/dist/submission.d.ts +16 -0
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +59 -16
- package/dist/submission.js.map +1 -1
- package/dist/tex-requirements.d.ts +12 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +26 -3
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +24 -2
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +98 -10
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/venue-rules.d.ts +12 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +81 -17
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +140 -12
- package/docs/e2e.md +14 -12
- package/docs/rules/cycle/evidence.md +87 -0
- package/docs/rules/cycle/port-done.md +75 -0
- package/docs/rules/cycle/record.md +152 -0
- package/docs/rules/cycle/stage.md +85 -0
- package/docs/rules/format/page-limit.md +18 -1
- package/docs/rules/tex/template.md +25 -5
- package/docs/rules/tex/venue-leftover.md +13 -0
- package/docs/rules.md +68 -55
- package/docs/submission.md +10 -1
- package/eslint-rules/cycle-stage.test.mjs +131 -0
- package/eslint-rules/cycle-stage.ts +141 -0
- package/eslint.config.mjs +41 -1
- package/fixtures/build-e2e/msr/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/msr/expect.json +18 -0
- package/fixtures/build-e2e/msr/paper.tex +49 -0
- package/fixtures/build-e2e/msr/paperlint.json +1 -0
- package/fixtures/build-e2e/msr-appendix-after/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/msr-appendix-after/expect.json +20 -0
- package/fixtures/build-e2e/msr-appendix-after/paper.tex +48 -0
- package/fixtures/build-e2e/msr-appendix-after/paperlint.json +1 -0
- package/lib/paper-config.ts +3 -0
- package/package.json +1 -1
- package/presets/aidc.jsonc +24 -3
- package/presets/aidc.md +6 -3
- package/presets/ieee-conference.md +1 -1
- package/presets/msr.jsonc +95 -0
- package/presets/msr.md +98 -0
- package/presets/venue-profile.schema.json +64 -0
- package/scripts/check.ts +4 -0
- package/scripts/refresh-deadlines.test.ts +431 -0
- package/scripts/refresh-deadlines.ts +387 -0
- package/skills/camera-ready/SKILL.md +8 -1
- package/skills/camera-ready/SKILL.md.spec.ts +7 -0
- package/skills/submission-portal/SKILL.md +2 -2
- package/skills/submission-portal/SKILL.md.spec.ts +1 -1
- package/skills/submit-paper/SKILL.md +9 -1
- package/skills/submit-paper/SKILL.md.spec.ts +8 -0
- package/src/adapters/hotcrp/cassette/README.md +22 -0
- package/src/adapters/hotcrp/cassette/deadlines-aidc26.html +17 -0
- package/src/adapters/hotcrp/cassette/deadlines-fse2027.html +24 -0
- package/src/adapters/hotcrp/cassette/deadlines-icse2027.html +25 -0
- package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +25 -0
- package/src/adapters/hotcrp/cassette/deadlines-nosuch.html +18 -0
- package/src/adapters/hotcrp/cassette/deadlines-sosp26.html +26 -0
- package/src/adapters/hotcrp/deadlines.io.test.ts +86 -0
- package/src/adapters/hotcrp/deadlines.io.ts +27 -0
- package/src/adapters/hotcrp/deadlines.test.ts +170 -0
- package/src/adapters/hotcrp/deadlines.ts +161 -0
- package/src/adapters/hotcrp/index.ts +2 -0
- package/src/cli.test.ts +9 -0
- package/src/cli.ts +50 -6
- package/src/cycle-rules.test.ts +278 -0
- package/src/cycle-rules.ts +273 -0
- package/src/domain/body-pages.test.ts +144 -0
- package/src/domain/body-pages.ts +61 -1
- package/src/domain/cycle.test.ts +456 -0
- package/src/domain/cycle.ts +432 -0
- package/src/domain/deadline.test.ts +386 -0
- package/src/domain/deadline.ts +472 -0
- package/src/new-paper.test.ts +28 -0
- package/src/paper-includes.test.ts +26 -0
- package/src/paper-includes.ts +20 -2
- package/src/paper-settings-commands.test.ts +1 -1
- package/src/paper-settings.test.ts +167 -0
- package/src/paper-settings.ts +85 -11
- package/src/ports/venue-deadlines.ts +14 -0
- package/src/preset-card-rule.test.ts +1 -0
- package/src/presets.test.ts +171 -1
- package/src/presets.ts +65 -15
- package/src/rule-cases.test.ts +144 -0
- package/src/submission.test.ts +147 -4
- package/src/submission.ts +100 -18
- package/src/tex-requirements.ts +52 -4
- package/src/tex-venue-rules.test.ts +237 -0
- package/src/tex-venue-rules.ts +130 -15
- package/src/venue-rules.test.ts +255 -2
- package/src/venue-rules.ts +115 -22
package/src/tex-venue-rules.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* requires of the LaTeX itself, before any build:
|
|
4
4
|
*
|
|
5
5
|
* tex/template error the `\documentclass` is the preset's class, with every option it names
|
|
6
|
+
* and none it forbids (`template_forbids`)
|
|
6
7
|
* tex/required-section error each section the preset requires is there, titled exactly, and
|
|
7
8
|
* where the preset says (`last`: after every section of the body)
|
|
8
9
|
* tex/venue-leftover warn the text a reader sees names ANOTHER shipped venue — its `name`
|
|
@@ -51,6 +52,7 @@ import type { PaperSource } from "./domain/paper-source.ts";
|
|
|
51
52
|
import { readPaper, reportInPaper } from "./tex-paper.ts";
|
|
52
53
|
import type { RequiredSection } from "./tex-requirements.ts";
|
|
53
54
|
import type { Finding, VenueRuleDeps } from "./venue-rules.ts";
|
|
55
|
+
import { isPorting } from "./domain/cycle.ts";
|
|
54
56
|
|
|
55
57
|
/** A finding and where it points; null points at the top of the file. */
|
|
56
58
|
export interface Located extends Finding {
|
|
@@ -107,20 +109,38 @@ type TemplateData = {
|
|
|
107
109
|
readonly template: string;
|
|
108
110
|
};
|
|
109
111
|
|
|
110
|
-
/**
|
|
112
|
+
/** What the template requires of a class line: its class and options, and the options it forbids. */
|
|
113
|
+
interface Wanted {
|
|
114
|
+
readonly cls: DocumentClass;
|
|
115
|
+
readonly forbids: readonly string[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A class line that names a class, against the template's: the same class, with every option, and
|
|
120
|
+
* none of the options the preset forbids.
|
|
121
|
+
*/
|
|
111
122
|
function judgeClass(
|
|
112
123
|
line: Extract<ClassLine, { readonly kind: "class" }>,
|
|
113
|
-
want:
|
|
124
|
+
want: Wanted,
|
|
114
125
|
data: TemplateData,
|
|
115
126
|
): readonly Located[] {
|
|
116
127
|
const at = spanOf(line.place);
|
|
117
|
-
if (line.cls !== want.cls)
|
|
128
|
+
if (line.cls !== want.cls.cls)
|
|
118
129
|
return [{ messageId: "wrongClass", data: { ...data, got: line.cls }, at }];
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
const forbidden = want.forbids.filter((o) => line.options.includes(o));
|
|
131
|
+
const without = want.forbids.map((o) => `\`${o}\``).join(", ");
|
|
132
|
+
return [
|
|
133
|
+
...forbidden.map((option) => ({
|
|
134
|
+
messageId: "forbiddenOption",
|
|
135
|
+
data: { ...data, option, without },
|
|
136
|
+
at,
|
|
137
|
+
})),
|
|
138
|
+
...missingOptions(line, want.cls).map((option) => ({
|
|
139
|
+
messageId: "missingOption",
|
|
140
|
+
data: { ...data, option },
|
|
141
|
+
at,
|
|
142
|
+
})),
|
|
143
|
+
];
|
|
124
144
|
}
|
|
125
145
|
|
|
126
146
|
/** A candidate as the message names it: its line, or an empty `\documentclass{}`. */
|
|
@@ -133,7 +153,7 @@ const candidateLine = (c: ClassCandidate): string =>
|
|
|
133
153
|
*/
|
|
134
154
|
function judgeCandidates(
|
|
135
155
|
candidates: readonly [ClassCandidate, ...ClassCandidate[]],
|
|
136
|
-
want:
|
|
156
|
+
want: Wanted,
|
|
137
157
|
data: TemplateData,
|
|
138
158
|
): readonly Located[] {
|
|
139
159
|
const matches = candidates.some(
|
|
@@ -150,7 +170,7 @@ function judgeCandidates(
|
|
|
150
170
|
/** The paper's class line against a template that was read: present, naming a class, that class. */
|
|
151
171
|
function judgeClassLine(
|
|
152
172
|
line: ClassLine,
|
|
153
|
-
want:
|
|
173
|
+
want: Wanted,
|
|
154
174
|
data: TemplateData,
|
|
155
175
|
): readonly Located[] {
|
|
156
176
|
switch (line.kind) {
|
|
@@ -183,7 +203,7 @@ export function judgeTemplate(
|
|
|
183
203
|
return [{ messageId: "badTemplate", data, at: null }];
|
|
184
204
|
}
|
|
185
205
|
const data = { venue: preset.label, template: documentClassLine(want) };
|
|
186
|
-
return judgeClassLine(line, want, data);
|
|
206
|
+
return judgeClassLine(line, { cls: want, forbids: t.forbids }, data);
|
|
187
207
|
}
|
|
188
208
|
|
|
189
209
|
/** Where a heading starts, or null when the parser gave it no place. */
|
|
@@ -248,8 +268,17 @@ export function judgeRequiredSections(
|
|
|
248
268
|
);
|
|
249
269
|
}
|
|
250
270
|
|
|
251
|
-
/**
|
|
252
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Another shipped venue: the word for it, what it is called in a paper's text, and when a name
|
|
273
|
+
* counts as naming it (`mentions`).
|
|
274
|
+
*/
|
|
275
|
+
export type OtherVenue = NamedVenue & Pick<Preset, "mentions">;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* What must follow a name for it to name a `with-year` venue: a year beside it — `2027`, `'27` or
|
|
279
|
+
* `’27`, after spaces or none (`~` renders as a space). A lookahead, so the finding is the name.
|
|
280
|
+
*/
|
|
281
|
+
const YEAR_BESIDE = "(?=[\\s\\u00a0]*(?:\\d{4}|['’]\\d{2})(?![\\p{L}\\p{N}]))";
|
|
253
282
|
|
|
254
283
|
/** `s` as a pattern that matches it literally. */
|
|
255
284
|
const literal = (s: string): string =>
|
|
@@ -271,7 +300,7 @@ export function judgeLeftover(
|
|
|
271
300
|
.map((alias) => ({
|
|
272
301
|
other: o.label,
|
|
273
302
|
re: new RegExp(
|
|
274
|
-
`(?<![\\p{L}\\p{N}])${literal(alias)}(?![\\p{L}\\p{N}])`,
|
|
303
|
+
`(?<![\\p{L}\\p{N}])${literal(alias)}(?![\\p{L}\\p{N}])${o.mentions === "with-year" ? YEAR_BESIDE : ""}`,
|
|
275
304
|
"gu",
|
|
276
305
|
),
|
|
277
306
|
})),
|
|
@@ -357,6 +386,8 @@ const RULES: Readonly<
|
|
|
357
386
|
"the class is `{{got}}`, and {{venue}} requires `{{template}}` — the page size, fonts and layout the venue checks come from the class",
|
|
358
387
|
missingOption:
|
|
359
388
|
"the class option `{{option}}` is missing: {{venue}} requires `{{template}}`",
|
|
389
|
+
forbiddenOption:
|
|
390
|
+
"the class option `{{option}}` is forbidden: {{venue}} requires `{{template}}` without {{without}}",
|
|
360
391
|
noCandidate:
|
|
361
392
|
"none of the {{count}} \\documentclass lines is `{{template}}`, which {{venue}} requires: {{candidates}}. The source picks one behind a TeX switch, which is not evaluated; make one of them the venue's",
|
|
362
393
|
},
|
|
@@ -442,7 +473,7 @@ export function otherVenues(
|
|
|
442
473
|
): readonly OtherVenue[] {
|
|
443
474
|
return shippedVenueNames(deps)
|
|
444
475
|
.filter((v) => !v.chain.every((f) => preset.chain.includes(f)))
|
|
445
|
-
.map(({ label, aliases }) => ({ label, aliases }));
|
|
476
|
+
.map(({ label, aliases, mentions }) => ({ label, aliases, mentions }));
|
|
446
477
|
}
|
|
447
478
|
|
|
448
479
|
/** What the venue-conformance rules are built with: the preset store, and the LaTeX reader. */
|
|
@@ -463,6 +494,10 @@ function readingOf(
|
|
|
463
494
|
const dir = dirname(filename);
|
|
464
495
|
const p = paperPreset(dir, deps);
|
|
465
496
|
if (p.kind !== "resolved") return null;
|
|
497
|
+
// The port to this venue's template is declared open work: the class line is the previous
|
|
498
|
+
// venue's by declaration, and `cycle/port-done` watches that declaration.
|
|
499
|
+
if (p.settings.cycles !== null && isPorting(p.settings.cycles.current))
|
|
500
|
+
return null;
|
|
466
501
|
const preset = p.preset;
|
|
467
502
|
const others = () => otherVenues(preset, deps);
|
|
468
503
|
const paper = readPaper(filename, src, deps);
|
|
@@ -508,3 +543,83 @@ export function formatRules(
|
|
|
508
543
|
): Readonly<Record<"layout-override", TexRuleModule>> {
|
|
509
544
|
return { "layout-override": rule("layout-override", deps) };
|
|
510
545
|
}
|
|
546
|
+
|
|
547
|
+
// ── cycle/port-done: the declared port is finished ───────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* `cycle/port-done` (error): the current cycle declares `"phase": "porting"`, and the paper's
|
|
551
|
+
* `\documentclass` is already the venue's template. The declaration silences every venue rule
|
|
552
|
+
* (`src/venue-rules.ts`, `readingOf` above); once the class line is the venue's, the port is done
|
|
553
|
+
* and the declaration is stale — this is the other half that keeps `porting` from being a mute
|
|
554
|
+
* button. Silent when the preset names no template: there is nothing to compare, as for `tex/template`.
|
|
555
|
+
*/
|
|
556
|
+
export function judgePortDone(
|
|
557
|
+
r: Pick<Reading, "src" | "latex" | "preset">,
|
|
558
|
+
cycleId: string,
|
|
559
|
+
): readonly Located[] {
|
|
560
|
+
if (r.preset.template === null) return [];
|
|
561
|
+
const mismatch = judgeTemplate(
|
|
562
|
+
r.latex.documentClass(r.src),
|
|
563
|
+
r.preset,
|
|
564
|
+
r.latex,
|
|
565
|
+
);
|
|
566
|
+
return mismatch.length > 0
|
|
567
|
+
? []
|
|
568
|
+
: [
|
|
569
|
+
{
|
|
570
|
+
messageId: "done",
|
|
571
|
+
data: {
|
|
572
|
+
venue: r.preset.label,
|
|
573
|
+
template: r.preset.template.text,
|
|
574
|
+
id: cycleId,
|
|
575
|
+
},
|
|
576
|
+
at: null,
|
|
577
|
+
},
|
|
578
|
+
];
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const PORT_DONE_META: TexRuleModule["meta"] = {
|
|
582
|
+
type: "problem",
|
|
583
|
+
docs: {
|
|
584
|
+
description:
|
|
585
|
+
"the current cycle still says `porting` while the \\documentclass is already the venue's template — the port is done, drop the phase",
|
|
586
|
+
url: "https://github.com/zernie/paperlint/blob/main/docs/rules/cycle/port-done.md",
|
|
587
|
+
},
|
|
588
|
+
schema: [],
|
|
589
|
+
messages: {
|
|
590
|
+
done: 'the \\documentclass is already `{{template}}`, which {{venue}} requires, and the cycle «{{id}}» still declares "phase": "porting" — the port is done: remove the phase, so the venue\'s format rules judge this paper again',
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
/** The `cycle` plugin's rule over the source, for every `paper.tex`. */
|
|
595
|
+
export function cyclePortRules(
|
|
596
|
+
deps: TexVenueRuleDeps,
|
|
597
|
+
): Readonly<Record<"port-done", TexRuleModule>> {
|
|
598
|
+
return {
|
|
599
|
+
"port-done": {
|
|
600
|
+
meta: PORT_DONE_META,
|
|
601
|
+
create(context) {
|
|
602
|
+
if (basename(context.filename) !== "paper.tex") return {};
|
|
603
|
+
return {
|
|
604
|
+
root() {
|
|
605
|
+
const p = paperPreset(dirname(context.filename), deps);
|
|
606
|
+
if (p.kind !== "resolved") return;
|
|
607
|
+
const current = p.settings.cycles?.current;
|
|
608
|
+
if (current?.kind !== "cycle" || !isPorting(current)) return;
|
|
609
|
+
const sc = context.sourceCode;
|
|
610
|
+
const paper = readPaper(context.filename, sc.raw ?? sc.text, deps);
|
|
611
|
+
const found = judgePortDone(
|
|
612
|
+
{ src: paper.text, latex: deps.latex, preset: p.preset },
|
|
613
|
+
current.cycle.id,
|
|
614
|
+
);
|
|
615
|
+
reportInPaper(context, PORT_DONE_META.messages, paper, found);
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export const CYCLE_PORT_RULE_LEVELS: Readonly<
|
|
624
|
+
Record<"cycle/port-done", "error">
|
|
625
|
+
> = { "cycle/port-done": "error" };
|
package/src/venue-rules.test.ts
CHANGED
|
@@ -231,6 +231,36 @@ describe("a paper that names no venue", () => {
|
|
|
231
231
|
lint({ venue: DECL, facts: null }, `${PAPER}/PIPELINE-STATUS.md`),
|
|
232
232
|
).toEqual([]);
|
|
233
233
|
});
|
|
234
|
+
|
|
235
|
+
it.each([
|
|
236
|
+
[
|
|
237
|
+
"a named venue",
|
|
238
|
+
{
|
|
239
|
+
id: "secdev-2027",
|
|
240
|
+
venue: { kind: "named", name: "SecDev 2027", url: "https://x.example" },
|
|
241
|
+
opened: "2027-01-10",
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
[
|
|
245
|
+
"the last cycle closed, none open",
|
|
246
|
+
{
|
|
247
|
+
id: "first-2026",
|
|
248
|
+
venue: { kind: "preset", extends: "paperlint:aisec" },
|
|
249
|
+
opened: "2026-07-01",
|
|
250
|
+
outcome: { kind: "withdrawn", date: "2026-07-18", evidence: "n.md" },
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
])(
|
|
254
|
+
"with `cycles` and %s: the warning says to set the venue IN THE CYCLE — `extends` beside `cycles` is refused",
|
|
255
|
+
(_, cycle) => {
|
|
256
|
+
const fs = lint({ venue: { cycles: [cycle] }, facts: null, pdf: null });
|
|
257
|
+
expect(ids(fs)).toEqual(["pdf/measured:noPresetCycle"]);
|
|
258
|
+
expect(fs[0]?.message).toMatch(
|
|
259
|
+
/^this paper's current cycle names no venue preset, so its page limit, fonts and format are not checked — set the venue in the current cycle in .*paperlint\.json \("venue": \{ "kind": "preset", "extends": "paperlint:…" \}\), or open a new cycle when the last one has ended$/,
|
|
260
|
+
);
|
|
261
|
+
expect(fs[0]?.message).not.toMatch(/set "extends"/);
|
|
262
|
+
},
|
|
263
|
+
);
|
|
234
264
|
});
|
|
235
265
|
|
|
236
266
|
describe("pdf/profile — the declaration must resolve, or nothing is judged", () => {
|
|
@@ -239,13 +269,13 @@ describe("pdf/profile — the declaration must resolve, or nothing is judged", (
|
|
|
239
269
|
"an unknown venue (a typo would silently disable every check)",
|
|
240
270
|
{ extends: "paperlint:agentic-dev", kind: "short" },
|
|
241
271
|
"preset",
|
|
242
|
-
/agenticdev, aidc, aisec, ieee-conference, realm/,
|
|
272
|
+
/agenticdev, aidc, aisec, ieee-conference, msr, realm/,
|
|
243
273
|
],
|
|
244
274
|
[
|
|
245
275
|
"the base TeX set is not a venue",
|
|
246
276
|
{ extends: "paperlint:tex-base" },
|
|
247
277
|
"preset",
|
|
248
|
-
/agenticdev, aidc, aisec, ieee-conference, realm/,
|
|
278
|
+
/agenticdev, aidc, aisec, ieee-conference, msr, realm/,
|
|
249
279
|
],
|
|
250
280
|
[
|
|
251
281
|
"paperlint.json that is not JSON",
|
|
@@ -863,6 +893,177 @@ describe("format/page-limit on AIDC: the two signals of where the references sta
|
|
|
863
893
|
});
|
|
864
894
|
});
|
|
865
895
|
|
|
896
|
+
/**
|
|
897
|
+
* A project's own venue that counts its appendices in the body (`appendix_in_body`): a body of ten
|
|
898
|
+
* pages, appendices included, and two pages holding only references — MSR's rule.
|
|
899
|
+
*/
|
|
900
|
+
const OWN_APPENDIX_IN_BODY = {
|
|
901
|
+
[`${PAPER}/own.jsonc`]: JSON.stringify({
|
|
902
|
+
type: "venue",
|
|
903
|
+
name: "Own",
|
|
904
|
+
url: "https://example.org/cfp",
|
|
905
|
+
extends: "paperlint:ieee-conference",
|
|
906
|
+
format: {
|
|
907
|
+
body_ends_at: "references",
|
|
908
|
+
appendix_in_body: true,
|
|
909
|
+
kinds: {
|
|
910
|
+
technical: { body_pages_max: 10, ref_pages_max: 2 },
|
|
911
|
+
open: {},
|
|
912
|
+
refs: { ref_pages_max: 2 },
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
}),
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
/** `own.jsonc` papers of the kind named, over IEEE facts with `pages` and the two anchors. */
|
|
919
|
+
const ownSplit = (
|
|
920
|
+
pages: readonly string[],
|
|
921
|
+
bib: number | null,
|
|
922
|
+
appendix: number | null,
|
|
923
|
+
kind = "technical",
|
|
924
|
+
) =>
|
|
925
|
+
lint({
|
|
926
|
+
venue: { extends: "./own.jsonc", kind },
|
|
927
|
+
extra: OWN_APPENDIX_IN_BODY,
|
|
928
|
+
facts: ieeeFacts((f) => {
|
|
929
|
+
f.pages_text = [...pages];
|
|
930
|
+
f.bib_anchor_page = bib;
|
|
931
|
+
f.appendix_anchor_page = appendix;
|
|
932
|
+
// banal's counts, which this counting replaces: neither is judged.
|
|
933
|
+
f.body_pages = 99;
|
|
934
|
+
f.ref_pages = 99;
|
|
935
|
+
}),
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
describe("format/page-limit with `appendix_in_body`: the appendix is body, the reference pages hold only references", () => {
|
|
939
|
+
const REFS = ["the end\nReferences\n[1] A.\n", "[2] B.\n", "[3] C.\n"];
|
|
940
|
+
|
|
941
|
+
it("ten pages of body and two of references pass; banal's counts are not judged", () => {
|
|
942
|
+
expect(ownSplit([...bodyPages(9), ...REFS], 10, null)).toEqual([]);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it("🔴 an appendix after the references that brings the body to eleven fails, naming where it starts", () => {
|
|
946
|
+
const fs = ownSplit(
|
|
947
|
+
[...bodyPages(9), ...REFS.slice(0, 2), "Appendix A.\nDetails\n"],
|
|
948
|
+
10,
|
|
949
|
+
12,
|
|
950
|
+
);
|
|
951
|
+
expect(fs.map((f) => f.message)).toEqual([
|
|
952
|
+
"body pages (up to the references on page 10, and the appendix from page 12): 11, over the limit 10 for Own/technical — a desk reject; cut the text",
|
|
953
|
+
]);
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it("🔴 three pages holding only references fail", () => {
|
|
957
|
+
const fs = ownSplit([...bodyPages(9), ...REFS, "[4] D.\n"], 10, null);
|
|
958
|
+
expect(fs.map((f) => f.message)).toEqual([
|
|
959
|
+
"pages holding only references: 3, over the limit 2 for Own/technical — a desk reject; cut the text",
|
|
960
|
+
]);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
it("an eleventh page of body before the references fails, appendices included", () => {
|
|
964
|
+
expect(ids(ownSplit([...bodyPages(10), ...REFS], 11, null))).toEqual([
|
|
965
|
+
"format/page-limit:pages",
|
|
966
|
+
]);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it.each<
|
|
970
|
+
[string, readonly string[], readonly [number | null, number | null], string]
|
|
971
|
+
>([
|
|
972
|
+
[
|
|
973
|
+
"no references heading",
|
|
974
|
+
bodyPages(3),
|
|
975
|
+
[null, null],
|
|
976
|
+
"format/page-limit:noReferences",
|
|
977
|
+
],
|
|
978
|
+
[
|
|
979
|
+
"an appendix anchor on a page with no «Appendix» line",
|
|
980
|
+
[...bodyPages(3), ...REFS],
|
|
981
|
+
[4, 6],
|
|
982
|
+
"format/page-limit:unclear",
|
|
983
|
+
],
|
|
984
|
+
])("%s: said, not counted", (_, pages, [bib, appendix], want) => {
|
|
985
|
+
expect(ids(ownSplit(pages, bib, appendix))).toEqual([want]);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it("a kind with no limits judges nothing; one with a reference limit only judges that", () => {
|
|
989
|
+
expect(ownSplit(bodyPages(3), null, null, "open")).toEqual([]);
|
|
990
|
+
expect(
|
|
991
|
+
ids(ownSplit([...bodyPages(12), ...REFS, "[4] D.\n"], 13, null, "refs")),
|
|
992
|
+
).toEqual(["format/page-limit:pages"]);
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
/** An MSR technical paper that declares who wrote it. */
|
|
997
|
+
const MSR = {
|
|
998
|
+
extends: "paperlint:msr",
|
|
999
|
+
kind: "technical",
|
|
1000
|
+
identity: ["Ada Example"],
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
/** MSR facts: `pages` as the PDF's text, the two anchors where hyperref put them. */
|
|
1004
|
+
const msrFacts = (
|
|
1005
|
+
pages: readonly string[],
|
|
1006
|
+
bib: number,
|
|
1007
|
+
appendix: number | null = null,
|
|
1008
|
+
) =>
|
|
1009
|
+
ieeeFacts((f) => {
|
|
1010
|
+
f.pages_text = [...pages];
|
|
1011
|
+
f.bib_anchor_page = bib;
|
|
1012
|
+
f.appendix_anchor_page = appendix;
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
/** References that start below the last body text on their page, then `more` pages of them. */
|
|
1016
|
+
const refsFrom = (more: number): string[] => [
|
|
1017
|
+
"the last lines of the conclusion\nR EFERENCES\n[1] D. E. Knuth.\n",
|
|
1018
|
+
...Array.from({ length: more }, (_, i) => `[${String(i + 2)}] More.\n`),
|
|
1019
|
+
];
|
|
1020
|
+
|
|
1021
|
+
describe("format/page-limit on MSR: ten pages of main text, appendices included, and two of only references", () => {
|
|
1022
|
+
it("🔴 ten pages of body, the references starting on the tenth, and two pages of only references pass", () => {
|
|
1023
|
+
expect(
|
|
1024
|
+
lint({
|
|
1025
|
+
venue: MSR,
|
|
1026
|
+
facts: msrFacts([...bodyPages(9), ...refsFrom(2)], 10),
|
|
1027
|
+
}),
|
|
1028
|
+
).toEqual([]);
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
it("🔴 eleven pages of body fail", () => {
|
|
1032
|
+
const fs = lint({
|
|
1033
|
+
venue: MSR,
|
|
1034
|
+
facts: msrFacts([...bodyPages(10), ...refsFrom(2)], 11),
|
|
1035
|
+
});
|
|
1036
|
+
expect(fs.map((f) => f.message)).toEqual([
|
|
1037
|
+
"body pages (up to the references on page 11, appendices included): 11, over the limit 10 for MSR/technical — a desk reject; cut the text",
|
|
1038
|
+
]);
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
it("🔴 an appendix before the references that brings the body to eleven fails", () => {
|
|
1042
|
+
const pages = [...bodyPages(9), "Appendix A.\nDetails\n", ...refsFrom(2)];
|
|
1043
|
+
expect(ids(lint({ venue: MSR, facts: msrFacts(pages, 11, 10) }))).toEqual([
|
|
1044
|
+
"format/page-limit:pages",
|
|
1045
|
+
]);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
it("🔴 an appendix after the references that brings the body to eleven fails", () => {
|
|
1049
|
+
const pages = [...bodyPages(9), ...refsFrom(1), "Appendix A.\nDetails\n"];
|
|
1050
|
+
const fs = lint({ venue: MSR, facts: msrFacts(pages, 10, 12) });
|
|
1051
|
+
expect(fs[0]?.message).toMatch(
|
|
1052
|
+
/^body pages \(up to the references on page 10, and the appendix from page 12\): 11, over the limit 10/,
|
|
1053
|
+
);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
it("🔴 three pages of only references fail", () => {
|
|
1057
|
+
const fs = lint({
|
|
1058
|
+
venue: MSR,
|
|
1059
|
+
facts: msrFacts([...bodyPages(9), ...refsFrom(3)], 10),
|
|
1060
|
+
});
|
|
1061
|
+
expect(fs.map((f) => f.message)).toEqual([
|
|
1062
|
+
"pages holding only references: 3, over the limit 2 for MSR/technical — a desk reject; cut the text",
|
|
1063
|
+
]);
|
|
1064
|
+
});
|
|
1065
|
+
});
|
|
1066
|
+
|
|
866
1067
|
/** An AIDC paper that declares who wrote it. */
|
|
867
1068
|
const BLIND = {
|
|
868
1069
|
extends: "paperlint:aidc",
|
|
@@ -1022,3 +1223,55 @@ describe("judgeBodyEnd, on a venue it does not apply to", () => {
|
|
|
1022
1223
|
expect(judgeBodyEnd(NO_TEXT, venue(null))).toEqual([]);
|
|
1023
1224
|
});
|
|
1024
1225
|
});
|
|
1226
|
+
|
|
1227
|
+
/** A paper on AIDC through `cycles`: one attempt, open unless `outcome` says otherwise. */
|
|
1228
|
+
const onCycle = (cycle: Record<string, unknown>) => ({
|
|
1229
|
+
identity: ["Ada Example"],
|
|
1230
|
+
cycles: [
|
|
1231
|
+
{
|
|
1232
|
+
id: "aidc-2026",
|
|
1233
|
+
venue: { kind: "preset", extends: "paperlint:aidc" },
|
|
1234
|
+
kind: "regular",
|
|
1235
|
+
opened: "2026-09-26",
|
|
1236
|
+
...cycle,
|
|
1237
|
+
},
|
|
1238
|
+
],
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
describe("the cycle decides what applies", () => {
|
|
1242
|
+
it("an open cycle reads as the flat form: the same findings on the same facts", () => {
|
|
1243
|
+
const facts = said((f) => (f.columns = 1));
|
|
1244
|
+
expect(ids(lint({ venue: onCycle({}), facts }))).toEqual(
|
|
1245
|
+
ids(lint({ venue: BLIND, facts })),
|
|
1246
|
+
);
|
|
1247
|
+
expect(ids(lint({ venue: onCycle({}), facts }))).toContain(
|
|
1248
|
+
"pdf/geometry:columns",
|
|
1249
|
+
);
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
it("`phase: porting` — the source is in the previous venue's template by declaration: every venue rule is silent", () => {
|
|
1253
|
+
const facts = said(
|
|
1254
|
+
(f) => ((f.columns = 1), (f.pages_text = ["Ada Example\n"])),
|
|
1255
|
+
);
|
|
1256
|
+
expect(lint({ venue: onCycle({ phase: "porting" }), facts })).toEqual([]);
|
|
1257
|
+
expect(lint({ venue: onCycle({ phase: "porting" }), facts: null })).toEqual(
|
|
1258
|
+
[],
|
|
1259
|
+
);
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
it("an ACCEPTED cycle at a blind venue: the camera-ready carries the authors, anonymity/identity is silent", () => {
|
|
1263
|
+
const facts = said(
|
|
1264
|
+
(f) =>
|
|
1265
|
+
(f.pages_text = ["Ada Example\n", ...bodyPages(11), "References\n"]),
|
|
1266
|
+
);
|
|
1267
|
+
const accepted = {
|
|
1268
|
+
kind: "accepted",
|
|
1269
|
+
date: "2026-10-16",
|
|
1270
|
+
evidence: "reviews/decision.md",
|
|
1271
|
+
};
|
|
1272
|
+
expect(ids(lint({ venue: onCycle({}), facts }))).toContain(
|
|
1273
|
+
"anonymity/identity:leak",
|
|
1274
|
+
);
|
|
1275
|
+
expect(lint({ venue: onCycle({ outcome: accepted }), facts })).toEqual([]);
|
|
1276
|
+
});
|
|
1277
|
+
});
|