paperlint 7.1.0 → 8.0.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/build.d.ts.map +1 -1
- package/dist/build.js +1 -6
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +3 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +53 -47
- 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/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +10 -13
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.js +2 -2
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +22 -91
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/papers.d.ts +1 -3
- package/dist/eslint-rules/papers.d.ts.map +1 -1
- package/dist/eslint-rules/papers.js +2 -4
- package/dist/eslint-rules/papers.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +8 -25
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/init.d.ts +1 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -9
- package/dist/init.js.map +1 -1
- package/dist/lib/markdown.d.ts +5 -81
- package/dist/lib/markdown.d.ts.map +1 -1
- package/dist/lib/markdown.js +5 -116
- package/dist/lib/markdown.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +2 -2
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/lib/skill-eval-fixture.d.ts +6 -4
- package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
- package/dist/lib/skill-eval-fixture.js +25 -18
- package/dist/lib/skill-eval-fixture.js.map +1 -1
- package/dist/new-paper.d.ts +5 -11
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +12 -23
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +4 -4
- package/dist/paper-prose.d.ts +17 -0
- package/dist/paper-prose.d.ts.map +1 -0
- package/dist/paper-prose.js +46 -0
- package/dist/paper-prose.js.map +1 -0
- 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/structure.d.ts.map +1 -1
- package/dist/structure.js +4 -3
- package/dist/structure.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/types.d.ts +0 -2
- package/dist/types.d.ts.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 +146 -20
- package/docs/e2e.md +4 -6
- package/docs/prior-art/paper-folder-scaffolding.md +24 -29
- 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/tex/claim-provenance.md +0 -1
- package/docs/rules.md +48 -47
- 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-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.ts +10 -13
- package/eslint-rules/paper-context.ts +2 -2
- package/eslint-rules/paper-typography.test.mjs +17 -85
- package/eslint-rules/paper-typography.ts +22 -98
- package/eslint-rules/papers.harness.mjs +1 -1
- package/eslint-rules/papers.ts +2 -4
- package/eslint-rules/tex-build.harness.mjs +2 -2
- package/eslint-rules/tex-build.ts +8 -25
- package/eslint.config.mjs +41 -2
- package/hooks/hooks.harness.ts +15 -15
- package/hooks/paper-edit-guard.hook.mjs +5 -8
- package/hooks/paper-edit-guard.test.ts +12 -3
- package/hooks/paper-skills-nudge.hook.mjs +2 -3
- package/hooks/paper-skills-nudge.test.ts +13 -3
- package/hooks/paper-status-gates.hook.mjs +2 -2
- package/hooks/paper-status-gates.test.ts +14 -2
- package/lib/markdown.test.mjs +5 -60
- package/lib/markdown.ts +9 -136
- package/lib/paper-config.ts +3 -2
- package/lib/skill-checks.test.mjs +4 -1
- package/lib/skill-eval-fixture.ts +25 -18
- package/package.json +1 -3
- 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/realm.jsonc +1 -1
- package/presets/venue-profile.schema.json +39 -0
- package/scripts/check.ts +4 -0
- package/scripts/harness-api.frozen.json +0 -5
- package/scripts/refresh-deadlines.test.ts +431 -0
- package/scripts/refresh-deadlines.ts +387 -0
- package/skills/cold-read-diff/SKILL.md +3 -3
- package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
- package/skills/grade-paper-writing/SKILL.md +14 -22
- package/skills/grade-paper-writing/SKILL.md.spec.ts +13 -21
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
- package/skills/grade-paper-writing/prose-lint.mjs +108 -278
- package/skills/grade-paper-writing/prose-lint.test.ts +184 -81
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +9 -12
- package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
- package/skills/paper-pipeline/description-language.eval.mjs +25 -15
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
- package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
- package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
- package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
- package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
- package/skills/paper-pipeline/scripts/README.md +0 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +8 -72
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +48 -156
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +81 -75
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/extract-pdf-facts.mjs +2 -2
- package/skills/tighten-paper/SKILL.md +29 -42
- package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +0 -13
- package/skills/verify-citations/scripts/bib-authors.cli.test.ts +23 -11
- package/skills/verify-citations/scripts/bib-authors.mjs +8 -93
- package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
- 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/build.harness.mjs +2 -2
- package/src/build.test.ts +1 -1
- package/src/build.ts +1 -6
- package/src/cli.harness.mjs +26 -46
- package/src/cli.test.ts +41 -40
- package/src/cli.ts +57 -58
- 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/folder-venue-rule.test.ts +1 -1
- package/src/init.test.ts +6 -7
- package/src/init.ts +3 -22
- package/src/lint-fix.test.ts +10 -4
- package/src/new-paper.harness.mjs +9 -28
- package/src/new-paper.test.ts +7 -6
- package/src/new-paper.ts +11 -30
- package/src/paper-files.test.ts +5 -4
- package/src/paper-files.ts +4 -4
- package/src/paper-prose.test.ts +49 -0
- package/src/paper-prose.ts +67 -0
- package/src/paper-settings.test.ts +167 -0
- package/src/paper-settings.ts +85 -11
- package/src/parse-args.test.ts +0 -1
- 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/structure.harness.ts +5 -11
- package/src/structure.ts +4 -3
- 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/types.ts +0 -2
- package/src/venue-rules.test.ts +82 -0
- package/src/venue-rules.ts +44 -12
- package/fixtures/real-markdown-paper/baseline.json +0 -22
- package/fixtures/real-markdown-paper/baseline.ts +0 -70
- package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
- package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
- package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
- package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
- package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
- package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
- package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
- package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
- package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
- package/skills/tighten-paper/structure.mjs +0 -364
- package/skills/tighten-paper/structure.test.ts +0 -117
- package/templates/paper/paper.md +0 -4
|
@@ -0,0 +1,25 @@
|
|
|
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 - MSR 2027</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">MSR 2027</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>Upcoming</h3><dl class="bsp"><dt><strong>Submission deadline</strong>: <span class="need-usertime" data-ts="1792468800">Tuesday Oct 20, 2026, 12 AM EDT</span></dt><dd>Submissions must be completed by this deadline to be reviewed.</dd>
|
|
18
|
+
<dt><strong>Resubmission deadline</strong>: <span class="need-usertime" data-ts="1792728000">Friday Oct 23, 2026, 12 AM EDT</span></dt><dd>Completed submissions may be updated until this deadline.</dd></dl>
|
|
19
|
+
<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="8XgRxNPYP5XebPvVSL6MyA==" src="https://static.hotcrp.com/@1749780400/scripts/jquery-1.12.4.min.js" crossorigin="anonymous" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script><![endif]-->
|
|
20
|
+
<![if !IE|gte IE 9]><script nonce="8XgRxNPYP5XebPvVSL6MyA==" src="https://static.hotcrp.com/@1749780400/scripts/jquery-3.7.1.min.js" crossorigin="anonymous" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="></script><![endif]>
|
|
21
|
+
<script nonce="8XgRxNPYP5XebPvVSL6MyA==">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":"msr2027-","snouns":["submission","submissions","Submission","Submissions"],"user":[]}</script><script nonce="8XgRxNPYP5XebPvVSL6MyA==" src="https://static.hotcrp.com/@1790985505/scripts/script.js" crossorigin="anonymous"></script>
|
|
22
|
+
<script nonce="8XgRxNPYP5XebPvVSL6MyA==" src="https://static.hotcrp.com/@1777496734/scripts/markdown-it.min.js" crossorigin="anonymous"></script>
|
|
23
|
+
<script nonce="8XgRxNPYP5XebPvVSL6MyA==" src="https://static.hotcrp.com/@1787955704/scripts/plugin.js" crossorigin="anonymous"></script>
|
|
24
|
+
<script nonce="8XgRxNPYP5XebPvVSL6MyA==">hotcrp.onload.time(240,0);hotcrp.init_deadlines({"now":1791194488.329003,"email":null,"sub":{"open":true,"sub":1792468800,"blind":true}})</script></body>
|
|
25
|
+
</html>
|
|
@@ -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/build.harness.mjs
CHANGED
|
@@ -482,7 +482,7 @@ try {
|
|
|
482
482
|
);
|
|
483
483
|
|
|
484
484
|
// ── no paper.tex: a REFUSAL, not a skip ───────────────────────────────────────────────
|
|
485
|
-
const bare = paper("bare", { "
|
|
485
|
+
const bare = paper("bare", { "PIPELINE-STATUS.md": "# x" });
|
|
486
486
|
const ntex = fakeTex();
|
|
487
487
|
const nr = await buildPaper(bare, {
|
|
488
488
|
cwd: root,
|
|
@@ -546,7 +546,7 @@ try {
|
|
|
546
546
|
|
|
547
547
|
// (c) no paper.tex: refused, and a PDF left from some earlier build goes too.
|
|
548
548
|
const bareStale = paper("bare-stale", {
|
|
549
|
-
"
|
|
549
|
+
"PIPELINE-STATUS.md": "# x",
|
|
550
550
|
"paper.pdf": "%PDF-stale-from-yesterday",
|
|
551
551
|
});
|
|
552
552
|
const bsTex = fakeTex();
|
package/src/build.test.ts
CHANGED
|
@@ -162,7 +162,7 @@ test("documentclass: an empty name is no class; a macro inside the name contribu
|
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
test("facts of a paper with no paper.tex: no class", () => {
|
|
165
|
-
assert.deepEqual(readFacts(paper({ "
|
|
165
|
+
assert.deepEqual(readFacts(paper({ "PIPELINE-STATUS.md": "# P\n" })), {
|
|
166
166
|
main: null,
|
|
167
167
|
documentclass: null,
|
|
168
168
|
venue: null,
|
package/src/build.ts
CHANGED
|
@@ -89,12 +89,7 @@ import { messageOf, printed } from "./domain/text.ts";
|
|
|
89
89
|
const PIPED: ["ignore", "pipe", "pipe"] = ["ignore", "pipe", "pipe"];
|
|
90
90
|
|
|
91
91
|
/** A directory counts as a paper by the same markers as `structure.ts` — one shared dictionary. */
|
|
92
|
-
export const PAPER_MARKERS = [
|
|
93
|
-
"PIPELINE-STATUS.md",
|
|
94
|
-
"paper.tex",
|
|
95
|
-
"paper.md",
|
|
96
|
-
CONFIG_FILE,
|
|
97
|
-
];
|
|
92
|
+
export const PAPER_MARKERS = ["PIPELINE-STATUS.md", "paper.tex", CONFIG_FILE];
|
|
98
93
|
|
|
99
94
|
/** The source paperlint compiles, and the job name every output file carries. */
|
|
100
95
|
export const MAIN = "paper.tex";
|