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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
|
5
|
+
<link href="@1790985506/stylesheets/style.css" rel="stylesheet">
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="images/review48.svg">
|
|
7
|
+
<title>No such conference - acsac2026</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body data-upload-limit="104857600" data-document-max-size="600000000"><div id="p-page" class="need-banner-offset"><header id="p-header"><hr class="c"><div id="h-site" class="header-site-page"><a class="q" href="https://acsac2026.hotcrp.com/"><span class="header-site-name">acsac2026</span></a></div><div id="h-right"></div><div id="h-page"><h1 id="h-title">No such conference</h1></div><hr class="c">
|
|
10
|
+
<div id="h-messages" class="want-mx-auto"></div></header>
|
|
11
|
+
<main id="p-body">
|
|
12
|
+
<p>Did you mean:</p><ul><li><a href="https://acsac20posters.hotcrp.com">ACSAC 2020 Posters & WIPS</a> (<a href="https://acsac20posters.hotcrp.com">acsac20posters.hotcrp.com</a>)</li><li><a href="https://acsac20.hotcrp.com">ACSAC 2020</a> (<a href="https://acsac20.hotcrp.com">acsac20.hotcrp.com</a>)</li><li><a href="https://acsac19.hotcrp.com">ACSAC 2019</a> (<a href="https://acsac19.hotcrp.com">acsac19.hotcrp.com</a>)</li></ul><hr class="c"></main></div><footer id="p-footer" class="need-banner-offset banner-bottom"><a class="noq" href="https://hotcrp.com/">HotCRP</a><!-- Version 3.4 --></footer><!--[if lt IE 9]><script src="@1749780400/scripts/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script><![endif]-->
|
|
13
|
+
<![if !IE|gte IE 9]><script src="@1749780400/scripts/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="></script><![endif]>
|
|
14
|
+
<script>window.siteinfo={"site_relative":"","base":"\/","suffix":"","assets":"","icons_url":"","cookie_params":"; Domain=.hotcrp.com; SameSite=Lax","postvalue":".empty","download_prefix":"acsac2026-","snouns":["submission","submissions","Submission","Submissions"],"user":[]}</script><script src="@1790985505/scripts/script.js"></script>
|
|
15
|
+
<script src="@1777496734/scripts/markdown-it.min.js"></script>
|
|
16
|
+
<script src="@1787955704/scripts/plugin.js"></script>
|
|
17
|
+
<script>hotcrp.onload.time(0,0)</script></body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
|
5
|
+
<meta name="google" content="notranslate">
|
|
6
|
+
<meta name="theme-color" content="#ffb7ca">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
8
|
+
<link href="https://static.hotcrp.com/@1790985506/stylesheets/style.css" rel="stylesheet">
|
|
9
|
+
<link href="https://static.hotcrp.com/@1786633883/stylesheets/mobile.css" media="screen and (max-width: 1100px)" rel="stylesheet">
|
|
10
|
+
<link href="https://static.hotcrp.com/@1788721683/stylesheets/plugin.css" rel="stylesheet">
|
|
11
|
+
<link rel="icon" type="image/svg+xml" href="https://static.hotcrp.com/images/review48.svg">
|
|
12
|
+
<title>Deadlines - SOSP 2026</title>
|
|
13
|
+
</head>
|
|
14
|
+
<body id="m-deadlines" data-upload-limit="104857600" data-document-max-size="600000000"><div id="p-page" class="need-banner-offset"><header id="p-header"><hr class="c"><div id="h-site" class="header-site-page"><a class="q" href="/"><span class="header-site-name">SOSP 2026</span></a></div><div id="h-right"></div><div id="h-page"><h1 id="h-title">Deadlines</h1></div><nav id="p-quicklinks" aria-label="Submission search"></nav><hr class="c">
|
|
15
|
+
<div id="h-messages" class="want-mx-auto"></div></header>
|
|
16
|
+
<main id="p-body">
|
|
17
|
+
<h3>Past</h3><dl class="bsp"><dt><strong>Resubmission deadline</strong>: <span class="need-usertime" data-ts="1786751999">Friday Aug 14, 2026, 7:59:59 PM EDT</span></dt><dd>Completed submissions may be updated until this deadline.</dd>
|
|
18
|
+
<dt><strong>Submission deadline</strong>: <span class="need-usertime" data-ts="1775131199">Thursday Apr 2, 2026, 7:59:59 AM EDT</span></dt><dd>Submissions must be completed by this deadline to be reviewed.</dd>
|
|
19
|
+
<dt><strong>Registration deadline</strong>: <span class="need-usertime" data-ts="1774612799">Friday Mar 27, 2026, 7:59:59 AM EDT</span></dt><dd>You can register new submissions until this deadline.</dd></dl>
|
|
20
|
+
<hr class="c"></main></div><footer id="p-footer" class="need-banner-offset banner-bottom"><a class="noq" href="https://hotcrp.com/">HotCRP</a><!-- Version 3.4 --></footer><!--[if lt IE 9]><script nonce="LW8qxXSv09MzquWfF8dAZw==" src="https://static.hotcrp.com/@1749780400/scripts/jquery-1.12.4.min.js" crossorigin="anonymous" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script><![endif]-->
|
|
21
|
+
<![if !IE|gte IE 9]><script nonce="LW8qxXSv09MzquWfF8dAZw==" src="https://static.hotcrp.com/@1749780400/scripts/jquery-3.7.1.min.js" crossorigin="anonymous" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="></script><![endif]>
|
|
22
|
+
<script nonce="LW8qxXSv09MzquWfF8dAZw==">window.siteinfo={"site_relative":"","base":"\/","suffix":"","assets":"https:\/\/static.hotcrp.com\/","icons_url":"@1787797517\/images\/icons.svg","cookie_params":"; Domain=.hotcrp.com; Secure; SameSite=Lax","postvalue":".empty","download_prefix":"sosp26-","snouns":["submission","submissions","Submission","Submissions"],"user":[]}</script><script nonce="LW8qxXSv09MzquWfF8dAZw==" src="https://static.hotcrp.com/@1790985505/scripts/script.js" crossorigin="anonymous"></script>
|
|
23
|
+
<script nonce="LW8qxXSv09MzquWfF8dAZw==" src="https://static.hotcrp.com/@1777496734/scripts/markdown-it.min.js" crossorigin="anonymous"></script>
|
|
24
|
+
<script nonce="LW8qxXSv09MzquWfF8dAZw==" src="https://static.hotcrp.com/@1787955704/scripts/plugin.js" crossorigin="anonymous"></script>
|
|
25
|
+
<script nonce="LW8qxXSv09MzquWfF8dAZw==">hotcrp.onload.time(240,0);hotcrp.init_deadlines({"now":1791194489.724288,"email":null,"sub":{"open":true,"sub":1775131199,"reg":1774612799,"blind":true},"final":{"open":true,"done":1788004799}})</script></body>
|
|
26
|
+
</html>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hotcrpDeadlines` over a local server that answers `GET /deadlines` with a recorded page: the
|
|
3
|
+
* request paperlint sends (the path, no credential) and what comes back through the parser; and a
|
|
4
|
+
* site that does not answer at all.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import {
|
|
9
|
+
createServer,
|
|
10
|
+
type IncomingMessage,
|
|
11
|
+
type Server,
|
|
12
|
+
type ServerResponse,
|
|
13
|
+
} from "node:http";
|
|
14
|
+
import { afterAll, beforeAll, test } from "vitest";
|
|
15
|
+
import { hotcrpDeadlines } from "./deadlines.io.ts";
|
|
16
|
+
|
|
17
|
+
const page = readFileSync(
|
|
18
|
+
new URL("./cassette/deadlines-msr2027.html", import.meta.url),
|
|
19
|
+
"utf8",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const seen: { url: string; authorization: string | undefined }[] = [];
|
|
23
|
+
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
24
|
+
seen.push({ url: req.url ?? "", authorization: req.headers.authorization });
|
|
25
|
+
if (req.url === "/deadlines") {
|
|
26
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
27
|
+
res.end(page);
|
|
28
|
+
} else {
|
|
29
|
+
res.writeHead(404, { "content-type": "text/html" });
|
|
30
|
+
res.end('<h1 id="h-title">No such conference</h1>');
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const portOf = (s: Server): number => {
|
|
35
|
+
const a = s.address();
|
|
36
|
+
assert.ok(a !== null && typeof a === "object", "the server is not listening");
|
|
37
|
+
return a.port;
|
|
38
|
+
};
|
|
39
|
+
const closeServer = (s: Server): Promise<void> =>
|
|
40
|
+
new Promise<void>((r) => {
|
|
41
|
+
s.close(() => {
|
|
42
|
+
r();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let base = "";
|
|
47
|
+
beforeAll(async () => {
|
|
48
|
+
await new Promise<void>((r) => server.listen(0, "127.0.0.1", r));
|
|
49
|
+
base = `http://127.0.0.1:${String(portOf(server))}`;
|
|
50
|
+
});
|
|
51
|
+
afterAll(() => closeServer(server));
|
|
52
|
+
|
|
53
|
+
test("GET <site>/deadlines, no credential; the page parses to the venue's deadlines", async () => {
|
|
54
|
+
const r = await hotcrpDeadlines({ url: base }).read();
|
|
55
|
+
assert.deepEqual(seen.at(-1), {
|
|
56
|
+
url: "/deadlines",
|
|
57
|
+
authorization: undefined,
|
|
58
|
+
});
|
|
59
|
+
assert.ok(r.ok, JSON.stringify(r));
|
|
60
|
+
assert.equal(r.value.open, true);
|
|
61
|
+
assert.deepEqual(
|
|
62
|
+
r.value.deadlines.map((d) => [d.what, d.at]),
|
|
63
|
+
[
|
|
64
|
+
["submission", "2026-10-20T04:00:00Z"],
|
|
65
|
+
["resubmission", "2026-10-23T04:00:00Z"],
|
|
66
|
+
],
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a site that is not a conference: refused with the page's title", async () => {
|
|
71
|
+
const r = await hotcrpDeadlines({ url: `${base}/nosuch` }).read();
|
|
72
|
+
assert.ok(!r.ok);
|
|
73
|
+
assert.equal(r.error.kind, "refused");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("a site that does not answer is unreachable, with the network's words", async () => {
|
|
77
|
+
const closed = createServer();
|
|
78
|
+
await new Promise<void>((r) => closed.listen(0, "127.0.0.1", r));
|
|
79
|
+
const port = portOf(closed);
|
|
80
|
+
await closeServer(closed);
|
|
81
|
+
const r = await hotcrpDeadlines({
|
|
82
|
+
url: `http://127.0.0.1:${String(port)}`,
|
|
83
|
+
}).read();
|
|
84
|
+
assert.ok(!r.ok);
|
|
85
|
+
assert.equal(r.error.kind, "unreachable");
|
|
86
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotCRP's deadlines page over Node's `fetch`: `GET <site>/deadlines`, no token — the page is
|
|
3
|
+
* public, and a deadline is nobody's secret.
|
|
4
|
+
*/
|
|
5
|
+
import { messageOf } from "../../domain/text.ts";
|
|
6
|
+
import { err } from "../../domain/result.ts";
|
|
7
|
+
import type { VenueDeadlines } from "../../ports/venue-deadlines.ts";
|
|
8
|
+
import { parseDeadlinesPage } from "./deadlines.ts";
|
|
9
|
+
|
|
10
|
+
const READ_TIMEOUT_MS = 60_000;
|
|
11
|
+
|
|
12
|
+
/** The deadlines of the HotCRP site at `url` (no trailing slash). */
|
|
13
|
+
export function hotcrpDeadlines(o: { readonly url: string }): VenueDeadlines {
|
|
14
|
+
return {
|
|
15
|
+
async read() {
|
|
16
|
+
try {
|
|
17
|
+
const r = await fetch(`${o.url}/deadlines`, {
|
|
18
|
+
headers: { Accept: "text/html" },
|
|
19
|
+
signal: AbortSignal.timeout(READ_TIMEOUT_MS),
|
|
20
|
+
});
|
|
21
|
+
return parseDeadlinesPage(r.status, await r.text());
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return err({ kind: "unreachable", detail: messageOf(e) });
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deadlines page parser on the pages five live HotCRP sites answered on 2026-10-05
|
|
3
|
+
* (`cassette/deadlines-*.html`, recorded whole), and on a site that does not exist. Each expected
|
|
4
|
+
* value was read off the page's text by hand before the parser existed.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { parseDeadlinesPage, whatOf } from "./index.ts";
|
|
9
|
+
|
|
10
|
+
type Site = "msr2027" | "aidc26" | "sosp26" | "icse2027" | "fse2027" | "nosuch";
|
|
11
|
+
const page = (site: Site): string =>
|
|
12
|
+
readFileSync(
|
|
13
|
+
new URL(`./cassette/deadlines-${site}.html`, import.meta.url),
|
|
14
|
+
"utf8",
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const read = (site: Site, status = 200) => {
|
|
18
|
+
const r = parseDeadlinesPage(status, page(site));
|
|
19
|
+
if (!r.ok) throw new Error(JSON.stringify(r.error));
|
|
20
|
+
return r.value;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe("parseDeadlinesPage", () => {
|
|
24
|
+
it("a conference with a resubmission deadline: the list carries it, the status object does not", () => {
|
|
25
|
+
expect(read("msr2027")).toEqual({
|
|
26
|
+
now: 1791194488.329003,
|
|
27
|
+
open: true,
|
|
28
|
+
blind: true,
|
|
29
|
+
deadlines: [
|
|
30
|
+
{
|
|
31
|
+
what: "submission",
|
|
32
|
+
label: "Submission deadline",
|
|
33
|
+
at: "2026-10-20T04:00:00Z",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
what: "resubmission",
|
|
37
|
+
label: "Resubmission deadline",
|
|
38
|
+
at: "2026-10-23T04:00:00Z",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("a workshop past its submission deadline, with a final-version deadline only the status object names", () => {
|
|
45
|
+
expect(read("aidc26")).toEqual({
|
|
46
|
+
now: 1791194489.473936,
|
|
47
|
+
open: true,
|
|
48
|
+
blind: true,
|
|
49
|
+
deadlines: [
|
|
50
|
+
{
|
|
51
|
+
what: "submission",
|
|
52
|
+
label: "Submission deadline",
|
|
53
|
+
at: "2026-10-03T11:59:59Z",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
what: "camera-ready",
|
|
57
|
+
label: "camera-ready",
|
|
58
|
+
at: "2026-10-31T11:59:59Z",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("parseDeadlinesPage — every kind at once", () => {
|
|
66
|
+
it("registration, submission, a late resubmission and a final version: sorted by time", () => {
|
|
67
|
+
expect(read("sosp26").deadlines).toEqual([
|
|
68
|
+
{
|
|
69
|
+
what: "registration",
|
|
70
|
+
label: "Registration deadline",
|
|
71
|
+
at: "2026-03-27T11:59:59Z",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
what: "submission",
|
|
75
|
+
label: "Submission deadline",
|
|
76
|
+
at: "2026-04-02T11:59:59Z",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
what: "resubmission",
|
|
80
|
+
label: "Resubmission deadline",
|
|
81
|
+
at: "2026-08-14T23:59:59Z",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
what: "camera-ready",
|
|
85
|
+
label: "camera-ready",
|
|
86
|
+
at: "2026-08-29T11:59:59Z",
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("parseDeadlinesPage — a closed round, AoE, and pages that are not deadlines", () => {
|
|
93
|
+
it("a round that is not open: `open` is false and the dates still read", () => {
|
|
94
|
+
const r = read("icse2027");
|
|
95
|
+
expect(r.open).toBe(false);
|
|
96
|
+
expect(r.deadlines.map((d) => [d.what, d.at])).toEqual([
|
|
97
|
+
["registration", "2026-06-24T11:59:59Z"],
|
|
98
|
+
["submission", "2026-07-09T11:59:00Z"],
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("parseDeadlinesPage — AoE and non-pages", () => {
|
|
104
|
+
it("the AoE a page prints is already an instant in UTC: «Oct 3, 11:59 PM AoE» is Oct 4, 11:59 UTC", () => {
|
|
105
|
+
expect(read("fse2027").deadlines).toEqual([
|
|
106
|
+
{
|
|
107
|
+
what: "submission",
|
|
108
|
+
label: "Submission deadline",
|
|
109
|
+
at: "2026-10-04T11:59:00Z",
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("a status object with no submission round: closed, blindness unknown, no deadlines", () => {
|
|
115
|
+
expect(
|
|
116
|
+
parseDeadlinesPage(200, 'hotcrp.init_deadlines({"now": 1})'),
|
|
117
|
+
).toEqual({
|
|
118
|
+
ok: true,
|
|
119
|
+
value: { now: 1, open: false, blind: null, deadlines: [] },
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("a refusal without a page title carries no message", () => {
|
|
124
|
+
expect(parseDeadlinesPage(503, "<html>down</html>")).toEqual({
|
|
125
|
+
ok: false,
|
|
126
|
+
error: { kind: "refused", httpStatus: 503, messages: [] },
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("a site that does not exist: refused with the page's title", () => {
|
|
131
|
+
expect(parseDeadlinesPage(404, page("nosuch"))).toEqual({
|
|
132
|
+
ok: false,
|
|
133
|
+
error: {
|
|
134
|
+
kind: "refused",
|
|
135
|
+
httpStatus: 404,
|
|
136
|
+
messages: [
|
|
137
|
+
{ message: "No such conference", field: null, status: null },
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("a 200 page that is not a deadlines page, and a status object of another shape: malformed", () => {
|
|
144
|
+
const detail: unknown = expect.stringContaining(
|
|
145
|
+
"no `hotcrp.init_deadlines(…)` call",
|
|
146
|
+
);
|
|
147
|
+
expect(parseDeadlinesPage(200, "<html><body>hello</body></html>")).toEqual({
|
|
148
|
+
ok: false,
|
|
149
|
+
error: {
|
|
150
|
+
kind: "malformed",
|
|
151
|
+
httpStatus: 200,
|
|
152
|
+
detail,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const r = parseDeadlinesPage(200, 'hotcrp.init_deadlines({"sub":1})');
|
|
156
|
+
expect(r.ok ? "parsed" : r.error.kind).toBe("malformed");
|
|
157
|
+
const notJson = parseDeadlinesPage(200, "hotcrp.init_deadlines({now: 1})");
|
|
158
|
+
expect(notJson.ok ? "parsed" : notJson.error.kind).toBe("malformed");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("whatOf", () => {
|
|
163
|
+
it("names the deadlines paperlint knows by their HotCRP labels, with or without a round name", () => {
|
|
164
|
+
expect(whatOf("Submission deadline")).toBe("submission");
|
|
165
|
+
expect(whatOf("Resubmission deadline")).toBe("resubmission");
|
|
166
|
+
expect(whatOf("Short papers registration deadline")).toBe("registration");
|
|
167
|
+
expect(whatOf("Final version deadline")).toBe("camera-ready");
|
|
168
|
+
expect(whatOf("Response deadline")).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotCRP's deadlines page (`<site>/deadlines`), parsed at the boundary into paperlint's
|
|
3
|
+
* `PortalDeadlines`. Pure.
|
|
4
|
+
*
|
|
5
|
+
* ── WHAT THE PAGE IS, measured 2026-10-05 on five live sites (`cassette/deadlines-*.html`) ───────
|
|
6
|
+
* The page is HTML, public, no account needed; the API endpoint of the same name (`/api/deadlines`)
|
|
7
|
+
* answers 401 «Missing credentials» without a token. The machine-readable part is the argument of
|
|
8
|
+
* `hotcrp.init_deadlines({…})` in the page's last script: the author's `status_json()`
|
|
9
|
+
* (kohler/hotcrp, src/contact.php), of which an anonymous visitor gets
|
|
10
|
+
*
|
|
11
|
+
* now the server's clock, Unix seconds with a fraction
|
|
12
|
+
* sub.open whether the submission round is open now
|
|
13
|
+
* sub.sub the submission deadline, Unix seconds (SubmissionRound::$submit)
|
|
14
|
+
* sub.reg the registration deadline, when it is earlier (SubmissionRound::$register)
|
|
15
|
+
* sub.blind true | "optional" | "until-review" (Conf::submission_blindness)
|
|
16
|
+
* final.done the final-version (camera-ready) deadline (final_deadline_for_display)
|
|
17
|
+
*
|
|
18
|
+
* The RESUBMISSION deadline (updates allowed until, `SubmissionRound::$resubmit`) is not in that
|
|
19
|
+
* object; the page lists it only in its `<dl>`, each entry a `<dt>` with the deadline's label and a
|
|
20
|
+
* `<span data-ts="…">` holding the same Unix seconds (pages/p_deadlines.php, `dl1`). So both are
|
|
21
|
+
* read: the JSON for what it names, the list for every deadline the page shows, matched by label.
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import {
|
|
25
|
+
instantOfUnix,
|
|
26
|
+
type DeadlineWhat,
|
|
27
|
+
type PortalDeadline,
|
|
28
|
+
type PortalDeadlines,
|
|
29
|
+
} from "../../domain/deadline.ts";
|
|
30
|
+
import { err, ok, type Result } from "../../domain/result.ts";
|
|
31
|
+
import type { PortalFailure } from "../../domain/submission.ts";
|
|
32
|
+
|
|
33
|
+
const Blind = z.union([
|
|
34
|
+
z.boolean(),
|
|
35
|
+
z.literal("optional"),
|
|
36
|
+
z.literal("until-review"),
|
|
37
|
+
]);
|
|
38
|
+
const Status = z.looseObject({
|
|
39
|
+
now: z.number(),
|
|
40
|
+
sub: z
|
|
41
|
+
.looseObject({
|
|
42
|
+
open: z.boolean().optional(),
|
|
43
|
+
sub: z.number().optional(),
|
|
44
|
+
reg: z.number().optional(),
|
|
45
|
+
blind: Blind.optional(),
|
|
46
|
+
})
|
|
47
|
+
.optional(),
|
|
48
|
+
final: z.looseObject({ done: z.number().optional() }).optional(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/** The script call that carries the status object; the argument is JSON. */
|
|
52
|
+
const INIT = /hotcrp\.init_deadlines\((\{.*?\})\)/s;
|
|
53
|
+
/** One listed deadline: its label in `<strong>`, its time in `data-ts`. */
|
|
54
|
+
const ENTRY = /<dt><strong>([^<]*)<\/strong>:\s*<span[^>]*\bdata-ts="(\d+)"/g;
|
|
55
|
+
/** The page's title, for a page that is no deadlines page (a site that does not exist). */
|
|
56
|
+
const TITLE = /<h1 id="h-title">([^<]*)</;
|
|
57
|
+
|
|
58
|
+
/** What a listed deadline's label names, in paperlint's words; null for one paperlint does not name. */
|
|
59
|
+
export function whatOf(label: string): DeadlineWhat | null {
|
|
60
|
+
const l = label.toLowerCase();
|
|
61
|
+
if (/\bresubmission deadline\b/.test(l)) return "resubmission";
|
|
62
|
+
if (/\bregistration deadline\b/.test(l)) return "registration";
|
|
63
|
+
if (/\bsubmission deadline\b/.test(l)) return "submission";
|
|
64
|
+
if (/\bfinal\b|\bcamera[- ]ready\b/.test(l)) return "camera-ready";
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The deadlines the status object names, each with the label the page would give it. */
|
|
69
|
+
type StatusJson = Readonly<z.infer<typeof Status>>;
|
|
70
|
+
|
|
71
|
+
function fromStatus(s: StatusJson): readonly PortalDeadline[] {
|
|
72
|
+
const named: readonly (readonly [DeadlineWhat, number | undefined])[] = [
|
|
73
|
+
["registration", s.sub?.reg],
|
|
74
|
+
["submission", s.sub?.sub],
|
|
75
|
+
["camera-ready", s.final?.done],
|
|
76
|
+
];
|
|
77
|
+
return named.flatMap(([what, at]) =>
|
|
78
|
+
at === undefined ? [] : [{ what, label: what, at: instantOfUnix(at) }],
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Every deadline the page lists, with its label as the page spells it. */
|
|
83
|
+
function fromList(html: string): readonly PortalDeadline[] {
|
|
84
|
+
return [...html.matchAll(ENTRY)].map((m) => {
|
|
85
|
+
// Both groups always participate in a match of ENTRY: there is no fallback to take.
|
|
86
|
+
const label = String(m[1]).trim();
|
|
87
|
+
return { what: whatOf(label), label, at: instantOfUnix(Number(m[2])) };
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Listed entries first (they carry the page's label); a status entry only when the list lacks its kind. */
|
|
92
|
+
function merged(
|
|
93
|
+
listed: readonly PortalDeadline[],
|
|
94
|
+
named: readonly PortalDeadline[],
|
|
95
|
+
): readonly PortalDeadline[] {
|
|
96
|
+
const extra = named.filter(
|
|
97
|
+
(n) => !listed.some((l) => l.what === n.what && l.at === n.at),
|
|
98
|
+
);
|
|
99
|
+
return [...listed, ...extra].toSorted((a, b) => a.at.localeCompare(b.at));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const refused = (httpStatus: number, html: string): PortalFailure => {
|
|
103
|
+
const title = TITLE.exec(html)?.[1]?.trim();
|
|
104
|
+
return {
|
|
105
|
+
kind: "refused",
|
|
106
|
+
httpStatus,
|
|
107
|
+
messages:
|
|
108
|
+
title === undefined
|
|
109
|
+
? []
|
|
110
|
+
: [{ message: title, field: null, status: null }],
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** The status object in the page's `init_deadlines` call, or why there is none. */
|
|
115
|
+
function statusOf(
|
|
116
|
+
httpStatus: number,
|
|
117
|
+
html: string,
|
|
118
|
+
): Result<StatusJson, PortalFailure> {
|
|
119
|
+
const m = INIT.exec(html);
|
|
120
|
+
if (m === null)
|
|
121
|
+
return err({
|
|
122
|
+
kind: "malformed",
|
|
123
|
+
httpStatus,
|
|
124
|
+
detail:
|
|
125
|
+
"the page has no `hotcrp.init_deadlines(…)` call — not a HotCRP deadlines page",
|
|
126
|
+
});
|
|
127
|
+
const json = ((): unknown => {
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(String(m[1])); // the group always participates in a match of INIT
|
|
130
|
+
} catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
})();
|
|
134
|
+
const parsed = Status.safeParse(json);
|
|
135
|
+
return parsed.success
|
|
136
|
+
? ok(parsed.data)
|
|
137
|
+
: err({
|
|
138
|
+
kind: "malformed",
|
|
139
|
+
httpStatus,
|
|
140
|
+
detail:
|
|
141
|
+
"the `init_deadlines` argument is not the status object HotCRP documents",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** `GET /deadlines` → the portal's deadlines, or why the page is not them. */
|
|
146
|
+
export function parseDeadlinesPage(
|
|
147
|
+
httpStatus: number,
|
|
148
|
+
html: string,
|
|
149
|
+
): Result<PortalDeadlines, PortalFailure> {
|
|
150
|
+
if (httpStatus < 200 || httpStatus >= 300)
|
|
151
|
+
return err(refused(httpStatus, html));
|
|
152
|
+
const status = statusOf(httpStatus, html);
|
|
153
|
+
if (!status.ok) return status;
|
|
154
|
+
const s = status.value;
|
|
155
|
+
return ok({
|
|
156
|
+
now: s.now,
|
|
157
|
+
open: s.sub?.open ?? false,
|
|
158
|
+
blind: s.sub?.blind ?? null,
|
|
159
|
+
deadlines: merged(fromList(html), fromStatus(s)),
|
|
160
|
+
});
|
|
161
|
+
}
|
package/src/cli.test.ts
CHANGED
|
@@ -344,6 +344,15 @@ test("no command prints the usage and exits 2; --kind with no value is refused",
|
|
|
344
344
|
assert.equal(parseArgs(["new", "x", "--kind"]).missingValue, "--kind");
|
|
345
345
|
});
|
|
346
346
|
|
|
347
|
+
test("--help names the cycles and where a deadline comes from — the only place a newcomer looks first", async () => {
|
|
348
|
+
const r = await cli(["--help"], root);
|
|
349
|
+
assert.equal(r.code, 0);
|
|
350
|
+
assert.match(r.out, /"cycles": \[ \{ "id"/);
|
|
351
|
+
assert.match(r.out, /A venue preset carries the portal's deadlines/);
|
|
352
|
+
assert.match(r.out, /"override": \{ "at", "reason", "evidence" \}/);
|
|
353
|
+
assert.match(r.out, /the earlier reading binds/);
|
|
354
|
+
});
|
|
355
|
+
|
|
347
356
|
test("src/cli.ts run as the program itself answers like the bin", () => {
|
|
348
357
|
const r = runNode(join(import.meta.dirname, "cli.ts"), ["--help"]);
|
|
349
358
|
assert.equal(r.status, 0);
|
package/src/cli.ts
CHANGED
|
@@ -42,7 +42,12 @@ import {
|
|
|
42
42
|
asEslintResults,
|
|
43
43
|
} from "./structure.ts";
|
|
44
44
|
import { buildPapers, papersIn, anyFailed, remedyFor, MAIN } from "./build.ts";
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
FRAGMENT_FILES,
|
|
47
|
+
includeBlocks,
|
|
48
|
+
paperBodies,
|
|
49
|
+
unreadLines,
|
|
50
|
+
} from "./paper-includes.ts";
|
|
46
51
|
import { prepareEngine } from "./build-engine.ts";
|
|
47
52
|
import { cacheRoot, cachedTree, runToolchain } from "./toolchain.ts";
|
|
48
53
|
import { banalInstaller, parseBanalSettings } from "./adapters/banal/index.ts";
|
|
@@ -69,6 +74,8 @@ import {
|
|
|
69
74
|
import {
|
|
70
75
|
FORMAT_RULE_LEVELS,
|
|
71
76
|
formatRules,
|
|
77
|
+
CYCLE_PORT_RULE_LEVELS,
|
|
78
|
+
cyclePortRules,
|
|
72
79
|
TEX_VENUE_RULE_LEVELS,
|
|
73
80
|
texVenueRules,
|
|
74
81
|
} from "./tex-venue-rules.ts";
|
|
@@ -159,6 +166,8 @@ export { init };
|
|
|
159
166
|
export { nextSteps } from "./init.ts";
|
|
160
167
|
|
|
161
168
|
import paperStages from "#eslint-rules/paper-stages";
|
|
169
|
+
import cycleStage from "#eslint-rules/cycle-stage";
|
|
170
|
+
import { CYCLE_RULE_LEVELS, cycleRules } from "./cycle-rules.ts";
|
|
162
171
|
import {
|
|
163
172
|
FOLDER_VENUE_RULE_LEVELS,
|
|
164
173
|
folderVenueRules,
|
|
@@ -275,6 +284,24 @@ settings — paperlint.json, at two levels, one schema. Both are optional.
|
|
|
275
284
|
"extends" names a venue preset: paperlint:<name> (shipped: ${SHIPPED_VENUES().join(", ")})
|
|
276
285
|
or ./path.jsonc, relative to the paperlint.json. npm presets are not supported yet.
|
|
277
286
|
|
|
287
|
+
cycles and deadlines — a paper's attempts at venues, oldest first, instead of "extends",
|
|
288
|
+
"kind" and "submission" (those are then the current attempt's, and refused beside "cycles"):
|
|
289
|
+
|
|
290
|
+
{ "cycles": [ { "id": "msr-2027", "venue": { "kind": "preset", "extends": "paperlint:msr" },
|
|
291
|
+
"kind": "technical", "opened": "2026-10-05",
|
|
292
|
+
"deadlines": [ { "what": "submission",
|
|
293
|
+
"observed": [ { "at": "2026-10-23 AoE", "source": "call",
|
|
294
|
+
"url": "<the call>", "read": "2026-10-05" } ] } ] } ] }
|
|
295
|
+
|
|
296
|
+
A venue preset carries the portal's deadlines, read and dated; the package updates them and
|
|
297
|
+
your dependency update brings them. A paper adds what the call says ("source": "call") and,
|
|
298
|
+
for a deadline moved by the chairs, "override": { "at", "reason", "evidence" } — evidence a
|
|
299
|
+
file in the paper folder. The override wins; otherwise the earlier reading binds. "what":
|
|
300
|
+
registration (the call's abstract), submission (the full paper), resubmission, notification,
|
|
301
|
+
camera-ready. A venue without a preset: "venue": { "kind": "named", "name", "url" }, every
|
|
302
|
+
deadline typed from its call. Lint never reads the clock: it checks the record, not the time
|
|
303
|
+
left. docs/configuration.md#the-papers-cycles
|
|
304
|
+
|
|
278
305
|
"rules" is { "<rule>": "<severity>" } for every paper file in scope, or ESLint flat-config
|
|
279
306
|
blocks (files, ignores, rules) with globs relative to that file. Order, later wins: paperlint's
|
|
280
307
|
own, the venue preset's, the root file's, the paper's. Optional rules (off unless turned on):
|
|
@@ -298,6 +325,22 @@ const REGISTER_AND_CASE_RULES = {
|
|
|
298
325
|
"heading-case": headingCaseRule(TEX_RULE_DEPS),
|
|
299
326
|
};
|
|
300
327
|
|
|
328
|
+
/**
|
|
329
|
+
* The paper's attempts (`cycles` in paperlint.json), judged on `PIPELINE-STATUS.md`: the record itself
|
|
330
|
+
* and its evidence (src/cycle-rules.ts), and the stages that name a cycle (eslint-rules/cycle-stage.ts).
|
|
331
|
+
* The `cycle` plugin's other rule, `port-done`, reads `paper.tex` (src/tex-venue-rules.ts).
|
|
332
|
+
*/
|
|
333
|
+
/** The markdown language with YAML frontmatter: what every markdown block of the config lints as. */
|
|
334
|
+
const MARKDOWN_WITH_FRONTMATTER = {
|
|
335
|
+
language: "markdown/gfm",
|
|
336
|
+
languageOptions: { frontmatter: "yaml" },
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const CYCLE_STATUS_RULES = {
|
|
340
|
+
...cycleRules({ files: nodeFiles, venuesDir: presetsDir() }),
|
|
341
|
+
...cycleStage.rules,
|
|
342
|
+
};
|
|
343
|
+
|
|
301
344
|
/** The rules of the files `paper.tex` includes, each read on its own as a fragment (#144). */
|
|
302
345
|
const FRAGMENT_RULES = {
|
|
303
346
|
"future-promise": texBuild["future-promise"],
|
|
@@ -328,10 +371,7 @@ export function buildConfig(
|
|
|
328
371
|
"paper/section-word": "warn",
|
|
329
372
|
"paper/leading-zero": "warn",
|
|
330
373
|
};
|
|
331
|
-
const md =
|
|
332
|
-
language: "markdown/gfm",
|
|
333
|
-
languageOptions: { frontmatter: "yaml" },
|
|
334
|
-
};
|
|
374
|
+
const md = MARKDOWN_WITH_FRONTMATTER;
|
|
335
375
|
|
|
336
376
|
const cfg: ConfigBlock[] = [
|
|
337
377
|
// 🔴 THE PROJECT'S PAPER TEMPLATE IS NOT A PAPER. `paperlint new` reads `<papers>/.template/`, and
|
|
@@ -369,12 +409,14 @@ export function buildConfig(
|
|
|
369
409
|
...folderVenueRules({ files: nodeFiles, venuesDir: presetsDir() }),
|
|
370
410
|
},
|
|
371
411
|
},
|
|
412
|
+
cycle: { rules: CYCLE_STATUS_RULES },
|
|
372
413
|
},
|
|
373
414
|
...md,
|
|
374
415
|
rules: {
|
|
375
416
|
"paper/stages": "error",
|
|
376
417
|
"paper/source": "error",
|
|
377
418
|
...FOLDER_VENUE_RULE_LEVELS,
|
|
419
|
+
...CYCLE_RULE_LEVELS,
|
|
378
420
|
},
|
|
379
421
|
},
|
|
380
422
|
{
|
|
@@ -427,6 +469,7 @@ export function buildConfig(
|
|
|
427
469
|
...pageLimitRules({ files: nodeFiles, venuesDir: presetsDir() }),
|
|
428
470
|
},
|
|
429
471
|
},
|
|
472
|
+
cycle: { rules: cyclePortRules(TEX_RULE_DEPS) },
|
|
430
473
|
},
|
|
431
474
|
language: "tex/latex",
|
|
432
475
|
rules: {
|
|
@@ -449,6 +492,7 @@ export function buildConfig(
|
|
|
449
492
|
...TEX_VENUE_RULE_LEVELS,
|
|
450
493
|
...FORMAT_RULE_LEVELS,
|
|
451
494
|
...PAGE_LIMIT_RULE_LEVELS,
|
|
495
|
+
...CYCLE_PORT_RULE_LEVELS,
|
|
452
496
|
// Silent for a paper that declares no `talk` (src/talk-rules.ts), but talk/undeclared.
|
|
453
497
|
...TALK_RULE_LEVELS,
|
|
454
498
|
},
|
|
@@ -462,7 +506,7 @@ export function buildConfig(
|
|
|
462
506
|
const fragments: readonly ConfigBlock[] = texLanguage
|
|
463
507
|
? [
|
|
464
508
|
{
|
|
465
|
-
files: [
|
|
509
|
+
files: [...FRAGMENT_FILES],
|
|
466
510
|
ignores: ["**/paper.tex"],
|
|
467
511
|
plugins: {
|
|
468
512
|
tex: {
|