paperlint 7.1.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 +6 -0
- 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 +40 -5
- 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/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-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 +12 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +19 -11
- package/dist/presets.js.map +1 -1
- package/dist/submission.d.ts +8 -0
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +41 -10
- package/dist/submission.js.map +1 -1
- package/dist/tex-requirements.d.ts +3 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +19 -0
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +19 -0
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +69 -0
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/venue-rules.d.ts +10 -0
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +26 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +139 -11
- 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.md +48 -43
- package/docs/submission.md +5 -0
- package/eslint-rules/cycle-stage.test.mjs +131 -0
- package/eslint-rules/cycle-stage.ts +141 -0
- package/eslint.config.mjs +41 -1
- 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/msr.jsonc +25 -1
- package/presets/msr.md +5 -1
- package/presets/venue-profile.schema.json +39 -0
- package/scripts/check.ts +4 -0
- package/scripts/refresh-deadlines.test.ts +431 -0
- package/scripts/refresh-deadlines.ts +387 -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 +43 -4
- package/src/cycle-rules.test.ts +278 -0
- package/src/cycle-rules.ts +273 -0
- 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 +1 -0
- 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/presets.test.ts +105 -0
- package/src/presets.ts +43 -10
- package/src/rule-cases.test.ts +144 -0
- package/src/submission.test.ts +105 -1
- package/src/submission.ts +73 -12
- package/src/tex-requirements.ts +31 -0
- package/src/tex-venue-rules.test.ts +76 -0
- package/src/tex-venue-rules.ts +85 -0
- package/src/venue-rules.test.ts +82 -0
- package/src/venue-rules.ts +44 -12
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. */
|
|
@@ -82,22 +94,29 @@ export interface SubmissionTarget {
|
|
|
82
94
|
readonly pdf: string;
|
|
83
95
|
/** The venue's call for papers (the preset's `url`); null when the preset names no venue. */
|
|
84
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[];
|
|
85
102
|
}
|
|
86
103
|
|
|
87
104
|
const configOf = (dir: string): string => join(dir, CONFIG_FILE);
|
|
88
105
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
106
|
+
/** Why a paper without a venue preset has no portal: the key to set, or — with cycles — where. */
|
|
107
|
+
const noPresetLine = (
|
|
91
108
|
paperDir: string,
|
|
92
|
-
|
|
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" }>,
|
|
93
119
|
): Result<SubmissionTarget, string> {
|
|
94
|
-
const p = paperPreset(paperDir, deps);
|
|
95
|
-
const problem = paperPresetProblem(paperDir, p);
|
|
96
|
-
if (problem !== null) return err(problem);
|
|
97
|
-
if (p.kind !== "resolved")
|
|
98
|
-
return err(
|
|
99
|
-
`${configOf(paperDir)}: no "extends" — the venue preset it names declares the portal ("portal": { "kind": …, "url": … })`,
|
|
100
|
-
);
|
|
101
120
|
const declared = p.preset.portal;
|
|
102
121
|
if (declared === null)
|
|
103
122
|
return err(
|
|
@@ -113,7 +132,29 @@ export function submissionTarget(
|
|
|
113
132
|
const pdf = join(paperDir, p.settings.pdf ?? "paper.pdf");
|
|
114
133
|
const identity = p.preset.identity;
|
|
115
134
|
const call = identity.type === "venue" ? identity.url : null;
|
|
116
|
-
|
|
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
|
+
}
|
|
117
158
|
}
|
|
118
159
|
|
|
119
160
|
/** The token from the environment, or the line naming the variable and where to get one. */
|
|
@@ -166,6 +207,25 @@ const pdfLine = (view: SubmissionView): string =>
|
|
|
166
207
|
? "portal PDF none"
|
|
167
208
|
: `portal PDF ${view.pdf.hash}, ${view.pdf.size === null ? "?" : String(view.pdf.size)} bytes, uploaded ${iso(view.pdf.uploadedAt)}`;
|
|
168
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
|
+
|
|
169
229
|
/** `show`'s report. Pure. */
|
|
170
230
|
export function showLines(
|
|
171
231
|
t: SubmissionTarget,
|
|
@@ -182,6 +242,7 @@ export function showLines(
|
|
|
182
242
|
pdfLine(view),
|
|
183
243
|
`local PDF ${local.shown} ${local.sha256 === null ? "(missing)" : `sha2-${local.sha256}`}`,
|
|
184
244
|
matchLine(view, local.sha256),
|
|
245
|
+
...t.deadlines.map(deadlineLine),
|
|
185
246
|
...view.messages.map(messageLine),
|
|
186
247
|
];
|
|
187
248
|
}
|
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[]>>;
|
|
@@ -219,6 +220,8 @@ export interface PresetFile {
|
|
|
219
220
|
readonly talk: VenueTalk | null;
|
|
220
221
|
/** Where the venue takes submissions (`paperlint submission`); null when the file does not say. */
|
|
221
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;
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
type KindsJson = Readonly<
|
|
@@ -276,6 +279,7 @@ interface PresetBodyJson {
|
|
|
276
279
|
};
|
|
277
280
|
readonly talk?: VenueTalkJson;
|
|
278
281
|
readonly portal?: VenuePortal;
|
|
282
|
+
readonly deadlines?: readonly Reading[];
|
|
279
283
|
}
|
|
280
284
|
|
|
281
285
|
/** An optional field as the typed preset holds it: absent is null. */
|
|
@@ -394,6 +398,32 @@ const anchorsOf = (
|
|
|
394
398
|
const portalOf = (p: PresetJson["portal"]): Readonly<VenuePortal> | null =>
|
|
395
399
|
p === undefined ? null : { kind: p.kind, url: p.url };
|
|
396
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
|
+
|
|
397
427
|
/**
|
|
398
428
|
* The text of one preset file → the typed file, or an Error naming every problem. The ONE parser
|
|
399
429
|
* of a preset: the toolchain reads its `tex`, the venue rules its `format`, the config its `rules`.
|
|
@@ -429,6 +459,7 @@ export function parsePreset(
|
|
|
429
459
|
registerAnchors: anchorsOf(j.register),
|
|
430
460
|
talk: j.talk === undefined ? null : venueTalkOf(j.talk),
|
|
431
461
|
portal: portalOf(j.portal),
|
|
462
|
+
deadlines: deadlinesOf(j.deadlines, file),
|
|
432
463
|
};
|
|
433
464
|
}
|
|
434
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`;
|
package/src/tex-venue-rules.ts
CHANGED
|
@@ -52,6 +52,7 @@ import type { PaperSource } from "./domain/paper-source.ts";
|
|
|
52
52
|
import { readPaper, reportInPaper } from "./tex-paper.ts";
|
|
53
53
|
import type { RequiredSection } from "./tex-requirements.ts";
|
|
54
54
|
import type { Finding, VenueRuleDeps } from "./venue-rules.ts";
|
|
55
|
+
import { isPorting } from "./domain/cycle.ts";
|
|
55
56
|
|
|
56
57
|
/** A finding and where it points; null points at the top of the file. */
|
|
57
58
|
export interface Located extends Finding {
|
|
@@ -493,6 +494,10 @@ function readingOf(
|
|
|
493
494
|
const dir = dirname(filename);
|
|
494
495
|
const p = paperPreset(dir, deps);
|
|
495
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;
|
|
496
501
|
const preset = p.preset;
|
|
497
502
|
const others = () => otherVenues(preset, deps);
|
|
498
503
|
const paper = readPaper(filename, src, deps);
|
|
@@ -538,3 +543,83 @@ export function formatRules(
|
|
|
538
543
|
): Readonly<Record<"layout-override", TexRuleModule>> {
|
|
539
544
|
return { "layout-override": rule("layout-override", deps) };
|
|
540
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", () => {
|
|
@@ -1193,3 +1223,55 @@ describe("judgeBodyEnd, on a venue it does not apply to", () => {
|
|
|
1193
1223
|
expect(judgeBodyEnd(NO_TEXT, venue(null))).toEqual([]);
|
|
1194
1224
|
});
|
|
1195
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
|
+
});
|
package/src/venue-rules.ts
CHANGED
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
* no geometry (no banal) pdf/measured (warn); geometry and body-size silent, page-limit judges
|
|
38
38
|
* only a body counted before the references; fonts still judge
|
|
39
39
|
* kind does not resolve pdf/profile (error); page-limit silent; everything else still judges
|
|
40
|
+
* cycle phase `porting` every rule silent — the source is in the previous venue's template by
|
|
41
|
+
* declaration (src/domain/cycle.ts); cycle/port-done watches the declaration
|
|
40
42
|
*/
|
|
41
43
|
import { dirname, isAbsolute, join, basename, relative } from "node:path";
|
|
42
44
|
import {
|
|
@@ -65,6 +67,7 @@ import {
|
|
|
65
67
|
type PageSplit,
|
|
66
68
|
} from "./domain/body-pages.ts";
|
|
67
69
|
import type { TextFacts } from "./facts-file.ts";
|
|
70
|
+
import { effectiveBlind, isPorting } from "./domain/cycle.ts";
|
|
68
71
|
|
|
69
72
|
// ── the verdict's vocabulary ─────────────────────────────────────────────────────────
|
|
70
73
|
|
|
@@ -96,8 +99,19 @@ export interface Resolved {
|
|
|
96
99
|
export type Assessment =
|
|
97
100
|
| { readonly kind: "no-venue" }
|
|
98
101
|
/** A `paperlint.json` that extends no preset yet — `pdf/measured` names the file to set. */
|
|
99
|
-
| {
|
|
102
|
+
| {
|
|
103
|
+
readonly kind: "no-preset";
|
|
104
|
+
readonly file: string;
|
|
105
|
+
/** The paper keeps `cycles`: its venue is the current cycle's, and `extends` beside them is refused. */
|
|
106
|
+
readonly cycles: boolean;
|
|
107
|
+
}
|
|
100
108
|
| { readonly kind: "unresolved"; readonly finding: Finding }
|
|
109
|
+
/**
|
|
110
|
+
* The current cycle declares its port to the venue's template as open work (`"phase": "porting"`):
|
|
111
|
+
* a PDF built with the previous venue's template says nothing about this venue's limits, so every
|
|
112
|
+
* venue rule is silent, and `cycle/port-done` speaks when the port is done and the phase still says so.
|
|
113
|
+
*/
|
|
114
|
+
| { readonly kind: "porting" }
|
|
101
115
|
| { readonly kind: "unbuilt"; readonly venue: Resolved }
|
|
102
116
|
| { readonly kind: "stale"; readonly finding: Finding }
|
|
103
117
|
| {
|
|
@@ -147,9 +161,13 @@ function kindOf(
|
|
|
147
161
|
/** How the venue reviews, and who the paper says wrote it: what `anonymity/*` judges by. */
|
|
148
162
|
const reviewOf = (
|
|
149
163
|
preset: Pick<Preset, "blind">,
|
|
150
|
-
settings: Pick<PaperSettings, "identity">,
|
|
164
|
+
settings: Pick<PaperSettings, "identity" | "cycles">,
|
|
151
165
|
): Pick<Resolved, "blind" | "identity"> => ({
|
|
152
|
-
|
|
166
|
+
// An accepted attempt's camera-ready carries the authors: no longer blind.
|
|
167
|
+
blind: effectiveBlind(
|
|
168
|
+
preset.blind,
|
|
169
|
+
settings.cycles?.current ?? { kind: "none" },
|
|
170
|
+
),
|
|
153
171
|
identity: settings.identity,
|
|
154
172
|
});
|
|
155
173
|
|
|
@@ -174,13 +192,25 @@ function freshFacts(
|
|
|
174
192
|
|
|
175
193
|
const isFinding = (v: object): v is Finding => "messageId" in v;
|
|
176
194
|
|
|
195
|
+
/** The paper's declared venue and kind, resolved: what the judges compare the facts against. */
|
|
196
|
+
const resolvedOf = (preset: Preset, settings: PaperSettings): Resolved => ({
|
|
197
|
+
venue: preset.label,
|
|
198
|
+
format: preset.format,
|
|
199
|
+
...kindOf(preset.label, preset.format, settings.kind),
|
|
200
|
+
...reviewOf(preset, settings),
|
|
201
|
+
});
|
|
202
|
+
|
|
177
203
|
/** One paper, assessed. Reads through `deps.files` only; never throws on a paper's files. */
|
|
178
204
|
export function assessPaper(paperDir: string, deps: VenueRuleDeps): Assessment {
|
|
179
205
|
const p = paperPreset(paperDir, deps);
|
|
180
206
|
if (p.kind === "none")
|
|
181
207
|
return p.settings === null
|
|
182
208
|
? { kind: "no-venue" }
|
|
183
|
-
: {
|
|
209
|
+
: {
|
|
210
|
+
kind: "no-preset",
|
|
211
|
+
file: join(paperDir, CONFIG_FILE),
|
|
212
|
+
cycles: p.settings.cycles !== null,
|
|
213
|
+
};
|
|
184
214
|
if (p.kind === "settings-problem")
|
|
185
215
|
return {
|
|
186
216
|
kind: "unresolved",
|
|
@@ -191,13 +221,9 @@ export function assessPaper(paperDir: string, deps: VenueRuleDeps): Assessment {
|
|
|
191
221
|
kind: "unresolved",
|
|
192
222
|
finding: finding("preset", { why: presetProblemText(p.problem) }),
|
|
193
223
|
};
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
format: p.preset.format,
|
|
198
|
-
...kindOf(label, p.preset.format, p.settings.kind),
|
|
199
|
-
...reviewOf(p.preset, p.settings),
|
|
200
|
-
};
|
|
224
|
+
if (p.settings.cycles !== null && isPorting(p.settings.cycles.current))
|
|
225
|
+
return { kind: "porting" };
|
|
226
|
+
const venue = resolvedOf(p.preset, p.settings);
|
|
201
227
|
const factsText = text(deps.files, factsPath(paperDir));
|
|
202
228
|
if (factsText === null) return { kind: "unbuilt", venue };
|
|
203
229
|
const facts = freshFacts(deps.files, paperDir, factsText);
|
|
@@ -558,7 +584,11 @@ const JUDGES: Readonly<Record<VenueRuleName, Judge>> = {
|
|
|
558
584
|
fresh: (a) => (a.kind === "stale" ? [a.finding] : []),
|
|
559
585
|
measured: (a, { shown }) =>
|
|
560
586
|
a.kind === "no-preset"
|
|
561
|
-
? [
|
|
587
|
+
? [
|
|
588
|
+
finding(a.cycles ? "noPresetCycle" : "noPreset", {
|
|
589
|
+
file: shown(a.file),
|
|
590
|
+
}),
|
|
591
|
+
]
|
|
562
592
|
: a.kind === "unbuilt"
|
|
563
593
|
? [
|
|
564
594
|
finding("unbuilt", {
|
|
@@ -655,6 +685,8 @@ const META: Readonly<Record<VenueRuleName, Meta>> = {
|
|
|
655
685
|
"not built yet, so `{{venue}}`'s page limit, fonts and format are not checked — run `paperlint build`",
|
|
656
686
|
noPreset:
|
|
657
687
|
'this paper names no venue preset yet, so its page limit, fonts and format are not checked — set "extends" in {{file}} (e.g. "paperlint:agenticdev"; see docs/rules.md)',
|
|
688
|
+
noPresetCycle:
|
|
689
|
+
'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 {{file}} ("venue": { "kind": "preset", "extends": "paperlint:…" }), or open a new cycle when the last one has ended',
|
|
658
690
|
noGeometry:
|
|
659
691
|
"the build measured no page geometry (banal was not found or failed), so page size, columns, page limits and font sizes were NOT checked — run `paperlint toolchain`, then `paperlint build`",
|
|
660
692
|
},
|