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.
Files changed (152) hide show
  1. package/.github/workflows/ci.yml +3 -0
  2. package/.github/workflows/deadlines.yml +100 -0
  3. package/README.md +24 -11
  4. package/dist/adapters/hotcrp/deadlines.d.ts +8 -0
  5. package/dist/adapters/hotcrp/deadlines.d.ts.map +1 -0
  6. package/dist/adapters/hotcrp/deadlines.io.d.ts +6 -0
  7. package/dist/adapters/hotcrp/deadlines.io.d.ts.map +1 -0
  8. package/dist/adapters/hotcrp/deadlines.io.js +26 -0
  9. package/dist/adapters/hotcrp/deadlines.io.js.map +1 -0
  10. package/dist/adapters/hotcrp/deadlines.js +134 -0
  11. package/dist/adapters/hotcrp/deadlines.js.map +1 -0
  12. package/dist/adapters/hotcrp/index.d.ts +2 -0
  13. package/dist/adapters/hotcrp/index.d.ts.map +1 -1
  14. package/dist/adapters/hotcrp/index.js +2 -0
  15. package/dist/adapters/hotcrp/index.js.map +1 -1
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +42 -7
  18. package/dist/cli.js.map +1 -1
  19. package/dist/cycle-rules.d.ts +24 -0
  20. package/dist/cycle-rules.d.ts.map +1 -0
  21. package/dist/cycle-rules.js +212 -0
  22. package/dist/cycle-rules.js.map +1 -0
  23. package/dist/domain/body-pages.d.ts +29 -0
  24. package/dist/domain/body-pages.d.ts.map +1 -1
  25. package/dist/domain/body-pages.js +25 -0
  26. package/dist/domain/body-pages.js.map +1 -1
  27. package/dist/domain/cycle.d.ts +148 -0
  28. package/dist/domain/cycle.d.ts.map +1 -0
  29. package/dist/domain/cycle.js +281 -0
  30. package/dist/domain/cycle.js.map +1 -0
  31. package/dist/domain/deadline.d.ts +128 -0
  32. package/dist/domain/deadline.d.ts.map +1 -0
  33. package/dist/domain/deadline.js +281 -0
  34. package/dist/domain/deadline.js.map +1 -0
  35. package/dist/eslint-rules/cycle-stage.d.ts +39 -0
  36. package/dist/eslint-rules/cycle-stage.d.ts.map +1 -0
  37. package/dist/eslint-rules/cycle-stage.js +119 -0
  38. package/dist/eslint-rules/cycle-stage.js.map +1 -0
  39. package/dist/lib/paper-config.d.ts.map +1 -1
  40. package/dist/lib/paper-config.js +2 -0
  41. package/dist/lib/paper-config.js.map +1 -1
  42. package/dist/paper-includes.d.ts +3 -0
  43. package/dist/paper-includes.d.ts.map +1 -1
  44. package/dist/paper-includes.js +17 -2
  45. package/dist/paper-includes.js.map +1 -1
  46. package/dist/paper-settings.d.ts +7 -0
  47. package/dist/paper-settings.d.ts.map +1 -1
  48. package/dist/paper-settings.js +61 -8
  49. package/dist/paper-settings.js.map +1 -1
  50. package/dist/ports/venue-deadlines.d.ts +14 -0
  51. package/dist/ports/venue-deadlines.d.ts.map +1 -0
  52. package/dist/ports/venue-deadlines.js +2 -0
  53. package/dist/ports/venue-deadlines.js.map +1 -0
  54. package/dist/presets.d.ts +22 -3
  55. package/dist/presets.d.ts.map +1 -1
  56. package/dist/presets.js +26 -17
  57. package/dist/presets.js.map +1 -1
  58. package/dist/submission.d.ts +16 -0
  59. package/dist/submission.d.ts.map +1 -1
  60. package/dist/submission.js +59 -16
  61. package/dist/submission.js.map +1 -1
  62. package/dist/tex-requirements.d.ts +12 -0
  63. package/dist/tex-requirements.d.ts.map +1 -1
  64. package/dist/tex-requirements.js +26 -3
  65. package/dist/tex-requirements.js.map +1 -1
  66. package/dist/tex-venue-rules.d.ts +24 -2
  67. package/dist/tex-venue-rules.d.ts.map +1 -1
  68. package/dist/tex-venue-rules.js +98 -10
  69. package/dist/tex-venue-rules.js.map +1 -1
  70. package/dist/venue-rules.d.ts +12 -1
  71. package/dist/venue-rules.d.ts.map +1 -1
  72. package/dist/venue-rules.js +81 -17
  73. package/dist/venue-rules.js.map +1 -1
  74. package/docs/configuration.md +140 -12
  75. package/docs/e2e.md +14 -12
  76. package/docs/rules/cycle/evidence.md +87 -0
  77. package/docs/rules/cycle/port-done.md +75 -0
  78. package/docs/rules/cycle/record.md +152 -0
  79. package/docs/rules/cycle/stage.md +85 -0
  80. package/docs/rules/format/page-limit.md +18 -1
  81. package/docs/rules/tex/template.md +25 -5
  82. package/docs/rules/tex/venue-leftover.md +13 -0
  83. package/docs/rules.md +68 -55
  84. package/docs/submission.md +10 -1
  85. package/eslint-rules/cycle-stage.test.mjs +131 -0
  86. package/eslint-rules/cycle-stage.ts +141 -0
  87. package/eslint.config.mjs +41 -1
  88. package/fixtures/build-e2e/msr/PIPELINE-STATUS.md +3 -0
  89. package/fixtures/build-e2e/msr/expect.json +18 -0
  90. package/fixtures/build-e2e/msr/paper.tex +49 -0
  91. package/fixtures/build-e2e/msr/paperlint.json +1 -0
  92. package/fixtures/build-e2e/msr-appendix-after/PIPELINE-STATUS.md +3 -0
  93. package/fixtures/build-e2e/msr-appendix-after/expect.json +20 -0
  94. package/fixtures/build-e2e/msr-appendix-after/paper.tex +48 -0
  95. package/fixtures/build-e2e/msr-appendix-after/paperlint.json +1 -0
  96. package/lib/paper-config.ts +3 -0
  97. package/package.json +1 -1
  98. package/presets/aidc.jsonc +24 -3
  99. package/presets/aidc.md +6 -3
  100. package/presets/ieee-conference.md +1 -1
  101. package/presets/msr.jsonc +95 -0
  102. package/presets/msr.md +98 -0
  103. package/presets/venue-profile.schema.json +64 -0
  104. package/scripts/check.ts +4 -0
  105. package/scripts/refresh-deadlines.test.ts +431 -0
  106. package/scripts/refresh-deadlines.ts +387 -0
  107. package/skills/camera-ready/SKILL.md +8 -1
  108. package/skills/camera-ready/SKILL.md.spec.ts +7 -0
  109. package/skills/submission-portal/SKILL.md +2 -2
  110. package/skills/submission-portal/SKILL.md.spec.ts +1 -1
  111. package/skills/submit-paper/SKILL.md +9 -1
  112. package/skills/submit-paper/SKILL.md.spec.ts +8 -0
  113. package/src/adapters/hotcrp/cassette/README.md +22 -0
  114. package/src/adapters/hotcrp/cassette/deadlines-aidc26.html +17 -0
  115. package/src/adapters/hotcrp/cassette/deadlines-fse2027.html +24 -0
  116. package/src/adapters/hotcrp/cassette/deadlines-icse2027.html +25 -0
  117. package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +25 -0
  118. package/src/adapters/hotcrp/cassette/deadlines-nosuch.html +18 -0
  119. package/src/adapters/hotcrp/cassette/deadlines-sosp26.html +26 -0
  120. package/src/adapters/hotcrp/deadlines.io.test.ts +86 -0
  121. package/src/adapters/hotcrp/deadlines.io.ts +27 -0
  122. package/src/adapters/hotcrp/deadlines.test.ts +170 -0
  123. package/src/adapters/hotcrp/deadlines.ts +161 -0
  124. package/src/adapters/hotcrp/index.ts +2 -0
  125. package/src/cli.test.ts +9 -0
  126. package/src/cli.ts +50 -6
  127. package/src/cycle-rules.test.ts +278 -0
  128. package/src/cycle-rules.ts +273 -0
  129. package/src/domain/body-pages.test.ts +144 -0
  130. package/src/domain/body-pages.ts +61 -1
  131. package/src/domain/cycle.test.ts +456 -0
  132. package/src/domain/cycle.ts +432 -0
  133. package/src/domain/deadline.test.ts +386 -0
  134. package/src/domain/deadline.ts +472 -0
  135. package/src/new-paper.test.ts +28 -0
  136. package/src/paper-includes.test.ts +26 -0
  137. package/src/paper-includes.ts +20 -2
  138. package/src/paper-settings-commands.test.ts +1 -1
  139. package/src/paper-settings.test.ts +167 -0
  140. package/src/paper-settings.ts +85 -11
  141. package/src/ports/venue-deadlines.ts +14 -0
  142. package/src/preset-card-rule.test.ts +1 -0
  143. package/src/presets.test.ts +171 -1
  144. package/src/presets.ts +65 -15
  145. package/src/rule-cases.test.ts +144 -0
  146. package/src/submission.test.ts +147 -4
  147. package/src/submission.ts +100 -18
  148. package/src/tex-requirements.ts +52 -4
  149. package/src/tex-venue-rules.test.ts +237 -0
  150. package/src/tex-venue-rules.ts +130 -15
  151. package/src/venue-rules.test.ts +255 -2
  152. package/src/venue-rules.ts +115 -22
@@ -0,0 +1,75 @@
1
+ # cycle/port-done
2
+
3
+ **Level:** error · **Reads:** `paper.tex` → the paper's `paperlint.json` (`cycles`) → the venue
4
+ preset's `template` · **Reported on:** `paper.tex`
5
+
6
+ ## What it catches
7
+
8
+ The current cycle declares `"phase": "porting"` — the source is still in the previous venue's
9
+ template, so the venue's format rules are silent — and the paper's `\documentclass` is already the
10
+ one the venue's preset requires. The port is done and the declaration is stale.
11
+
12
+ ## Why
13
+
14
+ `"phase": "porting"` is a record of open work, not a softer severity: the day a new venue is chosen
15
+ the source is still in the old venue's template, and a page count under the wrong template says
16
+ nothing about the new limit. While the phase is declared, every `pdf/*`, `format/*`, `anonymity/*`
17
+ and `tex/*` venue rule stays silent for that paper. A declaration that silences rules must not be
18
+ able to stay on after its reason is gone: once the class line is the venue's, this rule fires, and
19
+ the only way to be quiet again is to drop the phase — which turns the format rules back on.
20
+
21
+ ## Examples
22
+
23
+ Failing — `paperlint.json`:
24
+
25
+ ```json
26
+ {
27
+ "cycles": [
28
+ {
29
+ "id": "blind-2027",
30
+ "venue": { "kind": "preset", "extends": "./blindconf.jsonc" },
31
+ "opened": "2026-09-09",
32
+ "phase": "porting"
33
+ }
34
+ ]
35
+ }
36
+ ```
37
+
38
+ where the preset's `template` is `\documentclass[conference,compsoc]{IEEEtran}`, and `paper.tex`
39
+ begins with that very line:
40
+
41
+ ```text
42
+ papers/my-paper/paper.tex
43
+ 1:1 error the \documentclass is already `\documentclass[conference,compsoc]{IEEEtran}`, which
44
+ BlindConf requires, and the cycle «blind-2027» still declares "phase": "porting" — the
45
+ port is done: remove the phase, so the venue's format rules judge this paper again
46
+ cycle/port-done
47
+ ```
48
+
49
+ Passing:
50
+
51
+ - the same paper with the phase removed (or `"phase": "prepared"`);
52
+ - a cycle still porting whose `paper.tex` still has the old venue's class line — the port is open
53
+ work, as declared;
54
+ - a preset that names no `template`: there is nothing to compare, as for `tex/template`;
55
+ - a paper whose `paperlint.json` has no `cycles`.
56
+
57
+ ## Options / preset fields
58
+
59
+ No rule options. It reads the preset's `template`, the field [`tex/template`](../tex/template.md)
60
+ reads, and compares the way that rule does.
61
+
62
+ ## What it does not check
63
+
64
+ - Whether the port is complete beyond the class line — the page limit, the fonts, the sections. Those
65
+ are the format rules' to judge, and they judge again the moment the phase is dropped.
66
+ - A cycle declared `porting` whose class line is still the old one: that is the state the phase
67
+ describes.
68
+ - A `porting` cycle that has already sent a PDF — [`cycle/stage`](stage.md) reports that on the
69
+ scorecard.
70
+
71
+ ## How to fix
72
+
73
+ Remove `"phase": "porting"` from the current cycle in `paperlint.json`, then run `paperlint lint`
74
+ and `paperlint build`: the venue's format rules judge the paper again, and what they report is the
75
+ rest of the port.
@@ -0,0 +1,152 @@
1
+ # cycle/record
2
+
3
+ **Level:** error · **Reads:** the paper's `paperlint.json` (`cycles`) and the venue presets it names ·
4
+ **Reported on:** the first line of the paper's `PIPELINE-STATUS.md`
5
+
6
+ ## What it catches
7
+
8
+ The paper's `cycles` — its attempts at venues, one entry each, oldest first — do not describe one
9
+ consistent history:
10
+
11
+ - **one source's deadlines are out of order.** The order a venue's deadlines keep is
12
+
13
+ ```text
14
+ registration ≤ submission ≤ resubmission < notification < camera-ready
15
+ ```
16
+
17
+ and it is checked within each source: the portal's readings among themselves, the call's among
18
+ themselves, the overrides among themselves. A call's abstract day after its own paper day is a
19
+ date typed wrong or an AoE day miscounted.
20
+
21
+ - **a portal reading the venue's preset already carries.** A cycle whose venue is a preset derives
22
+ the portal's readings from it (`presets/msr.jsonc` carries MSR's); the same reading typed into the
23
+ paper is a second copy, and it goes stale the day paperlint updates the preset.
24
+
25
+ The structural problems of the record — a bare date where an instant is needed, two open cycles, an
26
+ open cycle that is not the last, two cycles with one id, one deadline in two entries, two readings
27
+ from one source, an override without its reason or evidence, an unknown key anywhere in the record
28
+ — are refused earlier, by `paperlint lint` itself before any rule runs, with one line naming the
29
+ file, the place and the problem, and exit code 2, the same way an unknown key in `paperlint.json` is
30
+ refused. This rule judges what the record parses with.
31
+
32
+ ## Why
33
+
34
+ A deadline is the one fact in the record whose error costs the attempt. The record keeps what each
35
+ page said — the portal, which refuses the upload, and the call, which only announces — with where and
36
+ when it was read, and the **earlier** reading binds unless a human override says otherwise. That the
37
+ portal and the call disagree is expected and is not a finding: at MSR 2027 the portal's "submissions
38
+ must be completed by" falls at 04:00 UTC on the call's _abstract_ day, four days before the call's
39
+ _paper_ day, and keeping both is how the earlier one is seen. What is a finding is a source that
40
+ contradicts itself, and a fact recorded in two places.
41
+
42
+ ## Examples
43
+
44
+ Failing — `paperlint.json`, the call's own dates inverted:
45
+
46
+ ```json
47
+ {
48
+ "cycles": [
49
+ {
50
+ "id": "conf-2027",
51
+ "venue": { "kind": "preset", "extends": "paperlint:acm-sigconf" },
52
+ "opened": "2026-09-09",
53
+ "deadlines": [
54
+ {
55
+ "what": "registration",
56
+ "observed": [
57
+ {
58
+ "at": "2026-10-25 AoE",
59
+ "source": "call",
60
+ "url": "https://conf.example/cfp",
61
+ "read": "2026-09-09"
62
+ }
63
+ ]
64
+ },
65
+ {
66
+ "what": "submission",
67
+ "observed": [
68
+ {
69
+ "at": "2026-10-23 AoE",
70
+ "source": "call",
71
+ "url": "https://conf.example/cfp",
72
+ "read": "2026-09-09"
73
+ }
74
+ ]
75
+ }
76
+ ]
77
+ }
78
+ ]
79
+ }
80
+ ```
81
+
82
+ ```text
83
+ papers/my-paper/PIPELINE-STATUS.md
84
+ 1:1 error paperlint.json, cycle «conf-2027»: the call's "registration" (2026-10-26T11:59:59Z) is
85
+ after its "submission" (2026-10-24T11:59:59Z) — a date typed wrong, or an AoE day
86
+ miscounted (write a call's date as "YYYY-MM-DD AoE" and paperlint converts it)
87
+ cycle/record
88
+ ```
89
+
90
+ Failing — a cycle at `paperlint:msr` that copies the portal's reading the preset carries:
91
+
92
+ ```text
93
+ 1:1 error paperlint.json, cycle «msr-2027»: the "submission" deadline records a portal reading,
94
+ and the preset paperlint:msr carries the portal's reading of it (2026-10-20T04:00:00Z,
95
+ read 2026-10-05) — a second copy drifts when paperlint updates the preset: delete this
96
+ one (the call's reading and an override stay) cycle/record
97
+ ```
98
+
99
+ Passing:
100
+
101
+ - the first record with the registration on `2026-10-20 AoE`;
102
+ - a cycle at `paperlint:msr` recording the call's abstract day as `registration` (`2026-10-20 AoE`)
103
+ and its paper day as `submission` (`2026-10-23 AoE`): the preset's portal `submission`
104
+ (`2026-10-20T04:00:00Z`) is earlier than both, binds, and is not a contradiction;
105
+ - a cycle with no `deadlines` at all;
106
+ - a paper whose `paperlint.json` has no `cycles` (the flat form, `"extends": "paperlint:…"`).
107
+
108
+ ## Options / preset fields
109
+
110
+ No rule options. One deadline entry:
111
+
112
+ ```jsonc
113
+ {
114
+ "what": "submission", // registration (the call's abstract) · submission (the full paper) · resubmission · notification · camera-ready
115
+ "observed": [
116
+ {
117
+ "at": "2026-10-23 AoE", // or an instant with its zone: 2026-10-20T04:00:00Z
118
+ "source": "call", // or "portal"
119
+ "url": "https://conf.example/cfp",
120
+ "read": "2026-10-05", // the day the page was read
121
+ },
122
+ ],
123
+ // optional; wins over every reading
124
+ "override": {
125
+ "at": "2026-10-27T04:00:00Z",
126
+ "reason": "the chairs extended the deadline by mail",
127
+ "evidence": "mail/extension.eml",
128
+ },
129
+ }
130
+ ```
131
+
132
+ The preset field is `deadlines` in a venue preset: a list of readings `{ "what", "at", "source",
133
+ "url", "read" }`, `at` in UTC. Shipped presets carry their portal's readings and are refreshed by a
134
+ scheduled workflow in paperlint's repository; your own preset may carry readings the same way.
135
+
136
+ ## What it does not check
137
+
138
+ - Whether a reading is still what the page says. The rule has no network and no clock; a shipped
139
+ preset's readings are re-read daily in paperlint's repository and reach you as a patch release.
140
+ A venue without a preset (`"kind": "named"`) is re-read by you.
141
+ - Whether a deadline has passed, or how long is left.
142
+ - A disagreement between sources — that is what the record holds; the earlier reading binds.
143
+ - Whether an override's evidence exists — that is [`cycle/evidence`](evidence.md).
144
+ - Whether the scorecard's stages name these cycles — that is [`cycle/stage`](stage.md).
145
+
146
+ ## How to fix
147
+
148
+ Correct the date. For a deadline read from a call for papers, write it as the call prints it,
149
+ `"YYYY-MM-DD AoE"`, and let paperlint do the arithmetic; for one read from a portal, write the instant
150
+ the portal shows with its zone. Delete a portal reading the venue's preset already carries. A
151
+ deadline the chairs moved by mail is an `override`, with the mail saved in the paper folder as its
152
+ `evidence`.
@@ -0,0 +1,85 @@
1
+ # cycle/stage
2
+
3
+ **Level:** error · **Reads:** the paper's `PIPELINE-STATUS.md` frontmatter (`stages`) and its
4
+ `paperlint.json` (`cycles`) · **Reported on:** the frontmatter of `PIPELINE-STATUS.md`
5
+
6
+ ## What it catches
7
+
8
+ With `cycles` declared in the paper's `paperlint.json`, a `stages` entry in the scorecard's
9
+ frontmatter must say which attempt it belongs to, `cycle: <id>`, and that attempt must exist. Three
10
+ things can be wrong:
11
+
12
+ - a stage names no `cycle`;
13
+ - a stage names a `cycle` no declared cycle has as its `id`;
14
+ - a `submitted` or `camera-ready` stage belongs to a cycle whose `phase` is still `porting` — a PDF
15
+ was sent to the venue while the record says the source is still in the previous venue's template.
16
+
17
+ Without `cycles` (the flat form) the rule is silent and `venue:` stays what it was.
18
+
19
+ ## Why
20
+
21
+ A paper's attempts are recorded once, in `paperlint.json`; the scorecard's stages are the evidence
22
+ — which PDF, how many bytes, when. A stage that names its cycle says which attempt the evidence
23
+ belongs to, and the venue's name is then spelled once, in the preset, instead of as a free string
24
+ retyped per stage (`REALM @ EMNLP 2026`, `AISec 2026 @ ACM CCS`, …). A stage whose cycle does not
25
+ exist is evidence of nothing.
26
+
27
+ The `porting` case is the other half of a declaration: `"phase": "porting"` silences the venue's
28
+ format rules because the source is, by declaration, not yet in the venue's template. A PDF sent
29
+ from such a cycle contradicts that declaration — either the port is done and the phase is stale, or
30
+ the stage is wrong.
31
+
32
+ ## Examples
33
+
34
+ Failing — `paperlint.json` declares one cycle, `open-2027`, and `PIPELINE-STATUS.md` has
35
+
36
+ ```yaml
37
+ ---
38
+ stages:
39
+ - stage: submitted
40
+ date: 2026-07-22
41
+ pdf: versions/a.pdf
42
+ bytes: 1
43
+ ---
44
+ ```
45
+
46
+ ```text
47
+ papers/my-paper/PIPELINE-STATUS.md
48
+ 1:1 error the «submitted» stage (2026-07-22) names no `cycle` — this paper declares `cycles` in
49
+ paperlint.json, so each stage says which attempt it belongs to: `cycle: <id>`, one of
50
+ open-2027 cycle/stage
51
+ ```
52
+
53
+ Passing:
54
+
55
+ ```yaml
56
+ ---
57
+ stages:
58
+ - stage: submitted
59
+ cycle: open-2027
60
+ date: 2026-07-22
61
+ pdf: versions/a.pdf
62
+ bytes: 1
63
+ ---
64
+ ```
65
+
66
+ Also passing: a paper without `cycles`, whatever its stages say.
67
+
68
+ ## Options / preset fields
69
+
70
+ No rule options.
71
+
72
+ ## What it does not check
73
+
74
+ - The PDF, its bytes, or the frozen source — [`paper/stages`](../../rules.md) and `paper/source`
75
+ do.
76
+ - Whether a stage's `date` falls inside its cycle (after `opened`, before the outcome's `date`).
77
+ - Whether a cycle without any `submitted` stage was in fact submitted; that is the portal's record
78
+ (`paperlint submission`), not the scorecard's.
79
+ - The rest of the cycle record — [`cycle/record`](record.md) does.
80
+
81
+ ## How to fix
82
+
83
+ Add `cycle: <id>` to each stage, with the id of the cycle the PDF was sent in. For a stage whose
84
+ cycle is still `porting`: if the port is done, remove `"phase": "porting"` from the cycle
85
+ ([`cycle/port-done`](port-done.md) says when); if no PDF was sent, remove the stage.
@@ -17,6 +17,19 @@ How the body is counted depends on the preset:
17
17
  appendices» excludes an appendix wherever it sits. What follows is not counted. The page the body
18
18
  ends on counts as body when body text stands above the heading there: a body that runs half a
19
19
  page past the limit is over the limit.
20
+ - **With `"appendix_in_body": true` as well** (MSR: «All submissions must not exceed 10 pages for
21
+ the main text, inclusive of all figures, tables, appendices, etc. Two more pages containing only
22
+ references are permitted.»), the appendix is body wherever it sits, and banal's two counts are
23
+ replaced by the text's. The body is every page up to the references — that page too when body
24
+ text stands above the heading — and, when the appendix starts after the references, every page
25
+ from the appendix's to the last. The pages between hold only references, and they are what
26
+ `ref_pages_max` limits. An appendix before the references ends nothing.
27
+
28
+ Why not banal's count for such a venue, measured on IEEEtran `[10pt,conference]` builds
29
+ (2026-10-05): banal types a page whose first heading reads «Appendix» as bibliography, so ten pages
30
+ of main text whose appendix stands before the references measured body 9, references 3 — a correct
31
+ paper failed on its references; and an appendix after the references was counted as reference
32
+ pages, so eleven pages of main text measured body 10, references 2 — an over-long paper passed.
20
33
 
21
34
  ## How it finds where the body ends
22
35
 
@@ -83,13 +96,17 @@ When a structural signal and the text disagree, the count is not made and the ru
83
96
  ## Options / preset fields
84
97
 
85
98
  No rule options. It reads, from the preset, `format.kinds.<kind>.body_pages_max`,
86
- `format.kinds.<kind>.ref_pages_max` and `format.body_ends_at`; from the paper's `paperlint.json`,
99
+ `format.kinds.<kind>.ref_pages_max`, `format.body_ends_at` and `format.appendix_in_body` (which
100
+ the schema accepts only beside `body_ends_at`); from the paper's `paperlint.json`,
87
101
  `kind`. A preset with no kinds, or a kind with no limits, has nothing to check.
88
102
 
89
103
  ## What it does not check
90
104
 
91
105
  - **An appendix before the references, without hyperref**, is counted as body: nothing structural
92
106
  says where it starts. With hyperref it ends the body.
107
+ - **An appendix after the references, without hyperref, under `appendix_in_body`**, is counted
108
+ as reference pages: nothing structural says where it starts. `ref_pages_max` still limits the
109
+ pages after the references, so a long one is reported, as references.
93
110
  - **An appendix whose heading does not read «Appendix…»** (the standard classes print `A` and the
94
111
  title) is reported as `unclear` when hyperref anchors it.
95
112
  - **A bibliography that is not numbered and not linked by hyperref** has no confirmed heading and no
@@ -5,8 +5,9 @@
5
5
  ## What it catches
6
6
 
7
7
  The paper is not set in the class its venue requires. The rule compares the `\documentclass` line
8
- of `paper.tex` with the preset's `template`: the class must be the same, and every option the
9
- template names must be there. The paper may add options of its own.
8
+ of `paper.tex` with the preset's `template`: the class must be the same, every option the
9
+ template names must be there, and no option the preset forbids (`template_forbids`) may be. The
10
+ paper may add other options of its own.
10
11
 
11
12
  A source that keeps one file for several venues and picks the class behind a TeX switch has several
12
13
  `\documentclass` lines. The switch is not evaluated: each line is a candidate, the paper passes when
@@ -49,6 +50,16 @@ The first line reports:
49
50
 
50
51
  > the class option `compsoc` is missing: aidc requires `\documentclass[conference,compsoc]{IEEEtran}`
51
52
 
53
+ Under a preset that forbids options — MSR: «LaTeX users must use \documentclass[10pt,conference]{IEEEtran}
54
+ without including the compsoc or compsocconf options» (https://2027.msrconf.org/track/msr-2027-technical-papers):
55
+
56
+ ```latex
57
+ \documentclass[10pt,conference,compsoc]{IEEEtran} % fails: compsoc is forbidden
58
+ \documentclass[10pt,conference]{IEEEtran} % passes
59
+ ```
60
+
61
+ > the class option `compsoc` is forbidden: MSR requires `\documentclass[10pt,conference]{IEEEtran}` without `compsoc`, `compsocconf`
62
+
52
63
  ## Preset field
53
64
 
54
65
  No rule options. It reads `template`, merged along `extends` (a child's replaces its parent's):
@@ -60,15 +71,24 @@ No rule options. It reads `template`, merged along `extends` (a child's replaces
60
71
  A whole `\documentclass[…]{…}` line, or a bare class name (`"article"`), which requires the class
61
72
  and no option. A value that is neither is reported as a finding that names the preset file.
62
73
 
74
+ ```jsonc
75
+ "template_forbids": ["compsoc", "compsocconf"]
76
+ ```
77
+
78
+ Class options the venue's call forbids, each reported where the class line carries it. Read with the
79
+ `template` of the same file, and only with it: the schema refuses it alone, and a child preset's
80
+ `template` replaces both.
81
+
63
82
  ## What it does not check
64
83
 
65
- - Options a venue forbids (`review`, `anonymous` in a camera-ready): only that what the venue
66
- requires is there.
84
+ - An option a venue forbids that its preset does not list in `template_forbids` (`review` in a
85
+ camera-ready).
67
86
  - The version of the class file (#94).
68
87
  - A `\documentclass` inside a comment: the rule reads the parse tree, so it does not count.
69
88
  - Which branch of a TeX switch builds: with several class lines, one matching candidate is enough.
70
89
 
71
90
  ## How to fix
72
91
 
73
- Set the `\documentclass` line to the one the message quotes, keeping your own extra options. A new
92
+ Set the `\documentclass` line to the one the message quotes, keeping your own extra options
93
+ and dropping a forbidden one. A new
74
94
  paper made with `paperlint new <name> --venue <preset>` already has it.
@@ -57,10 +57,23 @@ No rule options. It reads every shipped preset's `name` and `aliases`, collected
57
57
  A name the paper's own chain also declares (two workshops of one parent conference) is never
58
58
  reported.
59
59
 
60
+ ```jsonc
61
+ "mentions": "with-year"
62
+ ```
63
+
64
+ A venue whose name is also a common acronym counts only with a year beside the name: under
65
+ `paperlint:msr`, «First written for MSR 2027.», «MSR'27» and «MSR~2027» are reported, and
66
+ «Manual Speech Recognition (MSR)», «Microsoft Research (MSR)» or «MSR, in 2027» are not. Any
67
+ whole-word use counted for MSR at first, and an accepted ACSAC paper that defines «MSR» for itself
68
+ drew six warnings. Per preset, not hard-coded: a name that only a venue uses (`AgenticDev`) stays
69
+ reported with or without a year.
70
+
60
71
  ## What it does not check
61
72
 
62
73
  - A venue no shipped preset describes.
63
74
  - A name split by markup (`\textbf{AI}Sec`).
75
+ - A `with-year` venue named without a year («submitted to MSR»): the rule cannot tell it from the
76
+ acronym.
64
77
 
65
78
  ## How to fix
66
79