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/submission.ts
CHANGED
|
@@ -30,7 +30,19 @@ import {
|
|
|
30
30
|
} from "./domain/submission.ts";
|
|
31
31
|
import type { Files } from "./ports/files.ts";
|
|
32
32
|
import type { SubmissionPortal } from "./ports/submission-portal.ts";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
paperPreset,
|
|
35
|
+
unusablePresetLine,
|
|
36
|
+
type PaperPreset,
|
|
37
|
+
type PresetDeps,
|
|
38
|
+
} from "./presets.ts";
|
|
39
|
+
import type { PaperSettings } from "./paper-settings.ts";
|
|
40
|
+
import { currentDeadlines } from "./domain/cycle.ts";
|
|
41
|
+
import {
|
|
42
|
+
deadlinesInForce,
|
|
43
|
+
type InForce,
|
|
44
|
+
type Observation,
|
|
45
|
+
} from "./domain/deadline.ts";
|
|
34
46
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
35
47
|
|
|
36
48
|
/** The environment variable each portal kind's token is read from. */
|
|
@@ -80,22 +92,31 @@ export interface SubmissionTarget {
|
|
|
80
92
|
readonly id: number;
|
|
81
93
|
/** The paper's built PDF, absolute. */
|
|
82
94
|
readonly pdf: string;
|
|
95
|
+
/** The venue's call for papers (the preset's `url`); null when the preset names no venue. */
|
|
96
|
+
readonly call: string | null;
|
|
97
|
+
/**
|
|
98
|
+
* The deadlines in force for the current attempt: the paper's own entries (with `cycles`) over the
|
|
99
|
+
* venue preset's readings. Instants and where each came from โ never how long is left.
|
|
100
|
+
*/
|
|
101
|
+
readonly deadlines: readonly InForce[];
|
|
83
102
|
}
|
|
84
103
|
|
|
85
104
|
const configOf = (dir: string): string => join(dir, CONFIG_FILE);
|
|
86
105
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
106
|
+
/** Why a paper without a venue preset has no portal: the key to set, or โ with cycles โ where. */
|
|
107
|
+
const noPresetLine = (
|
|
89
108
|
paperDir: string,
|
|
90
|
-
|
|
109
|
+
settings: PaperSettings | null,
|
|
110
|
+
): string =>
|
|
111
|
+
settings === null || settings.cycles === null
|
|
112
|
+
? `${configOf(paperDir)}: no "extends" โ the venue preset it names declares the portal ("portal": { "kind": โฆ, "url": โฆ })`
|
|
113
|
+
: `${configOf(paperDir)}: the current cycle names no venue preset, and the preset is what declares the portal โ set the venue in the current cycle ("venue": { "kind": "preset", "extends": "paperlint:โฆ" }), or upload by hand`;
|
|
114
|
+
|
|
115
|
+
/** A resolved paper โ its portal and submission, or the line naming the file and key to set. */
|
|
116
|
+
function targetOf(
|
|
117
|
+
paperDir: string,
|
|
118
|
+
p: Extract<PaperPreset, { readonly kind: "resolved" }>,
|
|
91
119
|
): Result<SubmissionTarget, string> {
|
|
92
|
-
const p = paperPreset(paperDir, deps);
|
|
93
|
-
const problem = paperPresetProblem(paperDir, p);
|
|
94
|
-
if (problem !== null) return err(problem);
|
|
95
|
-
if (p.kind !== "resolved")
|
|
96
|
-
return err(
|
|
97
|
-
`${configOf(paperDir)}: no "extends" โ the venue preset it names declares the portal ("portal": { "kind": โฆ, "url": โฆ })`,
|
|
98
|
-
);
|
|
99
120
|
const declared = p.preset.portal;
|
|
100
121
|
if (declared === null)
|
|
101
122
|
return err(
|
|
@@ -109,7 +130,31 @@ export function submissionTarget(
|
|
|
109
130
|
const portal = supportedPortal(declared);
|
|
110
131
|
if (!portal.ok) return portal;
|
|
111
132
|
const pdf = join(paperDir, p.settings.pdf ?? "paper.pdf");
|
|
112
|
-
|
|
133
|
+
const identity = p.preset.identity;
|
|
134
|
+
const call = identity.type === "venue" ? identity.url : null;
|
|
135
|
+
const own =
|
|
136
|
+
p.settings.cycles === null
|
|
137
|
+
? []
|
|
138
|
+
: currentDeadlines(p.settings.cycles.current);
|
|
139
|
+
const deadlines = deadlinesInForce(own, p.preset.deadlines);
|
|
140
|
+
return ok({ portal: portal.value, id: sub.id, pdf, call, deadlines });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The paper directory โ its portal and submission, or the line naming the file and key to set. */
|
|
144
|
+
export function submissionTarget(
|
|
145
|
+
paperDir: string,
|
|
146
|
+
deps: PresetDeps,
|
|
147
|
+
): Result<SubmissionTarget, string> {
|
|
148
|
+
const p = paperPreset(paperDir, deps);
|
|
149
|
+
switch (p.kind) {
|
|
150
|
+
case "none":
|
|
151
|
+
return err(noPresetLine(paperDir, p.settings));
|
|
152
|
+
case "settings-problem":
|
|
153
|
+
case "preset-problem":
|
|
154
|
+
return err(unusablePresetLine(paperDir, p));
|
|
155
|
+
case "resolved":
|
|
156
|
+
return targetOf(paperDir, p);
|
|
157
|
+
}
|
|
113
158
|
}
|
|
114
159
|
|
|
115
160
|
/** The token from the environment, or the line naming the variable and where to get one. */
|
|
@@ -162,6 +207,25 @@ const pdfLine = (view: SubmissionView): string =>
|
|
|
162
207
|
? "portal PDF none"
|
|
163
208
|
: `portal PDF ${view.pdf.hash}, ${view.pdf.size === null ? "?" : String(view.pdf.size)} bytes, uploaded ${iso(view.pdf.uploadedAt)}`;
|
|
164
209
|
|
|
210
|
+
const readingText = (o: Observation): string =>
|
|
211
|
+
`${o.source} ${o.at}, read ${o.read}`;
|
|
212
|
+
|
|
213
|
+
/** One deadline in force, what set it, and the other readings of it. Pure. */
|
|
214
|
+
export function deadlineLine(d: InForce): string {
|
|
215
|
+
const by = d.by;
|
|
216
|
+
const why =
|
|
217
|
+
by.kind === "override"
|
|
218
|
+
? `override: ${by.override.reason} (${by.override.evidence})`
|
|
219
|
+
: `${by.reading.source}, read ${by.reading.read}`;
|
|
220
|
+
const others = d.readings.filter(
|
|
221
|
+
(r) => by.kind === "override" || r !== by.reading,
|
|
222
|
+
);
|
|
223
|
+
const also = others.length
|
|
224
|
+
? `; also ${others.map(readingText).join("; ")}`
|
|
225
|
+
: "";
|
|
226
|
+
return `deadline ${d.what} ${d.at} โ ${why}${also}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
165
229
|
/** `show`'s report. Pure. */
|
|
166
230
|
export function showLines(
|
|
167
231
|
t: SubmissionTarget,
|
|
@@ -178,10 +242,19 @@ export function showLines(
|
|
|
178
242
|
pdfLine(view),
|
|
179
243
|
`local PDF ${local.shown} ${local.sha256 === null ? "(missing)" : `sha2-${local.sha256}`}`,
|
|
180
244
|
matchLine(view, local.sha256),
|
|
245
|
+
...t.deadlines.map(deadlineLine),
|
|
181
246
|
...view.messages.map(messageLine),
|
|
182
247
|
];
|
|
183
248
|
}
|
|
184
249
|
|
|
250
|
+
/**
|
|
251
|
+
* What `update` says before it sends a PDF, dry run or not: the author opens that file and checks it
|
|
252
|
+
* against the venue's call. paperlint's checks are an aid; the venue's reading of the PDF is what
|
|
253
|
+
* counts. Pure.
|
|
254
|
+
*/
|
|
255
|
+
export const checkPdfLine = (shown: string, call: string | null): string =>
|
|
256
|
+
`before you rely on it, open ${shown} yourself and check it against ${call === null ? "the venue's call for papers" : `the call for papers, ${call}`}: the page count and what counts toward the limit, the template and its class options, anonymity, and that every figure, table and reference renders โ paperlint passing is not the venue accepting the format`;
|
|
257
|
+
|
|
185
258
|
/** `update`'s report. Pure. */
|
|
186
259
|
export function updateLines(
|
|
187
260
|
o: UpdateOutcome,
|
|
@@ -243,6 +316,17 @@ function abstractOf(
|
|
|
243
316
|
: ok(new TextDecoder().decode(bytes).trim());
|
|
244
317
|
}
|
|
245
318
|
|
|
319
|
+
/** What `update` sends: the PDF's bytes, the abstract's text, and whether to mark it submitted. */
|
|
320
|
+
const changeOf = (
|
|
321
|
+
a: SubmissionArgs & { readonly pdfPath: string },
|
|
322
|
+
bytes: Uint8Array,
|
|
323
|
+
abstract: string | null,
|
|
324
|
+
) => ({
|
|
325
|
+
pdf: { name: basename(a.pdfPath), bytes },
|
|
326
|
+
abstract,
|
|
327
|
+
submit: a.submit,
|
|
328
|
+
});
|
|
329
|
+
|
|
246
330
|
/** `update`: send the PDF (and the abstract, and "submitted" when asked); a dry run unless saving. */
|
|
247
331
|
async function runUpdate(
|
|
248
332
|
t: SubmissionTarget,
|
|
@@ -259,12 +343,10 @@ async function runUpdate(
|
|
|
259
343
|
);
|
|
260
344
|
const abstract = abstractOf(a, deps);
|
|
261
345
|
if (!abstract.ok) return (deps.err(abstract.error), 2);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
};
|
|
267
|
-
const r = await portal.update(t.id, change, { save: a.save });
|
|
346
|
+
deps.log(checkPdfLine(shown, t.call));
|
|
347
|
+
const r = await portal.update(t.id, changeOf(a, bytes, abstract.value), {
|
|
348
|
+
save: a.save,
|
|
349
|
+
});
|
|
268
350
|
if (!r.ok) return (deps.err(failureText(r.error)), 1);
|
|
269
351
|
const local = { shown, sha256: sha256Hex(bytes) };
|
|
270
352
|
updateLines(r.value, local, a.save).forEach((l) => {
|
package/src/tex-requirements.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type VenueTalkJson,
|
|
36
36
|
} from "./domain/talk.ts";
|
|
37
37
|
import type { VenuePortal } from "./domain/submission.ts";
|
|
38
|
+
import type { Reading } from "./domain/deadline.ts";
|
|
38
39
|
|
|
39
40
|
/** CTAN package name โ the names that prove it is installed. */
|
|
40
41
|
export type PackageProofs = Readonly<Record<string, readonly string[]>>;
|
|
@@ -140,6 +141,11 @@ export interface VenueFormat {
|
|
|
140
141
|
readonly fontsTitle: string | null;
|
|
141
142
|
/** How the body is counted: null is banal's count; `references`, the pages before the references. */
|
|
142
143
|
readonly bodyEndsAt: "references" | null;
|
|
144
|
+
/**
|
|
145
|
+
* With `bodyEndsAt`: appendices count as body wherever they sit, and the reference pages are the
|
|
146
|
+
* ones holding only references. Null: the appendix ends the body, as `references` alone counts it.
|
|
147
|
+
*/
|
|
148
|
+
readonly appendixInBody: boolean | null;
|
|
143
149
|
readonly kinds: ReadonlyMap<string, KindLimits>;
|
|
144
150
|
}
|
|
145
151
|
|
|
@@ -155,6 +161,7 @@ export const NO_FORMAT: VenueFormat = {
|
|
|
155
161
|
fontsText: null,
|
|
156
162
|
fontsTitle: null,
|
|
157
163
|
bodyEndsAt: null,
|
|
164
|
+
appendixInBody: null,
|
|
158
165
|
kinds: new Map(),
|
|
159
166
|
};
|
|
160
167
|
|
|
@@ -189,10 +196,14 @@ export interface PresetFile {
|
|
|
189
196
|
readonly extends: string | null;
|
|
190
197
|
/** The `\\documentclass` the venue's template uses, as the file spells it; null when it names none. */
|
|
191
198
|
readonly template: string | null;
|
|
199
|
+
/** Class options the venue forbids, read with `template`; empty when the file names none. */
|
|
200
|
+
readonly templateForbids: readonly string[];
|
|
192
201
|
/** Other names the venue goes by in a paper's text; empty when the file names none. */
|
|
193
202
|
readonly aliases: readonly string[];
|
|
194
203
|
/** Whether the venue reviews double-blind; null when the file does not say. */
|
|
195
204
|
readonly blind: boolean | null;
|
|
205
|
+
/** When the venue's names count as a mention in a paper's text; null when the file does not say. */
|
|
206
|
+
readonly mentions: "with-year" | null;
|
|
196
207
|
/** Sections the venue requires; null when the file names none (a child's list replaces its parent's). */
|
|
197
208
|
readonly requiredSections: readonly RequiredSection[] | null;
|
|
198
209
|
/** Null when the file declares no `tex` block (allowed only with `extends`). */
|
|
@@ -209,6 +220,8 @@ export interface PresetFile {
|
|
|
209
220
|
readonly talk: VenueTalk | null;
|
|
210
221
|
/** Where the venue takes submissions (`paperlint submission`); null when the file does not say. */
|
|
211
222
|
readonly portal: VenuePortal | null;
|
|
223
|
+
/** The edition's deadlines as they were read; null when the file names none (a child's list replaces its parent's). */
|
|
224
|
+
readonly deadlines: readonly Reading[] | null;
|
|
212
225
|
}
|
|
213
226
|
|
|
214
227
|
type KindsJson = Readonly<
|
|
@@ -227,6 +240,7 @@ interface FormatJson {
|
|
|
227
240
|
readonly fonts_text?: string;
|
|
228
241
|
readonly fonts_title?: string;
|
|
229
242
|
readonly body_ends_at?: "references";
|
|
243
|
+
readonly appendix_in_body?: boolean;
|
|
230
244
|
readonly kinds?: KindsJson;
|
|
231
245
|
}
|
|
232
246
|
|
|
@@ -244,8 +258,10 @@ type PresetJson = PresetBodyJson &
|
|
|
244
258
|
interface PresetBodyJson {
|
|
245
259
|
readonly extends?: string;
|
|
246
260
|
readonly template?: string;
|
|
261
|
+
readonly template_forbids?: readonly string[];
|
|
247
262
|
readonly aliases?: readonly string[];
|
|
248
263
|
readonly blind?: boolean;
|
|
264
|
+
readonly mentions?: "with-year";
|
|
249
265
|
readonly required_sections?: readonly {
|
|
250
266
|
readonly title: string;
|
|
251
267
|
readonly position?: "last";
|
|
@@ -263,6 +279,7 @@ interface PresetBodyJson {
|
|
|
263
279
|
};
|
|
264
280
|
readonly talk?: VenueTalkJson;
|
|
265
281
|
readonly portal?: VenuePortal;
|
|
282
|
+
readonly deadlines?: readonly Reading[];
|
|
266
283
|
}
|
|
267
284
|
|
|
268
285
|
/** An optional field as the typed preset holds it: absent is null. */
|
|
@@ -292,6 +309,7 @@ function formatOf(j: FormatJson = {}): VenueFormat {
|
|
|
292
309
|
fontsText: orNull(j.fonts_text),
|
|
293
310
|
fontsTitle: orNull(j.fonts_title),
|
|
294
311
|
bodyEndsAt: orNull(j.body_ends_at),
|
|
312
|
+
appendixInBody: orNull(j.appendix_in_body),
|
|
295
313
|
kinds: kindsOf(j.kinds),
|
|
296
314
|
};
|
|
297
315
|
}
|
|
@@ -376,6 +394,36 @@ const anchorsOf = (
|
|
|
376
394
|
},
|
|
377
395
|
}));
|
|
378
396
|
|
|
397
|
+
/** A preset's `portal`, or null when it names none. */
|
|
398
|
+
const portalOf = (p: PresetJson["portal"]): Readonly<VenuePortal> | null =>
|
|
399
|
+
p === undefined ? null : { kind: p.kind, url: p.url };
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* A preset's `deadlines`, or null when it names none. Two readings of one kind from one source are
|
|
403
|
+
* refused: the schema cannot say it, and a paper could not tell which binds.
|
|
404
|
+
*/
|
|
405
|
+
function deadlinesOf(
|
|
406
|
+
d: PresetJson["deadlines"],
|
|
407
|
+
file: string,
|
|
408
|
+
): readonly Reading[] | null {
|
|
409
|
+
if (d === undefined) return null;
|
|
410
|
+
const twice = d.find(
|
|
411
|
+
(r, i) =>
|
|
412
|
+
d.findIndex((x) => x.what === r.what && x.source === r.source) !== i,
|
|
413
|
+
);
|
|
414
|
+
if (twice !== undefined)
|
|
415
|
+
throw new Error(
|
|
416
|
+
`${file}: "deadlines" holds two "${twice.source}" readings of "${twice.what}" โ keep one, the latest`,
|
|
417
|
+
);
|
|
418
|
+
return d.map(({ what, at, source, url, read }) => ({
|
|
419
|
+
what,
|
|
420
|
+
at,
|
|
421
|
+
source,
|
|
422
|
+
url,
|
|
423
|
+
read,
|
|
424
|
+
}));
|
|
425
|
+
}
|
|
426
|
+
|
|
379
427
|
/**
|
|
380
428
|
* The text of one preset file โ the typed file, or an Error naming every problem. The ONE parser
|
|
381
429
|
* of a preset: the toolchain reads its `tex`, the venue rules its `format`, the config its `rules`.
|
|
@@ -400,18 +448,18 @@ export function parsePreset(
|
|
|
400
448
|
identity: identityOf(j),
|
|
401
449
|
extends: orNull(j.extends),
|
|
402
450
|
template: orNull(j.template),
|
|
451
|
+
templateForbids: j.template_forbids ?? [],
|
|
403
452
|
aliases: j.aliases ?? [],
|
|
404
453
|
blind: orNull(j.blind),
|
|
454
|
+
mentions: orNull(j.mentions),
|
|
405
455
|
requiredSections: sectionsOf(j.required_sections),
|
|
406
456
|
tex: texOf(j.tex),
|
|
407
457
|
format: formatOf(j.format),
|
|
408
458
|
rules: j.rules ?? {},
|
|
409
459
|
registerAnchors: anchorsOf(j.register),
|
|
410
460
|
talk: j.talk === undefined ? null : venueTalkOf(j.talk),
|
|
411
|
-
portal:
|
|
412
|
-
|
|
413
|
-
? null
|
|
414
|
-
: { kind: j.portal.kind, url: j.portal.url },
|
|
461
|
+
portal: portalOf(j.portal),
|
|
462
|
+
deadlines: deadlinesOf(j.deadlines, file),
|
|
415
463
|
};
|
|
416
464
|
}
|
|
417
465
|
|
|
@@ -10,6 +10,7 @@ import { memoryFiles } from "./adapters/memory/index.ts";
|
|
|
10
10
|
import { presetsDir } from "./package-dirs.ts";
|
|
11
11
|
import { latexReader } from "./adapters/latex/index.ts";
|
|
12
12
|
import {
|
|
13
|
+
cyclePortRules,
|
|
13
14
|
judgeRequiredSections,
|
|
14
15
|
judgeTemplate,
|
|
15
16
|
otherVenues,
|
|
@@ -77,6 +78,9 @@ function lint(
|
|
|
77
78
|
...Object.entries(formatRules(deps)).map(
|
|
78
79
|
([n, r]) => [`format/${n}`, r] as const,
|
|
79
80
|
),
|
|
81
|
+
...Object.entries(cyclePortRules(deps)).map(
|
|
82
|
+
([n, r]) => [`cycle/${n}`, r] as const,
|
|
83
|
+
),
|
|
80
84
|
]) {
|
|
81
85
|
const visitor = rule.create({
|
|
82
86
|
filename,
|
|
@@ -302,6 +306,78 @@ describe("tex/template โ the shipped ACM and ACL templates", () => {
|
|
|
302
306
|
});
|
|
303
307
|
});
|
|
304
308
|
|
|
309
|
+
const PORTING = {
|
|
310
|
+
id: "aidc-2026",
|
|
311
|
+
venue: { kind: "preset", extends: "paperlint:aidc" },
|
|
312
|
+
kind: "regular",
|
|
313
|
+
opened: "2026-09-26",
|
|
314
|
+
phase: "porting",
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
describe("tex/template โ a cycle in `porting`", () => {
|
|
318
|
+
it("the class line is the previous venue's by declaration: silent, where the flat form reports", () => {
|
|
319
|
+
const src = paper("\\documentclass[sigconf]{acmart}");
|
|
320
|
+
expect(lint(src, AIDC).map((f) => f.messageId)).toEqual(["wrongClass"]);
|
|
321
|
+
expect(lint(src, { cycles: [PORTING] })).toEqual([]);
|
|
322
|
+
expect(
|
|
323
|
+
lint(src, { cycles: [{ ...PORTING, phase: "prepared" }] }).map(
|
|
324
|
+
(f) => f.messageId,
|
|
325
|
+
),
|
|
326
|
+
).toEqual(["wrongClass"]);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("cycle/port-done โ the declared port is finished", () => {
|
|
331
|
+
it("porting declared and the class line already the venue's: the one finding, naming the cycle", () => {
|
|
332
|
+
const fs = lint(OFFICIAL, { cycles: [PORTING] });
|
|
333
|
+
expect(fs.map((f) => `${f.rule}:${f.messageId}`)).toEqual([
|
|
334
|
+
"cycle/port-done:done",
|
|
335
|
+
]);
|
|
336
|
+
expect(fs[0]?.message).toMatch(
|
|
337
|
+
/already `\\documentclass\[conference,compsoc\]\{IEEEtran\}`, which AIDC requires, and the cycle ยซaidc-2026ยป still declares "phase": "porting"/,
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("silent while the class line is still the old venue's, once the phase is dropped, and on the flat form", () => {
|
|
342
|
+
const old = paper("\\documentclass[sigconf]{acmart}");
|
|
343
|
+
expect(lint(old, { cycles: [PORTING] })).toEqual([]);
|
|
344
|
+
expect(
|
|
345
|
+
lint(OFFICIAL, { cycles: [{ ...PORTING, phase: "prepared" }] }),
|
|
346
|
+
).toEqual([]);
|
|
347
|
+
expect(lint(OFFICIAL, AIDC)).toEqual([]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("judges the text when the source carries no `raw`", () => {
|
|
351
|
+
expect(
|
|
352
|
+
lint(OFFICIAL, { cycles: [PORTING] }, { raw: false }).map(
|
|
353
|
+
(f) => `${f.rule}:${f.messageId}`,
|
|
354
|
+
),
|
|
355
|
+
).toContain("cycle/port-done:done");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("silent when the venue names no template, and when the preset does not resolve", () => {
|
|
359
|
+
const own = `${PAPER}/own.jsonc`;
|
|
360
|
+
const porting = (ext: string) => ({
|
|
361
|
+
cycles: [{ ...PORTING, venue: { kind: "preset", extends: ext } }],
|
|
362
|
+
});
|
|
363
|
+
expect(
|
|
364
|
+
lint(OFFICIAL, porting("./own.jsonc"), {
|
|
365
|
+
extra: {
|
|
366
|
+
[own]: JSON.stringify({
|
|
367
|
+
type: "family",
|
|
368
|
+
tex: { packages: { x: ["x"] } },
|
|
369
|
+
}),
|
|
370
|
+
},
|
|
371
|
+
}).filter((f) => f.rule === "cycle/port-done"),
|
|
372
|
+
).toEqual([]);
|
|
373
|
+
expect(
|
|
374
|
+
lint(OFFICIAL, porting("./missing.jsonc")).filter(
|
|
375
|
+
(f) => f.rule === "cycle/port-done",
|
|
376
|
+
),
|
|
377
|
+
).toEqual([]);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
305
381
|
describe("tex/template โ a project's own preset, and nothing to judge", () => {
|
|
306
382
|
it("a preset with no template: nothing to compare, no finding", () => {
|
|
307
383
|
const own = `${PAPER}/own.jsonc`;
|
|
@@ -322,6 +398,119 @@ describe("tex/template โ a project's own preset, and nothing to judge", () =>
|
|
|
322
398
|
});
|
|
323
399
|
});
|
|
324
400
|
|
|
401
|
+
/** A project's own venue on IEEEtran's conference class that forbids the two compsoc options. */
|
|
402
|
+
const forbidding = (body: Record<string, unknown> = {}) => ({
|
|
403
|
+
[`${PAPER}/own.jsonc`]: JSON.stringify(
|
|
404
|
+
venuePreset("Own", {
|
|
405
|
+
extends: "paperlint:ieee-conference",
|
|
406
|
+
template: "\\documentclass[10pt,conference]{IEEEtran}",
|
|
407
|
+
template_forbids: ["compsoc", "compsocconf"],
|
|
408
|
+
...body,
|
|
409
|
+
}),
|
|
410
|
+
),
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe("tex/template โ class options the preset forbids (`template_forbids`)", () => {
|
|
414
|
+
const judge = (cls: string, extra = forbidding()) =>
|
|
415
|
+
lint(paper(cls, "Text."), { extends: "./own.jsonc" }, { extra });
|
|
416
|
+
|
|
417
|
+
it("the template's own line passes, and so does an extra option the preset does not forbid", () => {
|
|
418
|
+
expect(judge("\\documentclass[10pt,conference]{IEEEtran}")).toEqual([]);
|
|
419
|
+
expect(
|
|
420
|
+
judge("\\documentclass[10pt,conference,letterpaper]{IEEEtran}"),
|
|
421
|
+
).toEqual([]);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("๐ด a forbidden option is reported at the class line, naming it and the venue's line", () => {
|
|
425
|
+
const fs = judge("\\documentclass[10pt,conference,compsoc]{IEEEtran}");
|
|
426
|
+
expect(ids(fs)).toEqual(["tex/template:forbiddenOption"]);
|
|
427
|
+
expect(fs[0]?.message).toBe(
|
|
428
|
+
"the class option `compsoc` is forbidden: Own requires `\\documentclass[10pt,conference]{IEEEtran}` without `compsoc`, `compsocconf`",
|
|
429
|
+
);
|
|
430
|
+
expect(fs[0]?.line).toBe(2);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("each forbidden option is one finding, beside a missing one", () => {
|
|
434
|
+
expect(
|
|
435
|
+
ids(judge("\\documentclass[compsoc,compsocconf,conference]{IEEEtran}")),
|
|
436
|
+
).toEqual([
|
|
437
|
+
"tex/template:forbiddenOption",
|
|
438
|
+
"tex/template:forbiddenOption",
|
|
439
|
+
"tex/template:missingOption",
|
|
440
|
+
]);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
describe("tex/template under paperlint:msr โ `[10pt,conference]`, without compsoc or compsocconf", () => {
|
|
445
|
+
const MSR = { extends: "paperlint:msr", kind: "technical" };
|
|
446
|
+
|
|
447
|
+
it("the class line MSR's call names passes", () => {
|
|
448
|
+
expect(
|
|
449
|
+
lint(paper("\\documentclass[10pt,conference]{IEEEtran}", "Text."), MSR),
|
|
450
|
+
).toEqual([]);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it.each([["compsoc"], ["compsocconf"]])(
|
|
454
|
+
"๐ด `%s` in the class options fails tex/template",
|
|
455
|
+
(option) => {
|
|
456
|
+
const fs = lint(
|
|
457
|
+
paper(`\\documentclass[10pt,conference,${option}]{IEEEtran}`, "Text."),
|
|
458
|
+
MSR,
|
|
459
|
+
);
|
|
460
|
+
expect(ids(fs)).toEqual(["tex/template:forbiddenOption"]);
|
|
461
|
+
expect(fs[0]?.message).toMatch(
|
|
462
|
+
`the class option \`${option}\` is forbidden: MSR requires`,
|
|
463
|
+
);
|
|
464
|
+
},
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
it("AIDC's class line fails MSR on both counts: compsoc forbidden, 10pt missing", () => {
|
|
468
|
+
expect(ids(lint(OFFICIAL, MSR))).toEqual([
|
|
469
|
+
"tex/template:forbiddenOption",
|
|
470
|
+
"tex/template:missingOption",
|
|
471
|
+
]);
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe("tex/template โ forbidden options behind a switch, and down a chain", () => {
|
|
476
|
+
it("behind a TeX switch, a candidate with a forbidden option is not the venue's class", () => {
|
|
477
|
+
const two = (a: string, b: string) =>
|
|
478
|
+
`\\def\\v{1}\n\\if\\v1 ${a} \\fi\n\\if\\v2 ${b} \\fi\n\\begin{document}\nText.\n\\end{document}\n`;
|
|
479
|
+
const compsoc = "\\documentclass[10pt,conference,compsoc]{IEEEtran}";
|
|
480
|
+
const opts = { extra: forbidding() };
|
|
481
|
+
expect(
|
|
482
|
+
ids(lint(two(compsoc, compsoc), { extends: "./own.jsonc" }, opts)),
|
|
483
|
+
).toEqual(["tex/template:noCandidate"]);
|
|
484
|
+
expect(
|
|
485
|
+
lint(
|
|
486
|
+
two(compsoc, "\\documentclass[10pt,conference]{IEEEtran}"),
|
|
487
|
+
{ extends: "./own.jsonc" },
|
|
488
|
+
opts,
|
|
489
|
+
),
|
|
490
|
+
).toEqual([]);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("a child's template replaces its parent's, the forbidden options with it", () => {
|
|
494
|
+
const child = `${PAPER}/child.jsonc`;
|
|
495
|
+
const extra = {
|
|
496
|
+
...forbidding(),
|
|
497
|
+
[child]: JSON.stringify(
|
|
498
|
+
venuePreset("Child", {
|
|
499
|
+
extends: "./own.jsonc",
|
|
500
|
+
template: "\\documentclass[conference]{IEEEtran}",
|
|
501
|
+
}),
|
|
502
|
+
),
|
|
503
|
+
};
|
|
504
|
+
expect(
|
|
505
|
+
lint(
|
|
506
|
+
paper("\\documentclass[conference,compsoc]{IEEEtran}", "Text."),
|
|
507
|
+
{ extends: "./child.jsonc" },
|
|
508
|
+
{ extra },
|
|
509
|
+
),
|
|
510
|
+
).toEqual([]);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
325
514
|
describe("tex/template โ a template the reader cannot read, a bare class name, nothing to judge", () => {
|
|
326
515
|
it("a template the reader cannot read is said once, naming its file โ the class is not judged", () => {
|
|
327
516
|
const own = `${PAPER}/own.jsonc`;
|
|
@@ -580,6 +769,53 @@ describe("tex/venue-leftover โ another venue named in the text", () => {
|
|
|
580
769
|
});
|
|
581
770
|
});
|
|
582
771
|
|
|
772
|
+
describe("tex/venue-leftover โ MSR, a name that is also an acronym, counts only with a year (`mentions`)", () => {
|
|
773
|
+
const under = (text: string) =>
|
|
774
|
+
leftovers(lint(aidcPaper(`${text}\n${BODY}${STATEMENT}`), AIDC));
|
|
775
|
+
|
|
776
|
+
it.each([
|
|
777
|
+
[
|
|
778
|
+
"an acronym the paper defines",
|
|
779
|
+
"We score it by Manual Speech Recognition (MSR), and MSR scores rise.",
|
|
780
|
+
],
|
|
781
|
+
[
|
|
782
|
+
"Microsoft Research",
|
|
783
|
+
"Researchers at Microsoft Research (MSR) built it, and MSR released it.",
|
|
784
|
+
],
|
|
785
|
+
[
|
|
786
|
+
"a citation of an MSR paper: the key, and the entry in the bibliography",
|
|
787
|
+
"As shown~\\cite{msr2024}.\n\\begin{thebibliography}{1}\\bibitem{msr2024} A. Author, ``Mining,'' in \\emph{Proc. MSR 2024}, 2024.\\end{thebibliography}",
|
|
788
|
+
],
|
|
789
|
+
["a year that is not beside the name", "MSR, in 2027, scored best."],
|
|
790
|
+
])("silent: %s", (_, text) => {
|
|
791
|
+
expect(under(text)).toEqual([]);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it.each([
|
|
795
|
+
["First written for MSR 2027.", "MSR"],
|
|
796
|
+
["A version appeared as MSR'27 work.", "MSR"],
|
|
797
|
+
["Submitted to MSR~2027.", "MSR"],
|
|
798
|
+
])("๐ด reported: ยซ%sยป", (text, name) => {
|
|
799
|
+
const fs = under(text);
|
|
800
|
+
expect(fs.map((f) => f.message)).toEqual([
|
|
801
|
+
expect.stringMatching(
|
|
802
|
+
new RegExp(`^ยซ${name}ยป names MSR, and this paper extends AIDC`),
|
|
803
|
+
),
|
|
804
|
+
]);
|
|
805
|
+
expect(fs[0]?.line).toBe(4);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("under msr itself, its own name with a year is never a leftover", () => {
|
|
809
|
+
const tex = paper(
|
|
810
|
+
"\\documentclass[10pt,conference]{IEEEtran}",
|
|
811
|
+
"First written for MSR 2027.",
|
|
812
|
+
);
|
|
813
|
+
expect(
|
|
814
|
+
leftovers(lint(tex, { extends: "paperlint:msr", kind: "technical" })),
|
|
815
|
+
).toEqual([]);
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
|
|
583
819
|
describe("tex/venue-leftover โ a URL is not text, a definition body is", () => {
|
|
584
820
|
it("\\href: a name in the URL is not reported, a name in the link text is", () => {
|
|
585
821
|
const inUrl = aidcPaper(
|
|
@@ -736,6 +972,7 @@ describe("otherVenues", () => {
|
|
|
736
972
|
"acm-sigconf",
|
|
737
973
|
"AgenticDev",
|
|
738
974
|
"AISec",
|
|
975
|
+
"MSR",
|
|
739
976
|
]);
|
|
740
977
|
});
|
|
741
978
|
});
|