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/docs/rules.md
CHANGED
|
@@ -3,49 +3,53 @@
|
|
|
3
3
|
The README lists what each check catches in one line. This page says exactly which file each one
|
|
4
4
|
reads and when it fails. Errors fail `paperlint lint`; warnings print and do not.
|
|
5
5
|
|
|
6
|
-
| Rule | Level | Reads
|
|
7
|
-
| --------------------------------------------------------------------- | -------------------------------------------- |
|
|
8
|
-
| `paper/stages` | error | `PIPELINE-STATUS.md`
|
|
9
|
-
| `paper/source` | error | `PIPELINE-STATUS.md`
|
|
10
|
-
| [`
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
13
|
-
| `
|
|
14
|
-
| `paper/
|
|
15
|
-
| `
|
|
16
|
-
| `paper/
|
|
17
|
-
| `paper/
|
|
18
|
-
| `paper/
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
| [`
|
|
26
|
-
| [`tex/
|
|
27
|
-
| [`tex/
|
|
28
|
-
| [`
|
|
29
|
-
| [`
|
|
30
|
-
| [`tex/
|
|
31
|
-
| [`tex/
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
| `pdf/
|
|
41
|
-
|
|
|
42
|
-
| [`
|
|
43
|
-
| [`
|
|
44
|
-
|
|
|
45
|
-
| [`talk/
|
|
46
|
-
| [`talk/
|
|
47
|
-
| [`talk/
|
|
48
|
-
| [`talk/
|
|
6
|
+
| Rule | Level | Reads | Fails when |
|
|
7
|
+
| --------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
8
|
+
| `paper/stages` | error | `PIPELINE-STATUS.md` | a declared stage's PDF is missing, or the `bytes:` it names does not match the file's real size, or a frozen PDF exists that no stage declares |
|
|
9
|
+
| `paper/source` | error | `PIPELINE-STATUS.md` | a declared stage has no frozen `.tex` beside its PDF (a commit hash does not count — squash and gc destroy it) |
|
|
10
|
+
| [`cycle/record`](rules/cycle/record.md) | error | `paperlint.json` (`cycles`) | the paper's attempts at venues (`cycles`, one entry per attempt) hold one source's deadlines out of order (registration ≤ submission ≤ resubmission < notification < camera-ready), or copy a portal reading the venue's preset carries; reported on `PIPELINE-STATUS.md`. A record that does not parse — a bare date, two open cycles, an unknown key — is refused by `paperlint lint` itself before any rule runs, exit 2 — [page](rules/cycle/record.md) |
|
|
11
|
+
| [`cycle/evidence`](rules/cycle/evidence.md) | error | `paperlint.json` (`cycles`) → the paper folder | a closed cycle (`accepted`, `rejected`, `withdrawn`) or a deadline's `override` names an `evidence` file — the decision mail, the reviews, the withdrawal note, the chairs' mail — that is not in the paper folder; reported on `PIPELINE-STATUS.md` — [page](rules/cycle/evidence.md) |
|
|
12
|
+
| [`cycle/stage`](rules/cycle/stage.md) | error | `PIPELINE-STATUS.md` → `paperlint.json` (`cycles`) | with `cycles` declared, a stage names no `cycle: <id>`, names one no cycle has, or a `submitted`/`camera-ready` stage belongs to a cycle whose `phase` is still `porting` — [page](rules/cycle/stage.md) |
|
|
13
|
+
| [`cycle/port-done`](rules/cycle/port-done.md) | error | `paper.tex` → `paperlint.json` (`cycles`) → the preset's `template` | the current cycle still declares `"phase": "porting"` (which silences the venue's format rules) while the `\documentclass` is already the venue's template: the port is done, drop the phase — [page](rules/cycle/port-done.md) |
|
|
14
|
+
| [`paper/folder-venue-leftover`](rules/paper/folder-venue-leftover.md) | warn | the folder name → `paperlint.json` | the paper's folder is named after another shipped venue than the one it extends (`papers/aisec-2026/` for a paper now set for AIDC); reported on `PIPELINE-STATUS.md` — [page](rules/paper/folder-venue-leftover.md) |
|
|
15
|
+
| `paper/author-list` | error | `paper.tex` → `_build/references.json` | an entry's authors are not those of the version it cites — typically the arXiv preprint's list under a published venue. Checked online by `paperlint build` (DBLP), reported on the entry's own line |
|
|
16
|
+
| `paper/section-word` | warn | `paper.tex` and its body files, `paper.md` | `§` or `\S\ref` instead of the word Section. **Fixable:** `--fix` writes `Section~\ref{…}` / `Section 5` |
|
|
17
|
+
| `paper/leading-zero` | warn | `paper.tex` and its body files, `paper.md` | a decimal below 1 without its leading zero (`.05` instead of `0.05`, IEEE / ISO 80000-1 style), counted only where the reader sees it — prose, math and table cells, not macro options, column specs, comments, code or tikz. A number right after `¶` or `§` (`¶¶.42`) is a paragraph designator and is not reported; in a paragraph that cites numbers that way, another bare decimal is reported without a fix, with the zero as a suggestion. **Fixable** |
|
|
18
|
+
| `paper/figure-ref-style` | warn | `paper.tex` and its body files | `Fig.~\ref` and `Figure~\ref` mixed in one document; each minority occurrence is reported. **Fixable:** `--fix` writes the majority form |
|
|
19
|
+
| `bib/reachable-entry` | warn | `paper.tex` (its `filecontents` bibliography) | an entry has no doi, url or arXiv id — a reader has nothing to follow. Reported on the entry; no fix, the link has to be looked up |
|
|
20
|
+
| `paper/cite-exists` | error | `paper.tex` → `_build/references.json` | an entry's doi or arXiv id resolves to nothing, or to a different work. Checked online by `paperlint build`; a work that was merely not found is not a finding |
|
|
21
|
+
| `paper/refs-fresh` | error | `paper.tex` → `_build/references.json` | the bibliography changed since the build checked it (its SHA-256 differs) — run `npx paperlint build` |
|
|
22
|
+
| `paper/refs-checked` | warn | `paper.tex` → `_build/references.json` | the paper has a bibliography and no build has checked it, or the last build could not (no network) — so the two rules above did not run |
|
|
23
|
+
| `tex/future-promise` | warn | `paper.tex` and its body files | a camera-ready build still says "will be released" about something already handed over |
|
|
24
|
+
| `tex/acm-frontmatter-override` | error | `paper.tex` | an `acmart` build overrides ACM's front-matter commands and drops template elements from page 1 |
|
|
25
|
+
| [`tex/template`](rules/tex/template.md) | error | `paper.tex` → `paperlint.json` | the `\documentclass` is not the preset's `template` class, or lacks an option it names — [page](rules/tex/template.md) |
|
|
26
|
+
| [`tex/required-section`](rules/tex/required-section.md) | error | `paper.tex` → `paperlint.json` | a section the preset requires (AIDC: «LLM Usage Statement», last) is missing or not where it must be — [page](rules/tex/required-section.md) |
|
|
27
|
+
| [`tex/heading-case`](rules/tex/heading-case.md) | optional; the AgenticDev preset sets `error` | `paper.tex` and the files it includes | a word in the paper's `\title` or in a `\section`, `\subsection`, `\subsubsection`, `\paragraph` or `\subparagraph` title is not in the style its scope asks: `{ "title", "headings", "levels"? }`, each `chicago-headline` (Conference Publishing's quote of the Chicago Manual of Style), `sentence` (first word only) or `any`. Words that are a preposition in one title and a particle in another (`up`, `on`, `in`) are not read mid-title. **Fixable:** `--fix` — [page](rules/tex/heading-case.md) |
|
|
28
|
+
| [`format/layout-override`](rules/format/layout-override.md) | error | `paper.tex` → `paperlint.json` | the source changes the page layout the venue's template sets for the whole document — `geometry`, a layout `\setlength`, `\linespread`, `\baselinestretch` — [page](rules/format/layout-override.md) |
|
|
29
|
+
| [`anonymity/identity`](rules/anonymity/identity.md) | error | `paper.tex` → `_build/paper.facts.json` | a double-blind venue (`"blind": true`) and no `identity` declared, or a declared name, handle, email or project in the PDF's text, metadata or link targets — [page](rules/anonymity/identity.md) |
|
|
30
|
+
| [`tex/venue-leftover`](rules/tex/venue-leftover.md) | warn | `paper.tex` → `paperlint.json` | the text names another shipped venue than the one the paper extends («Submission to VenueA 2026» in a paper now set for VenueB) — [page](rules/tex/venue-leftover.md) |
|
|
31
|
+
| [`tex/claim-provenance`](rules/tex/claim-provenance.md) | warn | `paper.tex` | a sentence of the body states a number and does not say whose it is — no citation or link, no _we_/_our_, no `\ref` or "Table 2", no sample ("n = 134", "48 runs") — [page](rules/tex/claim-provenance.md) |
|
|
32
|
+
| [`tex/register`](rules/tex/register.md) | warn | `paper.tex` and the files it includes | the body runs to a post's register: more than 0.8 sentences per 1000 words open with _And_, _So_, _But_, _Nor_, _Or_ or _Yet_ — the limit is an option — [page](rules/tex/register.md) |
|
|
33
|
+
| [`tex/contrast-frames`](rules/tex/contrast-frames.md) | warn | `paper.tex` and the files it includes → `paperlint.json` | contrast frames (_X, not Y_, _not X but Y_, _rather than_, _instead of_, _as opposed to_) per 10,000 words fall outside the band the venue preset's anchors set (AIDC: 0–10.4, from five accepted ACSAC papers); above it, each frame is reported too — [page](rules/tex/contrast-frames.md) |
|
|
34
|
+
| [`tex/claim-emphasis`](rules/tex/claim-emphasis.md) | warn | `paper.tex` and the files it includes → `paperlint.json` | claims set in bold inside a sentence (six words or more, or holding a number; not a term, not a label opening a paragraph) per 10,000 words fall outside the preset's band (AIDC: 0–17.8); above it, each is reported too — [page](rules/tex/claim-emphasis.md) |
|
|
35
|
+
| [`tex/relation-markers`](rules/tex/relation-markers.md) | warn | `paper.tex` and the files it includes → `paperlint.json` | words that name how a sentence relates to the one before (_because_, _therefore_, _however_, _for example_ …) per 10,000 words fall outside the preset's band (AIDC: 25.4–135.0) — the floor that catches relations left to infer — [page](rules/tex/relation-markers.md) |
|
|
36
|
+
| `review/frontmatter` | error | `reviews/*.md` | the review's frontmatter fails paperlint's JSON Schema — a `findings` record without `id`/`status`, an unknown field, or an **open** finding with no `cause` (`skill-defect` · `missing-skill` · `hook` · `rule`). A review with no `findings` key is not checked for findings |
|
|
37
|
+
| `sibling/frontmatter` | warn | `siblings/*.md` (not `README.md`) | a sibling card's frontmatter has no `read:` saying how much of the competing paper was read (`full` · `abstract` · `none`) |
|
|
38
|
+
| `pdf/fresh` | error | `paper.tex` → `_build/paper.facts.json` | the paper names a venue and the facts cannot be judged: not JSON, a schema other than 3, or they describe a PDF that is gone or differs from the one on disk (its SHA-256) |
|
|
39
|
+
| `pdf/profile` | error | `paper.tex` → `paperlint.json` | `paperlint.json` is not JSON or has an unknown key, its `extends` does not resolve (not found, a cycle, a chain longer than four, a preset that fails the schema, an npm name), it names no `kind` while the preset has kinds, or names a kind the preset does not have |
|
|
40
|
+
| `pdf/fonts` | error | `paper.tex` → `_build/paper.facts.json` | a font the pages draw is Type 3 or not embedded, or no font starts with the family the venue preset names for body text (`fonts_text`) or headings (`fonts_title`) |
|
|
41
|
+
| `pdf/geometry` | error | `paper.tex` → `_build/paper.facts.json` | the page width or height is more than `dimTol` (default 0.05 in) off the preset's, or the column count differs |
|
|
42
|
+
| [`format/page-limit`](rules/format/page-limit.md) | error | `paper.tex` → `_build/paper.facts.json` | body or reference pages exceed the limits of the paper's kind. With `body_ends_at: "references"` (AIDC) the body ends at the references or the appendix, whichever comes first — found by hyperref's anchors, then the heading line — and the page it ends on counts when body text stands above the heading; with `appendix_in_body` too (MSR) the appendix is body wherever it sits and the reference pages are the ones holding only references; an end it cannot find, or signals that disagree, are reported — [page](rules/format/page-limit.md) |
|
|
43
|
+
| [`pdf/body-size`](rules/pdf/body-size.md) | error | `paper.tex` → `_build/paper.facts.json` | the body font size is more than `body_pt_tol` off the preset's, or the reference font size is outside the preset's range widened by `body_pt_tol`. banal reads 0.3 pt above the size the glyphs are set in; checked on five built accepted papers (none reported) and three variants one size off (each reported) — [page](rules/pdf/body-size.md) |
|
|
44
|
+
| `pdf/measured` | warn | `paper.tex` → `_build/paper.facts.json` | the paper's `paperlint.json` names no venue preset yet; or it names one and there are no facts (it was not built), or the facts carry no page geometry (banal was not found) — so the checks above did not run |
|
|
45
|
+
| [`talk/profile`](rules/talk/profile.md) | error | `paper.tex` → `paperlint.json` | the paper declares `talk` and names no preset, the preset has no `talk` block, the mode is not one the venue allows, or the venue sets no talk slot for the paper's kind — [page](rules/talk/profile.md) |
|
|
46
|
+
| [`talk/required-files`](rules/talk/required-files.md) | error | `paper.tex` → `paperlint.json`, the talk folder | a file the venue requires for the paper's talk mode (video, one-slide, captions…) is not in the talk folder — [page](rules/talk/required-files.md) |
|
|
47
|
+
| [`talk/undeclared`](rules/talk/undeclared.md) | warn | `paper.tex` → the paper's `talk/` folder | an `.mp4` sits in `talk/` and the paper declares no `talk`, so nothing checks it — [page](rules/talk/undeclared.md) |
|
|
48
|
+
| [`talk/duration`](rules/talk/duration.md) | error | `paper.tex` → the talk video | the video runs longer than the slot's `talk_s_max`, or its audio track is more than 1 s off the video's length — [page](rules/talk/duration.md) |
|
|
49
|
+
| [`talk/duration-floor`](rules/talk/duration-floor.md) | warn | `paper.tex` → the talk video | the video runs shorter than the slot's `talk_s_min` — [page](rules/talk/duration-floor.md) |
|
|
50
|
+
| [`talk/video-format`](rules/talk/video-format.md) | error | `paper.tex` → the talk video | the video is not ISO media, is QuickTime where the venue asks for MP4, is under `min_height_px`, or over `max_bytes` — [page](rules/talk/video-format.md) |
|
|
51
|
+
| [`talk/one-slide-size`](rules/talk/one-slide-size.md) | error | `paper.tex` → the one-slide PNG | the one-slide image is not the size the paper or the preset states, or not a PNG — [page](rules/talk/one-slide-size.md) |
|
|
52
|
+
| [`talk/captions-cover`](rules/talk/captions-cover.md) | warn | `paper.tex` → the captions and the video | the captions start late, end early or leave a long gap (uncalibrated thresholds) — [page](rules/talk/captions-cover.md) |
|
|
49
53
|
|
|
50
54
|
**Body files** are the files `paper.tex` includes from its document body (`\input`, `\include`,
|
|
51
55
|
`\subfile`, nested ones too), found in the paper's directory — not a preamble include, not a file
|
|
@@ -238,6 +242,7 @@ The numbers and packages above illustrate the shape; take yours from the venue's
|
|
|
238
242
|
| `template` | the `\documentclass` the venue's template uses — a whole line (`\documentclass[conference,compsoc]{IEEEtran}`) or a bare class name; `tex/template` requires the paper's class and every option named here |
|
|
239
243
|
| `aliases` | other names the venue goes by in a paper's text — its acronym, its parent conference: `["AISec", "ACM CCS"]`. Collected along `extends`, with `name`. Read by `tex/venue-leftover` and `paper/folder-venue-leftover` |
|
|
240
244
|
| `portal` | where the venue takes submissions, `{ "kind": "hotcrp", "url": "https://…" }` — read by [`paperlint submission`](submission.md); a child's value replaces its parent's |
|
|
245
|
+
| `deadlines` | the edition's deadlines as read: `[{ "what", "at", "source", "url", "read" }]`, `at` in UTC, one reading per kind and source. A paper whose current cycle names the preset derives them ([`docs/configuration.md`](configuration.md#deadlines)); shipped presets' portal readings are refreshed daily in paperlint's repository; a child's list replaces its parent's |
|
|
241
246
|
| `blind` | the venue reviews double-blind — set only where its call says so, with the quote. Turns on `anonymity/identity`; a child's value replaces its parent's |
|
|
242
247
|
| `required_sections` | sections the venue requires, `[{ "title": "LLM Usage Statement", "position": "last" }]`: the exact title, and `last` when it must close the paper. Read by `tex/required-section`; a child's list replaces its parent's |
|
|
243
248
|
| `format` | page size, columns, fonts, font sizes, and `kinds` (page limits per kind of paper) |
|
package/docs/submission.md
CHANGED
|
@@ -44,6 +44,11 @@ whether that PDF is the local one: HotCRP stores a PDF's sha256 as `sha2-<hex>`,
|
|
|
44
44
|
compares it with the sha256 of the paper's `paper.pdf` (or its `pdf` setting, or `--pdf <file>`).
|
|
45
45
|
`match yes` means the portal holds exactly the file you built. Exit 0 whenever the portal answered.
|
|
46
46
|
|
|
47
|
+
Then one `deadline` line per deadline in force for the current attempt — the venue preset's portal
|
|
48
|
+
readings with the paper's own call readings and overrides over them ([Deadlines](configuration.md#deadlines)):
|
|
49
|
+
the instant that binds, what set it (a reading and the day it was read, or an override with its
|
|
50
|
+
reason and evidence), and the other readings of it. Instants only; how long is left is not printed.
|
|
51
|
+
|
|
47
52
|
## `update`
|
|
48
53
|
|
|
49
54
|
Sends the PDF — the paper's own unless `--pdf <file>` names another — and, when asked, a new
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cycle/stage` on a scorecard's frontmatter beside a settings file: a stage names its cycle, the
|
|
3
|
+
* cycle exists, and no PDF was sent from a cycle still porting. Silent on the flat form.
|
|
4
|
+
*/
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import markdown from "@eslint/markdown";
|
|
8
|
+
import { Linter } from "eslint";
|
|
9
|
+
import { test } from "vitest";
|
|
10
|
+
import { useTempDir, writeTree } from "../test/support.ts";
|
|
11
|
+
import cycleStage from "./cycle-stage.ts";
|
|
12
|
+
|
|
13
|
+
const root = useTempDir("cycle-stage-");
|
|
14
|
+
const linter = new Linter({ cwd: root });
|
|
15
|
+
|
|
16
|
+
const SETTINGS = JSON.stringify({
|
|
17
|
+
cycles: [
|
|
18
|
+
{
|
|
19
|
+
id: "alpha-2026",
|
|
20
|
+
venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
|
|
21
|
+
opened: "2026-07-01",
|
|
22
|
+
outcome: { kind: "rejected", date: "2026-09-08", evidence: "r.md" },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "beta-2027",
|
|
26
|
+
venue: { kind: "preset", extends: "paperlint:ieee-conference" },
|
|
27
|
+
opened: "2026-09-09",
|
|
28
|
+
phase: "porting",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** The messages of the rule on a PIPELINE-STATUS.md with `frontmatter`, beside `settings`. */
|
|
34
|
+
function lint(name, frontmatter, settings = SETTINGS) {
|
|
35
|
+
const dir = writeTree(join(root, name), {
|
|
36
|
+
...(settings === null ? {} : { "paperlint.json": settings }),
|
|
37
|
+
});
|
|
38
|
+
const msgs = linter.verify(
|
|
39
|
+
`---\n${frontmatter}\n---\n\n# Status\n`,
|
|
40
|
+
[
|
|
41
|
+
{
|
|
42
|
+
files: ["**/*.md"],
|
|
43
|
+
plugins: { markdown, cycle: cycleStage },
|
|
44
|
+
language: "markdown/gfm",
|
|
45
|
+
languageOptions: { frontmatter: "yaml" },
|
|
46
|
+
rules: { "cycle/stage": "error" },
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
join(dir, "PIPELINE-STATUS.md"),
|
|
50
|
+
);
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
msgs.filter((m) => m.fatal),
|
|
53
|
+
[],
|
|
54
|
+
`${name}: the rule threw`,
|
|
55
|
+
);
|
|
56
|
+
return msgs.map((m) => m.message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const SUBMITTED = (cycle) =>
|
|
60
|
+
`stages:\n - stage: submitted\n ${cycle}\n date: 2026-08-06\n pdf: versions/a.pdf\n bytes: 1`;
|
|
61
|
+
|
|
62
|
+
test("a stage naming a declared cycle: silent", () => {
|
|
63
|
+
assert.deepEqual(lint("ok", SUBMITTED("cycle: alpha-2026")), []);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("the flat form (no `cycles`), no settings file, or no stages: silent", () => {
|
|
67
|
+
const flat = JSON.stringify({ extends: "paperlint:aidc" });
|
|
68
|
+
assert.deepEqual(lint("flat", SUBMITTED("venue: Somewhere"), flat), []);
|
|
69
|
+
assert.deepEqual(lint("none", SUBMITTED("venue: Somewhere"), null), []);
|
|
70
|
+
assert.deepEqual(lint("empty", "stages: []"), []);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("a stage with no `cycle` names the declared ids; one naming an unknown id names it", () => {
|
|
74
|
+
assert.deepEqual(lint("no-cycle", SUBMITTED("venue: Somewhere")), [
|
|
75
|
+
"the «submitted» stage (2026-08-06) names no `cycle` — this paper declares `cycles` in paperlint.json, so each stage says which attempt it belongs to: `cycle: <id>`, one of alpha-2026 · beta-2027",
|
|
76
|
+
]);
|
|
77
|
+
assert.deepEqual(lint("unknown", SUBMITTED("cycle: gamma-2028")), [
|
|
78
|
+
"the «submitted» stage (2026-08-06) names the cycle «gamma-2028», which paperlint.json does not declare — the declared ids: alpha-2026 · beta-2027",
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("a PDF sent from a cycle still porting is named; a stage that sends none is not", () => {
|
|
83
|
+
assert.deepEqual(lint("porting", SUBMITTED("cycle: beta-2027")), [
|
|
84
|
+
"the «submitted» stage (2026-08-06) belongs to the cycle «beta-2027», whose phase is still `porting`: a PDF was sent while the record says the source is in the previous venue's template. Finish the port and drop the phase, or the stage is wrong",
|
|
85
|
+
]);
|
|
86
|
+
assert.deepEqual(
|
|
87
|
+
lint(
|
|
88
|
+
"arxiv-porting",
|
|
89
|
+
SUBMITTED("cycle: beta-2027").replace("submitted", "arxiv"),
|
|
90
|
+
),
|
|
91
|
+
[],
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a settings file that is not JSON, or frontmatter that is not YAML: silent — other rules report those", () => {
|
|
96
|
+
assert.deepEqual(lint("not-json", SUBMITTED("venue: X"), "{ not json"), []);
|
|
97
|
+
assert.deepEqual(lint("not-yaml", "stages: [unclosed"), []);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("`cycles: []` declared: a stage without a cycle is told there are none to name", () => {
|
|
101
|
+
const [msg] = lint(
|
|
102
|
+
"no-cycles",
|
|
103
|
+
SUBMITTED("venue: X"),
|
|
104
|
+
JSON.stringify({ cycles: [] }),
|
|
105
|
+
);
|
|
106
|
+
assert.match(msg, /one of \(none\)$/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("a stage and a cycle that are not strings, and a quoted date, are shown as written", () => {
|
|
110
|
+
const [msg] = lint(
|
|
111
|
+
"numbers",
|
|
112
|
+
'stages:\n - stage: 5\n cycle: 7\n date: "2026-08-06"\n pdf: versions/a.pdf\n bytes: 1',
|
|
113
|
+
);
|
|
114
|
+
assert.match(msg, /the «5» stage \(2026-08-06\) names the cycle «7»/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("a stage entry with no `stage` key is shown as an empty name", () => {
|
|
118
|
+
const [msg] = lint(
|
|
119
|
+
"no-stage",
|
|
120
|
+
"stages:\n - cycle: gamma-2028\n date: 2026-08-06\n pdf: versions/a.pdf\n bytes: 1",
|
|
121
|
+
);
|
|
122
|
+
assert.match(
|
|
123
|
+
msg,
|
|
124
|
+
/^the «» stage \(2026-08-06\) names the cycle «gamma-2028»/,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a YAML date without quotes is still a date in the message", () => {
|
|
129
|
+
const [msg] = lint("yaml-date", SUBMITTED("venue: X"));
|
|
130
|
+
assert.match(msg, /\(2026-08-06\)/);
|
|
131
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cycle/stage` — a declared stage names the ATTEMPT it belongs to, and that attempt exists.
|
|
3
|
+
*
|
|
4
|
+
* With `cycles` in the paper's `paperlint.json`, a `stages` entry in the scorecard's frontmatter
|
|
5
|
+
* says `cycle: <id>` instead of a free `venue:` string: the venue's name is then the cycle's
|
|
6
|
+
* preset's, spelled once, and a consumer can tell which rows of the scorecard belong to which
|
|
7
|
+
* attempt. Three things can be wrong, each an error:
|
|
8
|
+
*
|
|
9
|
+
* noCycle `cycles` is declared and a stage names no `cycle`
|
|
10
|
+
* unknownCycle the stage's `cycle` is no declared cycle's id
|
|
11
|
+
* porting a `submitted` or `camera-ready` stage in a cycle whose phase is still `porting` —
|
|
12
|
+
* a PDF was sent while the record says the source is in the previous venue's template
|
|
13
|
+
*
|
|
14
|
+
* Without `cycles` (the flat form) the rule is silent: `venue:` stays what it was. The settings file
|
|
15
|
+
* is read here only for the cycles' ids and phases; `cycle/record` (src/cycle-rules.ts) judges the
|
|
16
|
+
* whole record.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { load } from "js-yaml";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import type { RuleContext } from "./rule-context.ts";
|
|
23
|
+
|
|
24
|
+
const SETTINGS_FILE = "paperlint.json";
|
|
25
|
+
|
|
26
|
+
/** What this rule reads of a cycle: its id and phase. `cycle/record` parses the rest. */
|
|
27
|
+
const CycleHead = z.looseObject({
|
|
28
|
+
id: z.string(),
|
|
29
|
+
phase: z.string().optional(),
|
|
30
|
+
});
|
|
31
|
+
const Settings = z.looseObject({
|
|
32
|
+
cycles: z.array(CycleHead.catch({ id: "" })).optional(),
|
|
33
|
+
});
|
|
34
|
+
const Frontmatter = z.looseObject({ stages: z.unknown().optional() }).catch({});
|
|
35
|
+
const Stage = z
|
|
36
|
+
.looseObject({
|
|
37
|
+
stage: z.unknown().optional(),
|
|
38
|
+
cycle: z.unknown().optional(),
|
|
39
|
+
date: z.unknown().optional(),
|
|
40
|
+
})
|
|
41
|
+
.catch({});
|
|
42
|
+
|
|
43
|
+
/** The stages that send a PDF to the venue: a cycle still porting cannot have one. */
|
|
44
|
+
const SENT: readonly string[] = ["submitted", "camera-ready"];
|
|
45
|
+
|
|
46
|
+
interface Head {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly phase: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The declared cycles' heads, or null when the paper has no `cycles` (or no readable settings). */
|
|
52
|
+
function cyclesOf(dir: string): readonly Head[] | null {
|
|
53
|
+
const file = join(dir, SETTINGS_FILE);
|
|
54
|
+
if (!existsSync(file)) return null;
|
|
55
|
+
try {
|
|
56
|
+
const s = Settings.safeParse(JSON.parse(readFileSync(file, "utf8")));
|
|
57
|
+
return s.success && s.data.cycles !== undefined
|
|
58
|
+
? s.data.cycles.map((c) => ({ id: c.id, phase: c.phase ?? "prepared" }))
|
|
59
|
+
: null;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const text = (v: unknown): string =>
|
|
66
|
+
typeof v === "string" ? v : v === undefined ? "" : JSON.stringify(v);
|
|
67
|
+
|
|
68
|
+
/** A YAML date without quotes is a `Date`; shown as the day it names, as `paper/stages` does. */
|
|
69
|
+
const dateText = (v: unknown): string =>
|
|
70
|
+
v instanceof Date ? v.toISOString().slice(0, 10) : text(v);
|
|
71
|
+
|
|
72
|
+
interface Finding {
|
|
73
|
+
readonly messageId: "noCycle" | "unknownCycle" | "porting";
|
|
74
|
+
readonly data: Readonly<Record<string, string>>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Each stage against the declared cycles. Pure. */
|
|
78
|
+
export function judgeStages(
|
|
79
|
+
stages: readonly unknown[],
|
|
80
|
+
cycles: readonly Head[],
|
|
81
|
+
): readonly Finding[] {
|
|
82
|
+
const known = cycles.map((c) => c.id).join(" · ") || "(none)";
|
|
83
|
+
return stages.flatMap((entry): readonly Finding[] => {
|
|
84
|
+
const s = Stage.parse(entry);
|
|
85
|
+
const stage = text(s.stage);
|
|
86
|
+
const date = dateText(s.date);
|
|
87
|
+
if (s.cycle === undefined)
|
|
88
|
+
return [{ messageId: "noCycle", data: { stage, date, known } }];
|
|
89
|
+
const id = text(s.cycle);
|
|
90
|
+
const cycle = cycles.find((c) => c.id === id);
|
|
91
|
+
if (cycle === undefined)
|
|
92
|
+
return [{ messageId: "unknownCycle", data: { stage, date, id, known } }];
|
|
93
|
+
return SENT.includes(stage) && cycle.phase === "porting"
|
|
94
|
+
? [{ messageId: "porting", data: { stage, date, id } }]
|
|
95
|
+
: [];
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default {
|
|
100
|
+
rules: {
|
|
101
|
+
stage: {
|
|
102
|
+
meta: {
|
|
103
|
+
type: "problem" as const,
|
|
104
|
+
docs: {
|
|
105
|
+
description:
|
|
106
|
+
"with `cycles` declared, every stage names the cycle it belongs to, and no PDF is sent from a cycle still porting",
|
|
107
|
+
url: "https://github.com/zernie/paperlint/blob/main/docs/rules/cycle/stage.md",
|
|
108
|
+
},
|
|
109
|
+
schema: [],
|
|
110
|
+
messages: {
|
|
111
|
+
noCycle:
|
|
112
|
+
"the «{{stage}}» stage ({{date}}) names no `cycle` — this paper declares `cycles` in paperlint.json, so each stage says which attempt it belongs to: `cycle: <id>`, one of {{known}}",
|
|
113
|
+
unknownCycle:
|
|
114
|
+
"the «{{stage}}» stage ({{date}}) names the cycle «{{id}}», which paperlint.json does not declare — the declared ids: {{known}}",
|
|
115
|
+
porting:
|
|
116
|
+
"the «{{stage}}» stage ({{date}}) belongs to the cycle «{{id}}», whose phase is still `porting`: a PDF was sent while the record says the source is in the previous venue's template. Finish the port and drop the phase, or the stage is wrong",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
create(context: RuleContext) {
|
|
120
|
+
const cycles = cyclesOf(dirname(context.filename));
|
|
121
|
+
if (cycles === null) return {};
|
|
122
|
+
return {
|
|
123
|
+
yaml(node: { readonly value: string }) {
|
|
124
|
+
const data = ((): unknown => {
|
|
125
|
+
try {
|
|
126
|
+
return load(node.value);
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined; // `paper/stages` reports the unreadable YAML
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
const stages = Frontmatter.parse(data).stages;
|
|
132
|
+
if (!Array.isArray(stages)) return;
|
|
133
|
+
judgeStages(stages, cycles).forEach((f) => {
|
|
134
|
+
context.report({ node, messageId: f.messageId, data: f.data });
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|