paperlint 7.0.0 → 7.1.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/README.md +18 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/domain/body-pages.d.ts +29 -0
- package/dist/domain/body-pages.d.ts.map +1 -1
- package/dist/domain/body-pages.js +25 -0
- package/dist/domain/body-pages.js.map +1 -1
- package/dist/paper-includes.d.ts +3 -0
- package/dist/paper-includes.d.ts.map +1 -1
- package/dist/paper-includes.js +17 -2
- package/dist/paper-includes.js.map +1 -1
- package/dist/presets.d.ts +10 -3
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +8 -7
- 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 +19 -7
- package/dist/submission.js.map +1 -1
- package/dist/tex-requirements.d.ts +9 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +7 -3
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +5 -2
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +29 -10
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/venue-rules.d.ts +2 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +55 -7
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +1 -1
- package/docs/e2e.md +14 -12
- package/docs/rules/format/page-limit.md +18 -1
- package/docs/rules/tex/template.md +25 -5
- package/docs/rules/tex/venue-leftover.md +13 -0
- package/docs/rules.md +63 -55
- package/docs/submission.md +5 -1
- package/fixtures/build-e2e/msr/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/msr/expect.json +18 -0
- package/fixtures/build-e2e/msr/paper.tex +49 -0
- package/fixtures/build-e2e/msr/paperlint.json +1 -0
- package/fixtures/build-e2e/msr-appendix-after/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/msr-appendix-after/expect.json +20 -0
- package/fixtures/build-e2e/msr-appendix-after/paper.tex +48 -0
- package/fixtures/build-e2e/msr-appendix-after/paperlint.json +1 -0
- package/package.json +1 -1
- package/presets/ieee-conference.md +1 -1
- package/presets/msr.jsonc +71 -0
- package/presets/msr.md +94 -0
- package/presets/venue-profile.schema.json +25 -0
- package/skills/camera-ready/SKILL.md +8 -1
- package/skills/camera-ready/SKILL.md.spec.ts +7 -0
- package/skills/submission-portal/SKILL.md +2 -2
- package/skills/submission-portal/SKILL.md.spec.ts +1 -1
- package/skills/submit-paper/SKILL.md +9 -1
- package/skills/submit-paper/SKILL.md.spec.ts +8 -0
- package/src/cli.ts +7 -2
- package/src/domain/body-pages.test.ts +144 -0
- package/src/domain/body-pages.ts +61 -1
- package/src/new-paper.test.ts +27 -0
- package/src/paper-includes.test.ts +26 -0
- package/src/paper-includes.ts +20 -2
- package/src/paper-settings-commands.test.ts +1 -1
- package/src/preset-card-rule.test.ts +1 -0
- package/src/presets.test.ts +66 -1
- package/src/presets.ts +23 -6
- package/src/submission.test.ts +42 -3
- package/src/submission.ts +28 -7
- package/src/tex-requirements.ts +21 -4
- package/src/tex-venue-rules.test.ts +161 -0
- package/src/tex-venue-rules.ts +45 -15
- package/src/venue-rules.test.ts +173 -2
- package/src/venue-rules.ts +71 -10
package/presets/msr.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "MSR — venue card"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# MSR (Technical Papers)
|
|
6
|
+
|
|
7
|
+
The card of the venue preset `paperlint:msr` ([`msr.jsonc`](msr.jsonc)): what the venue asks for,
|
|
8
|
+
in prose. The numbers a machine checks live in the preset; the section at the end is generated from
|
|
9
|
+
it. The `submit-paper` skill reads this card for the venue's specifics.
|
|
10
|
+
|
|
11
|
+
**Re-verify the call for papers every year**
|
|
12
|
+
([2027.msrconf.org/track/msr-2027-technical-papers](https://2027.msrconf.org/track/msr-2027-technical-papers)):
|
|
13
|
+
the template changed from ACM to IEEE in 2027 («Note that IEEE format is being used this year,
|
|
14
|
+
whereas last year it was ACM format.»), and limits and dates change between editions.
|
|
15
|
+
|
|
16
|
+
## The venue (2027 edition)
|
|
17
|
+
|
|
18
|
+
- **Venue**: the Technical Papers track of **MSR 2027**, the International Conference on Mining
|
|
19
|
+
Software Repositories.
|
|
20
|
+
- **Portal**: HotCRP, `https://msr2027.hotcrp.com/` («Papers must be submitted through HotCRP»).
|
|
21
|
+
- **Dates** (the call, «AoE (UTC-12h)»): abstract **2026-10-20**, paper **2026-10-23**; early
|
|
22
|
+
reject notification 2026-12-03; author response 2026-12-04 to 2026-12-08; notification
|
|
23
|
+
2027-01-08; camera-ready 2027-01-26. The call's prose names the weekdays «Mon, 20 Oct 2026» and
|
|
24
|
+
«Thurs, 23 Oct 2026», its date table «Tue 20 Oct 2026» and «Fri 23 Oct 2026»; the calendar sides
|
|
25
|
+
with the table (2026-10-20 is a Tuesday).
|
|
26
|
+
- 🔴 **The portal closes earlier than the call says.** Read 2026-10-05 on
|
|
27
|
+
`https://msr2027.hotcrp.com/deadlines`: «Submission deadline: Tuesday Oct 20, 2026, 12 AM EDT»
|
|
28
|
+
(`"sub":1792468800`, 2026-10-20 04:00 UTC) and «Resubmission deadline: Friday Oct 23, 2026, 12 AM
|
|
29
|
+
EDT» («Completed submissions may be updated until this deadline»). Midnight EDT at the start of
|
|
30
|
+
each day is about a day and a half before the end of that day AoE. The portal is what refuses the
|
|
31
|
+
upload: plan for its times, and re-read the page, since chairs change them.
|
|
32
|
+
- **Format**, verbatim from the call: «LaTeX users must use \documentclass[10pt,conference]{IEEEtran}
|
|
33
|
+
without including the compsoc or compsocconf options», «title in 24pt font and full text in 10pt
|
|
34
|
+
type», and «Alterations of spacing, font size, and other changes that deviate from the
|
|
35
|
+
instructions may result in desk rejection without further review.»
|
|
36
|
+
- **Double-anonymous**: «The MSR 2027 Technical Track will employ a double-anonymous review process.
|
|
37
|
+
Thus, no submission may reveal its authors' identities.» The call adds: names omitted, prior work
|
|
38
|
+
cited in the third person, and a preprint must not say it was submitted to MSR. The portal's data
|
|
39
|
+
says `"blind":true`.
|
|
40
|
+
- **Attendance**: «If a submission is accepted, at least one author of the paper is required to
|
|
41
|
+
register for MSR 2027 and present the paper.» The call does not say whether a remote presentation
|
|
42
|
+
is allowed.
|
|
43
|
+
- **New in 2027**, from the call: a submission may be desk rejected for fabricated references
|
|
44
|
+
(«citations to papers, authors, venues, datasets, tools, or other artifacts that do not exist»),
|
|
45
|
+
clear signs of unverified AI-generated text, or hidden instructions aimed at automated tools.
|
|
46
|
+
|
|
47
|
+
## Paper kinds and limits
|
|
48
|
+
|
|
49
|
+
| kind | main-text pages | pages of only references | what the call says |
|
|
50
|
+
| ----------- | --------------: | -----------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `technical` | 10 | 2 | «All submissions must not exceed 10 pages for the main text, inclusive of all figures, tables, appendices, etc. Two more pages containing only references are permitted.» |
|
|
52
|
+
|
|
53
|
+
«Accepted papers will be allowed one extra page for the main text of the camera-ready version.» —
|
|
54
|
+
not the submission's limit, so not in the preset.
|
|
55
|
+
|
|
56
|
+
🔴 **Appendices count in the ten pages**, and a page that holds anything but references is a
|
|
57
|
+
main-text page. banal's count gets both wrong on an IEEEtran build (an appendix before the
|
|
58
|
+
references turns its pages into «bibliography»; one after them into «references»), so the preset
|
|
59
|
+
counts from the text (`format.appendix_in_body`): the main text is every page up to the references —
|
|
60
|
+
that page too when text stands above the heading — and every page from an appendix that follows the
|
|
61
|
+
references; the pages between hold only references. **Load hyperref**: an appendix after the
|
|
62
|
+
references is found by hyperref's anchor, and without it is counted as reference pages. See
|
|
63
|
+
`docs/rules/format/page-limit.md`.
|
|
64
|
+
|
|
65
|
+
## What else the preset encodes
|
|
66
|
+
|
|
67
|
+
- **The IEEE template**, through `extends: paperlint:ieee-conference` — letter paper, two columns,
|
|
68
|
+
Times at 10 pt, IEEEtran's TeX packages ([`ieee-conference.md`](ieee-conference.md)) — with
|
|
69
|
+
`10pt` and `conference` required in the class line and `compsoc` and `compsocconf` forbidden
|
|
70
|
+
(`tex/template`, `template_forbids`).
|
|
71
|
+
- **Double-blind review** (`blind`), so `anonymity/identity` checks the PDF against the paper's
|
|
72
|
+
declared `identity`.
|
|
73
|
+
- **Where submissions go** (`portal`): `paperlint submission` reads and updates a paper's
|
|
74
|
+
submission through HotCRP's API on that site.
|
|
75
|
+
- **Names**: `MSR`, what `tex/venue-leftover` looks for in another venue's paper — only with a year
|
|
76
|
+
beside it (`mentions: with-year`: «MSR 2027», «MSR'27»), since «MSR» is also Microsoft Research
|
|
77
|
+
and an acronym papers define for themselves.
|
|
78
|
+
- **Not encoded**: the capitalization of the title and headings (`tex/heading-case` stays off: the
|
|
79
|
+
IEEE guidelines' rule has not been read and quoted here), and the register of the body (no
|
|
80
|
+
accepted MSR paper in the IEEE template exists yet to anchor `tex/contrast-frames` and its
|
|
81
|
+
siblings).
|
|
82
|
+
|
|
83
|
+
## What the preset resolves to
|
|
84
|
+
|
|
85
|
+
<!-- paperlint:preset-rules -->
|
|
86
|
+
<!-- prettier-ignore-start -->
|
|
87
|
+
<!-- Generated from msr.jsonc by `npx eslint --fix` (rule preset/card-rules). Edit the preset, not this section. -->
|
|
88
|
+
|
|
89
|
+
**`paperlint:msr`** — venue preset for MSR · call for papers: <https://2027.msrconf.org/track/msr-2027-technical-papers> · extends `paperlint:ieee-conference`
|
|
90
|
+
|
|
91
|
+
No preset of this chain sets a rule: under it, every rule keeps paperlint's default.
|
|
92
|
+
|
|
93
|
+
<!-- prettier-ignore-end -->
|
|
94
|
+
<!-- /paperlint:preset-rules -->
|
|
@@ -34,6 +34,16 @@
|
|
|
34
34
|
"type": "string",
|
|
35
35
|
"minLength": 1
|
|
36
36
|
},
|
|
37
|
+
"template_forbids": {
|
|
38
|
+
"description": "Class options the venue's call forbids in the \\documentclass line (`[\"compsoc\", \"compsocconf\"]`: MSR's «without including the compsoc or compsocconf options»). `tex/template` reports each one a paper's class line carries. Read with the `template` of the same file, and only with it: a child's `template` replaces both.",
|
|
39
|
+
"type": "array",
|
|
40
|
+
"minItems": 1,
|
|
41
|
+
"uniqueItems": true,
|
|
42
|
+
"items": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"minLength": 1
|
|
45
|
+
}
|
|
46
|
+
},
|
|
37
47
|
"blind": {
|
|
38
48
|
"description": "The venue reviews double-blind: its call says submissions must be anonymized. Read by `anonymity/identity`, which then requires the paper's `identity` and finds none of it in the PDF. Set only where the venue says so, with the quote. A child's value replaces its parent's.",
|
|
39
49
|
"type": "boolean"
|
|
@@ -47,6 +57,11 @@
|
|
|
47
57
|
"minLength": 1
|
|
48
58
|
}
|
|
49
59
|
},
|
|
60
|
+
"mentions": {
|
|
61
|
+
"description": "When the venue's `name` and `aliases` count as naming it in a paper's text (`tex/venue-leftover`). Absent: any whole-word use. `with-year`: only with a year beside the name — `MSR 2027`, `MSR'27`, `MSR~2027` — for a name that is also a common acronym (MSR: Microsoft Research, or an acronym a paper defines), where a bare use names something else far more often than the venue. A child's value replaces its parent's.",
|
|
62
|
+
"type": "string",
|
|
63
|
+
"enum": ["with-year"]
|
|
64
|
+
},
|
|
50
65
|
"portal": {
|
|
51
66
|
"description": "Where the venue takes submissions, read by `paperlint submission`: the portal's kind (an adapter exists for `hotcrp`) and the site's base URL, the one its pages and `/api/` hang off. A paper says which submission is its own with `submission: { id }` in its paperlint.json. A child's value replaces its parent's.",
|
|
52
67
|
"type": "object",
|
|
@@ -86,6 +101,9 @@
|
|
|
86
101
|
"description": "What the venue's call for papers sets. Every key is optional; a child preset's key replaces its parent's, and a child's kind replaces that kind.",
|
|
87
102
|
"type": "object",
|
|
88
103
|
"additionalProperties": false,
|
|
104
|
+
"dependencies": {
|
|
105
|
+
"appendix_in_body": ["body_ends_at"]
|
|
106
|
+
},
|
|
89
107
|
"properties": {
|
|
90
108
|
"page_size": {
|
|
91
109
|
"type": "string",
|
|
@@ -140,6 +158,10 @@
|
|
|
140
158
|
"type": "string",
|
|
141
159
|
"enum": ["references"]
|
|
142
160
|
},
|
|
161
|
+
"appendix_in_body": {
|
|
162
|
+
"description": "With `body_ends_at: references`: the venue counts its appendices in the body limit and allows reference pages that hold only references (MSR: «10 pages for the main text, inclusive of all figures, tables, appendices, etc. Two more pages containing only references are permitted.»). `format/page-limit` then counts as body the pages up to the references and, when the appendix starts after them, every page from the appendix (found by hyperref's anchor) to the last; the pages between are the reference pages, judged against `ref_pages_max`. An appendix before the references is body and ends nothing.",
|
|
163
|
+
"type": "boolean"
|
|
164
|
+
},
|
|
143
165
|
"page_limits_not_gated": {
|
|
144
166
|
"type": "object"
|
|
145
167
|
},
|
|
@@ -347,6 +369,9 @@
|
|
|
347
369
|
}
|
|
348
370
|
},
|
|
349
371
|
"required": ["type"],
|
|
372
|
+
"dependencies": {
|
|
373
|
+
"template_forbids": ["template"]
|
|
374
|
+
},
|
|
350
375
|
"allOf": [
|
|
351
376
|
{
|
|
352
377
|
"if": {
|
|
@@ -4,7 +4,7 @@ description: On acceptance, turn the anonymized-for-review paper into the de-ano
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:1c9ad89e55c3e89a compiled from skills/camera-ready/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# camera-ready — de-anonymize an accepted paper into the final proceedings version
|
|
10
10
|
|
|
@@ -94,6 +94,13 @@ and going over is a hard reject at the proceedings stage. Recompile
|
|
|
94
94
|
If tight, ACM allows references to spill into the reference-only extra pages.
|
|
95
95
|
|
|
96
96
|
## 5. Ship
|
|
97
|
+
- **Before the final PDF goes up, hand the last check to the author, in plain words.** Tell them that
|
|
98
|
+
paperlint passing is not the venue or the publisher accepting the format, and ask them to open the
|
|
99
|
+
exact PDF they are about to upload and check it against the venue's call for papers and its
|
|
100
|
+
camera-ready instructions themselves — the page count and what counts toward the limit (the
|
|
101
|
+
camera-ready's extra page, if any), the template and its class options, the author block and
|
|
102
|
+
acknowledgements now de-anonymized, and that every figure, table and reference renders. Upload once
|
|
103
|
+
they say they have looked.
|
|
97
104
|
- Rebuild the final PDF, upload to the camera-ready system (often the same HotCRP, or an ACM eRights /
|
|
98
105
|
TAPS pipeline — follow the copyright email).
|
|
99
106
|
- Complete the **ACM/IEEE eRights** copyright form → paste the returned copyright block + DOI into the
|
|
@@ -109,6 +109,13 @@ and going over is a hard reject at the proceedings stage. Recompile
|
|
|
109
109
|
If tight, ACM allows references to spill into the reference-only extra pages.
|
|
110
110
|
|
|
111
111
|
## 5. Ship
|
|
112
|
+
- **Before the final PDF goes up, hand the last check to the author, in plain words.** Tell them that
|
|
113
|
+
paperlint passing is not the venue or the publisher accepting the format, and ask them to open the
|
|
114
|
+
exact PDF they are about to upload and check it against the venue's call for papers and its
|
|
115
|
+
camera-ready instructions themselves — the page count and what counts toward the limit (the
|
|
116
|
+
camera-ready's extra page, if any), the template and its class options, the author block and
|
|
117
|
+
acknowledgements now de-anonymized, and that every figure, table and reference renders. Upload once
|
|
118
|
+
they say they have looked.
|
|
112
119
|
- Rebuild the final PDF, upload to the camera-ready system (often the same HotCRP, or an ACM eRights /
|
|
113
120
|
TAPS pipeline — follow the copyright email).
|
|
114
121
|
- Complete the **ACM/IEEE eRights** copyright form → paste the returned copyright block + DOI into the
|
|
@@ -4,7 +4,7 @@ description: "Read or change a paper's submission on the venue's HotCRP portal v
|
|
|
4
4
|
allowed-tools: [Read, Grep, Glob, Bash]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:4b3a8c0b51ebe206 compiled from skills/submission-portal/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# submission-portal — read and update the submission on the portal, then prove it landed
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ supported; for any other portal the command says so and the upload is done by ha
|
|
|
14
14
|
## What must be in place
|
|
15
15
|
|
|
16
16
|
- **The venue preset declares the portal**: `"portal": { "kind": "hotcrp", "url": "<the site>" }`
|
|
17
|
-
(`paperlint:aidc`
|
|
17
|
+
(`paperlint:aidc` and `paperlint:msr` have it). For your own venue preset, add it there.
|
|
18
18
|
- **The paper's `paperlint.json` names its submission**: `"submission": { "id": <number> }` — the
|
|
19
19
|
number HotCRP shows for the paper.
|
|
20
20
|
- **A token in `HOTCRP_TOKEN`**, created by the author on the HotCRP site under Account settings →
|
|
@@ -15,7 +15,7 @@ supported; for any other portal the command says so and the upload is done by ha
|
|
|
15
15
|
## What must be in place
|
|
16
16
|
|
|
17
17
|
- **The venue preset declares the portal**: \`"portal": { "kind": "hotcrp", "url": "<the site>" }\`
|
|
18
|
-
(\`paperlint:aidc\`
|
|
18
|
+
(\`paperlint:aidc\` and \`paperlint:msr\` have it). For your own venue preset, add it there.
|
|
19
19
|
- **The paper's \`paperlint.json\` names its submission**: \`"submission": { "id": <number> }\` — the
|
|
20
20
|
number HotCRP shows for the paper.
|
|
21
21
|
- **A token in \`HOTCRP_TOKEN\`**, created by the author on the HotCRP site under Account settings →
|
|
@@ -4,7 +4,7 @@ description: End-to-end playbook for submitting a peer-reviewed paper to a doubl
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Skill]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:81d5777962c2864a compiled from skills/submit-paper/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# submit-paper — get a reviewed paper from "done" to "ready for review"
|
|
10
10
|
|
|
@@ -20,6 +20,7 @@ paper against:
|
|
|
20
20
|
- `presets/agenticdev.md` — AgenticDev @ ASE (workshop; ASE workshop proceedings).
|
|
21
21
|
- `presets/aisec.md` — AISec @ ACM CCS (security workshop; ACM DL; harder bar).
|
|
22
22
|
- `presets/aidc.md` — AIDC @ IEEE ACSAC (workshop; IEEE template, LLM Usage Statement).
|
|
23
|
+
- `presets/msr.md` — MSR Technical Papers (conference; IEEE template from 2027, appendices count in the page limit).
|
|
23
24
|
- `presets/realm.md` — REALM @ EMNLP (ACL family, OpenReview).
|
|
24
25
|
|
|
25
26
|
**The card is prose; the machine-checked format is the PRESET beside it.** A paper declares its venue
|
|
@@ -110,6 +111,13 @@ Many workshop HotCRPs have **no supplementary-upload field** — check first; if
|
|
|
110
111
|
An independent researcher with no ties usually checks **none** — that's honest.
|
|
111
112
|
|
|
112
113
|
## 5. Submit
|
|
114
|
+
- **Before the PDF goes up — and before every Replace — hand the last check to the author, in plain
|
|
115
|
+
words.** Tell them that paperlint passing is not the venue accepting the format: the preset is
|
|
116
|
+
transcribed from a call that can change, and any check can miss something. Ask them to open the
|
|
117
|
+
exact PDF they are about to upload and check it against the venue's call for papers themselves —
|
|
118
|
+
the page count and what counts toward the limit (appendices, references), the template and its
|
|
119
|
+
class options, anonymity, and that every figure, table and reference renders. Upload once they say
|
|
120
|
+
they have looked. (`paperlint submission update` prints the same request before it sends.)
|
|
113
121
|
- **Save and submit**, then **mark it "ready for review."** A saved draft alone is NOT evaluated — this
|
|
114
122
|
is the step people miss.
|
|
115
123
|
- You can edit until the deadline: use **Replace** on the Submission field to swap the PDF for a late
|
|
@@ -36,6 +36,7 @@ paper against:
|
|
|
36
36
|
- \`presets/agenticdev.md\` — AgenticDev @ ASE (workshop; ASE workshop proceedings).
|
|
37
37
|
- \`presets/aisec.md\` — AISec @ ACM CCS (security workshop; ACM DL; harder bar).
|
|
38
38
|
- \`presets/aidc.md\` — AIDC @ IEEE ACSAC (workshop; IEEE template, LLM Usage Statement).
|
|
39
|
+
- \`presets/msr.md\` — MSR Technical Papers (conference; IEEE template from 2027, appendices count in the page limit).
|
|
39
40
|
- \`presets/realm.md\` — REALM @ EMNLP (ACL family, OpenReview).
|
|
40
41
|
|
|
41
42
|
**The card is prose; the machine-checked format is the PRESET beside it.** A paper declares its venue
|
|
@@ -126,6 +127,13 @@ Many workshop HotCRPs have **no supplementary-upload field** — check first; if
|
|
|
126
127
|
An independent researcher with no ties usually checks **none** — that's honest.
|
|
127
128
|
|
|
128
129
|
## 5. Submit
|
|
130
|
+
- **Before the PDF goes up — and before every Replace — hand the last check to the author, in plain
|
|
131
|
+
words.** Tell them that paperlint passing is not the venue accepting the format: the preset is
|
|
132
|
+
transcribed from a call that can change, and any check can miss something. Ask them to open the
|
|
133
|
+
exact PDF they are about to upload and check it against the venue's call for papers themselves —
|
|
134
|
+
the page count and what counts toward the limit (appendices, references), the template and its
|
|
135
|
+
class options, anonymity, and that every figure, table and reference renders. Upload once they say
|
|
136
|
+
they have looked. (\`paperlint submission update\` prints the same request before it sends.)
|
|
129
137
|
- **Save and submit**, then **mark it "ready for review."** A saved draft alone is NOT evaluated — this
|
|
130
138
|
is the step people miss.
|
|
131
139
|
- You can edit until the deadline: use **Replace** on the Submission field to swap the PDF for a late
|
package/src/cli.ts
CHANGED
|
@@ -42,7 +42,12 @@ import {
|
|
|
42
42
|
asEslintResults,
|
|
43
43
|
} from "./structure.ts";
|
|
44
44
|
import { buildPapers, papersIn, anyFailed, remedyFor, MAIN } from "./build.ts";
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
FRAGMENT_FILES,
|
|
47
|
+
includeBlocks,
|
|
48
|
+
paperBodies,
|
|
49
|
+
unreadLines,
|
|
50
|
+
} from "./paper-includes.ts";
|
|
46
51
|
import { prepareEngine } from "./build-engine.ts";
|
|
47
52
|
import { cacheRoot, cachedTree, runToolchain } from "./toolchain.ts";
|
|
48
53
|
import { banalInstaller, parseBanalSettings } from "./adapters/banal/index.ts";
|
|
@@ -462,7 +467,7 @@ export function buildConfig(
|
|
|
462
467
|
const fragments: readonly ConfigBlock[] = texLanguage
|
|
463
468
|
? [
|
|
464
469
|
{
|
|
465
|
-
files: [
|
|
470
|
+
files: [...FRAGMENT_FILES],
|
|
466
471
|
ignores: ["**/paper.tex"],
|
|
467
472
|
plugins: {
|
|
468
473
|
tex: {
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
bodyEnd,
|
|
4
4
|
isAppendixHeading,
|
|
5
5
|
isReferencesHeading,
|
|
6
|
+
pageSplit,
|
|
7
|
+
type Anchors,
|
|
6
8
|
} from "./body-pages.ts";
|
|
7
9
|
|
|
8
10
|
describe("isReferencesHeading", () => {
|
|
@@ -298,3 +300,145 @@ describe("bodyEnd: where the appendix is not taken, or not found", () => {
|
|
|
298
300
|
});
|
|
299
301
|
});
|
|
300
302
|
});
|
|
303
|
+
|
|
304
|
+
describe("pageSplit: the appendix counts as body, the reference pages are the ones holding only references", () => {
|
|
305
|
+
it("no appendix: the body up to the references, the rest references", () => {
|
|
306
|
+
expect(
|
|
307
|
+
pageSplit(["body", "end\nReferences\n[1] A.", "[2] B.", "[3] C."], {
|
|
308
|
+
bib: 2,
|
|
309
|
+
appendix: null,
|
|
310
|
+
}),
|
|
311
|
+
).toEqual({
|
|
312
|
+
kind: "found",
|
|
313
|
+
page: 2,
|
|
314
|
+
bodyPages: 2,
|
|
315
|
+
refPages: 2,
|
|
316
|
+
appendixFrom: null,
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("🔴 an appendix before the references is body: it does not end the body", () => {
|
|
321
|
+
expect(
|
|
322
|
+
pageSplit(
|
|
323
|
+
[
|
|
324
|
+
"body",
|
|
325
|
+
"Appendix A.\nDetails",
|
|
326
|
+
"more details\nReferences\n[1] A.",
|
|
327
|
+
"[2] B.",
|
|
328
|
+
],
|
|
329
|
+
{ bib: 3, appendix: 2 },
|
|
330
|
+
),
|
|
331
|
+
).toEqual({
|
|
332
|
+
kind: "found",
|
|
333
|
+
page: 3,
|
|
334
|
+
bodyPages: 3,
|
|
335
|
+
refPages: 1,
|
|
336
|
+
appendixFrom: null,
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("🔴 an appendix after the references is body: the pages from it to the end", () => {
|
|
341
|
+
expect(
|
|
342
|
+
pageSplit(
|
|
343
|
+
[
|
|
344
|
+
"body",
|
|
345
|
+
"end\nReferences\n[1] A.",
|
|
346
|
+
"[2] B.",
|
|
347
|
+
"[3] C.\nAppendix A.\nx",
|
|
348
|
+
"y",
|
|
349
|
+
],
|
|
350
|
+
{ bib: 2, appendix: 4 },
|
|
351
|
+
),
|
|
352
|
+
).toEqual({
|
|
353
|
+
kind: "found",
|
|
354
|
+
page: 2,
|
|
355
|
+
bodyPages: 4,
|
|
356
|
+
refPages: 1,
|
|
357
|
+
appendixFrom: 4,
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe("pageSplit: the appendix and the references on one page", () => {
|
|
363
|
+
it("an appendix after the references on their own page: that page is counted once", () => {
|
|
364
|
+
expect(
|
|
365
|
+
pageSplit(["body", "end\nReferences\n[1] A.\nAppendix A.\nx"], {
|
|
366
|
+
bib: 2,
|
|
367
|
+
appendix: 2,
|
|
368
|
+
}),
|
|
369
|
+
).toEqual({
|
|
370
|
+
kind: "found",
|
|
371
|
+
page: 2,
|
|
372
|
+
bodyPages: 2,
|
|
373
|
+
refPages: 0,
|
|
374
|
+
appendixFrom: 2,
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("an appendix above the references on their page is body before them, like any text", () => {
|
|
379
|
+
expect(
|
|
380
|
+
pageSplit(["body", "end\nAppendix A.\nx\nReferences\n[1] A.", "[2] B."], {
|
|
381
|
+
bib: 2,
|
|
382
|
+
appendix: 2,
|
|
383
|
+
}),
|
|
384
|
+
).toEqual({
|
|
385
|
+
kind: "found",
|
|
386
|
+
page: 2,
|
|
387
|
+
bodyPages: 2,
|
|
388
|
+
refPages: 1,
|
|
389
|
+
appendixFrom: null,
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("the references at the top of their page: that page holds only references", () => {
|
|
394
|
+
expect(
|
|
395
|
+
pageSplit(["body", "References\n[1] A.", "Appendix A.\nx"], {
|
|
396
|
+
bib: 2,
|
|
397
|
+
appendix: 3,
|
|
398
|
+
}),
|
|
399
|
+
).toEqual({
|
|
400
|
+
kind: "found",
|
|
401
|
+
page: 2,
|
|
402
|
+
bodyPages: 2,
|
|
403
|
+
refPages: 1,
|
|
404
|
+
appendixFrom: 3,
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe("pageSplit: what is said rather than counted, and what is not looked for", () => {
|
|
410
|
+
it.each<[string, readonly string[], Anchors, string]>([
|
|
411
|
+
[
|
|
412
|
+
"no references heading",
|
|
413
|
+
["body", "Appendix\nx"],
|
|
414
|
+
{ bib: null, appendix: 2 },
|
|
415
|
+
"missing",
|
|
416
|
+
],
|
|
417
|
+
[
|
|
418
|
+
"an appendix anchor with no heading",
|
|
419
|
+
["body", "References\n[1] A.", "x"],
|
|
420
|
+
{ bib: 2, appendix: 3 },
|
|
421
|
+
"disagree",
|
|
422
|
+
],
|
|
423
|
+
[
|
|
424
|
+
"a references anchor with no heading",
|
|
425
|
+
["body", "x"],
|
|
426
|
+
{ bib: 2, appendix: null },
|
|
427
|
+
"disagree",
|
|
428
|
+
],
|
|
429
|
+
])("%s: said, not counted", (_, pages, anchors, kind) => {
|
|
430
|
+
expect(pageSplit(pages, anchors).kind).toBe(kind);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("without anchors the references heading is found in the text, and no appendix is looked for", () => {
|
|
434
|
+
expect(
|
|
435
|
+
pageSplit(["body", "end\nReferences\n[1] A.", "Appendix A.\nx"]),
|
|
436
|
+
).toEqual({
|
|
437
|
+
kind: "found",
|
|
438
|
+
page: 2,
|
|
439
|
+
bodyPages: 2,
|
|
440
|
+
refPages: 1,
|
|
441
|
+
appendixFrom: null,
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
});
|
package/src/domain/body-pages.ts
CHANGED
|
@@ -186,8 +186,11 @@ function appendixAt(
|
|
|
186
186
|
};
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/** A body end that was found. */
|
|
190
|
+
type Found = Extract<BodyEnd, { readonly kind: "found" }>;
|
|
191
|
+
|
|
189
192
|
/** The body's end at a heading: the heading's page counts when body text stands above it. */
|
|
190
|
-
function endAt(pages: readonly PageReading[], at: At):
|
|
193
|
+
function endAt(pages: readonly PageReading[], at: At): Found {
|
|
191
194
|
const running = runningLines(pages);
|
|
192
195
|
const body = at.above.some((l) => /\p{L}/u.test(l) && !running.has(shape(l)));
|
|
193
196
|
return {
|
|
@@ -221,3 +224,60 @@ export function bodyEnd(
|
|
|
221
224
|
.toSorted((a, b) => a.page - b.page || a.line - b.line)[0];
|
|
222
225
|
return first === undefined ? { kind: "missing" } : endAt(pages, first);
|
|
223
226
|
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The pages of a paper split as a venue counts them when its body limit includes the appendices
|
|
230
|
+
* and only the pages holding nothing but references are reference pages (MSR: «10 pages for the
|
|
231
|
+
* main text, inclusive of all figures, tables, appendices, etc. Two more pages containing only
|
|
232
|
+
* references are permitted.»):
|
|
233
|
+
*
|
|
234
|
+
* body the pages up to the references — their page too when body text stands above the
|
|
235
|
+
* heading — and, when the appendix starts after the references, every page from the
|
|
236
|
+
* appendix's to the last, which holds appendix text
|
|
237
|
+
* references the pages between, which hold only references
|
|
238
|
+
*
|
|
239
|
+
* An appendix before the references is body like any other text, and ends nothing. The appendix is
|
|
240
|
+
* found by hyperref's anchor only (`appendixAt`): without hyperref, an appendix after the references
|
|
241
|
+
* is not told from them and is counted with them. Pure.
|
|
242
|
+
*/
|
|
243
|
+
export type PageSplit =
|
|
244
|
+
| {
|
|
245
|
+
readonly kind: "found";
|
|
246
|
+
/** 1-based page holding the references heading. */
|
|
247
|
+
readonly page: number;
|
|
248
|
+
readonly bodyPages: number;
|
|
249
|
+
readonly refPages: number;
|
|
250
|
+
/** 1-based page the appendix starts on, when it follows the references; else null. */
|
|
251
|
+
readonly appendixFrom: number | null;
|
|
252
|
+
}
|
|
253
|
+
| { readonly kind: "missing" }
|
|
254
|
+
| Extract<BodyEnd, { readonly kind: "disagree" }>;
|
|
255
|
+
|
|
256
|
+
/** Whether heading `a` stands after heading `b` in the document. */
|
|
257
|
+
const after = (a: At, b: At): boolean =>
|
|
258
|
+
a.page > b.page || (a.page === b.page && a.line > b.line);
|
|
259
|
+
|
|
260
|
+
export function pageSplit(
|
|
261
|
+
texts: readonly string[],
|
|
262
|
+
anchors: Anchors = { bib: null, appendix: null },
|
|
263
|
+
): PageSplit {
|
|
264
|
+
const pages = texts.map(readPage);
|
|
265
|
+
const refs = referencesAt(pages, anchors.bib);
|
|
266
|
+
const appendix = appendixAt(pages, anchors.appendix);
|
|
267
|
+
if (refs.kind === "disagree") return refs;
|
|
268
|
+
if (appendix.kind === "disagree") return appendix;
|
|
269
|
+
if (refs.kind === "missing") return { kind: "missing" };
|
|
270
|
+
const upTo = endAt(pages, refs).bodyPages;
|
|
271
|
+
const tail =
|
|
272
|
+
appendix.kind === "at" && after(appendix, refs) ? appendix.page : null;
|
|
273
|
+
// The pages from the appendix to the end, less the ones already counted up to the references.
|
|
274
|
+
const appendixPages = tail === null ? 0 : pages.length - Math.max(tail, upTo);
|
|
275
|
+
const bodyPages = upTo + appendixPages;
|
|
276
|
+
return {
|
|
277
|
+
kind: "found",
|
|
278
|
+
page: refs.page + 1,
|
|
279
|
+
bodyPages,
|
|
280
|
+
refPages: pages.length - bodyPages,
|
|
281
|
+
appendixFrom: tail === null ? null : tail + 1,
|
|
282
|
+
};
|
|
283
|
+
}
|
package/src/new-paper.test.ts
CHANGED
|
@@ -234,6 +234,33 @@ describe("paperlint new --venue / --kind", () => {
|
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
+
describe("paperlint new --venue msr", () => {
|
|
238
|
+
it("`new --venue msr` writes MSR's class line, and once the paper declares who wrote it, lint is clean", async () => {
|
|
239
|
+
const r = await newIn(["--venue", "msr", "--kind", "technical"]);
|
|
240
|
+
const tex = readFileSync(join(r.dir, "paper.tex"), "utf8");
|
|
241
|
+
expect(tex).toContain("\\documentclass[10pt,conference]{IEEEtran}\n");
|
|
242
|
+
expect(r.out).not.toMatch(/tex\/template|format\/page-limit|pdf\/profile/);
|
|
243
|
+
// MSR reviews double-anonymously: the one thing the stub lacks is the authors' identity.
|
|
244
|
+
expect(r.out).toMatch(/anonymity\/identity/);
|
|
245
|
+
expect(r.code).toBe(1);
|
|
246
|
+
writeFileSync(
|
|
247
|
+
join(r.dir, "paperlint.json"),
|
|
248
|
+
JSON.stringify({ ...r.settings(), identity: ["Ada Example"] }),
|
|
249
|
+
);
|
|
250
|
+
const out: string[] = [];
|
|
251
|
+
const code = await run(["lint", r.dir], {
|
|
252
|
+
cwd: r.dir,
|
|
253
|
+
log: (s: string) => out.push(s),
|
|
254
|
+
err: () => {},
|
|
255
|
+
});
|
|
256
|
+
// Guards: what remains is the reminder that the PDF is not built yet — a warning, not a finding
|
|
257
|
+
// about the paper.
|
|
258
|
+
expect(out.join("\n")).toMatch(/\(0 errors, 1 warning\)/);
|
|
259
|
+
expect(out.join("\n")).toMatch(/pdf\/measured/);
|
|
260
|
+
expect(code).toBe(0);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
237
264
|
describe("paperlint new --venue — a venue it cannot use", () => {
|
|
238
265
|
it("an unknown venue exits 2, lists the shipped presets, and creates nothing", async () => {
|
|
239
266
|
const r = await newIn(["--venue", "icse"]);
|
|
@@ -102,6 +102,32 @@ describe("lint reads the files paper.tex includes from its body", () => {
|
|
|
102
102
|
]);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
it("🔴 an included file that is not `.tex` — a `.bbl` — is not handed to ESLint, which parsed it as JavaScript (#153)", async () => {
|
|
106
|
+
const { byFile, err } = await lint(
|
|
107
|
+
project({
|
|
108
|
+
"papers/a/paper.tex": doc(
|
|
109
|
+
"{article}",
|
|
110
|
+
"",
|
|
111
|
+
"Text, as Figure~\\ref{f1} shows.\n\\input{paper.bbl}",
|
|
112
|
+
),
|
|
113
|
+
// What bibtex writes: a comment first, which ESLint's JavaScript parser stops at.
|
|
114
|
+
"papers/a/paper.bbl":
|
|
115
|
+
"% Generated by IEEEtran.bst\n\\begin{thebibliography}{1}\n" +
|
|
116
|
+
"\\bibitem{a} A. Author, ``A result with p < .05,'' 2024.\n" +
|
|
117
|
+
"\\end{thebibliography}\n",
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
// Guards: the `.bbl` was the one file of the run with a parse error («Unexpected token %»).
|
|
121
|
+
// It is bibtex's output, not prose the author edits: no fragment rule reports in it, and
|
|
122
|
+
// paper.tex — which reads it, spliced, as TeX typesets it — draws no finding from its `.05`.
|
|
123
|
+
expect(byFile).toEqual({
|
|
124
|
+
"papers/a/PIPELINE-STATUS.md": [],
|
|
125
|
+
"papers/a/paper.tex": [],
|
|
126
|
+
});
|
|
127
|
+
// Read, not missing: no «lint did not read» note names it.
|
|
128
|
+
expect(err).not.toMatch(/paper\.bbl/);
|
|
129
|
+
});
|
|
130
|
+
|
|
105
131
|
it("--fix rewrites the included file, to the paper's majority form", async () => {
|
|
106
132
|
const root = project({});
|
|
107
133
|
const before = readFileSync(join(root, "papers/a/paper.tex"), "utf8");
|
package/src/paper-includes.ts
CHANGED
|
@@ -14,18 +14,35 @@
|
|
|
14
14
|
* `paper.tex`, so only the files named here are ever enumerated;
|
|
15
15
|
* 2. `settings.paperlint.paper` for `paper.tex` and its body files: the main file and the whole set,
|
|
16
16
|
* for the rules whose verdict is the paper's (eslint-rules/paper-context.ts).
|
|
17
|
+
*
|
|
18
|
+
* ── ONLY `.tex` FILES ARE HANDED TO ESLint (#153) ────────────────────────────────
|
|
19
|
+
* An include may resolve to a file of another extension: `\input{paper.bbl}` in a paper that ships
|
|
20
|
+
* bibtex's output instead of a `.bib`, a plot exported as `.pgf`. Handed to ESLint, such a file got
|
|
21
|
+
* no language — the fragment block claims `.tex` only — and the JavaScript parser stopped at its
|
|
22
|
+
* first `%`. It is not given the LaTeX language either: the fragment rules judge and `--fix` the
|
|
23
|
+
* prose an author writes in that file, and a file of another extension is in practice a tool's
|
|
24
|
+
* output, whose words are someone else's (the titles of cited works) and whose fix the next run of
|
|
25
|
+
* the tool overwrites. TeX still reads it, and so does `readPaper`: the rules over the whole paper
|
|
26
|
+
* see it spliced into `paper.tex`, where TeX typesets it.
|
|
17
27
|
*/
|
|
18
|
-
import { join, relative } from "node:path";
|
|
28
|
+
import { extname, join, relative } from "node:path";
|
|
19
29
|
import { PAPERLINT_SETTINGS } from "#eslint-rules/paper-context";
|
|
20
30
|
import { callerPath } from "./caller-path.ts";
|
|
21
31
|
import type { AbsolutePath } from "./domain/paths.ts";
|
|
22
32
|
import type { ConfigBlock } from "./rules-config.ts";
|
|
23
33
|
import { bodyFiles, type PaperDeps, type Unread } from "./tex-paper.ts";
|
|
24
34
|
|
|
35
|
+
/** The files the fragment block (`buildConfig`) claims: the only body files ESLint is handed. */
|
|
36
|
+
export const FRAGMENT_FILES: readonly string[] = ["**/*.tex"];
|
|
37
|
+
|
|
38
|
+
/** Whether a body file is one the fragment block claims — the extension its glob names. */
|
|
39
|
+
const isFragment = (file: string): boolean => extname(file) === ".tex";
|
|
40
|
+
|
|
25
41
|
/** One paper's main file and body files, and the includes that resolved nowhere. */
|
|
26
42
|
export interface PaperBody {
|
|
27
43
|
readonly dir: string;
|
|
28
44
|
readonly main: AbsolutePath;
|
|
45
|
+
/** The body files ESLint lints, each as a fragment: the `.tex` ones. */
|
|
29
46
|
readonly files: readonly AbsolutePath[];
|
|
30
47
|
readonly missing: readonly Unread[];
|
|
31
48
|
}
|
|
@@ -40,7 +57,8 @@ export function paperBodies(
|
|
|
40
57
|
const bytes = deps.files.readBytes(main);
|
|
41
58
|
if (bytes === null) return [];
|
|
42
59
|
const body = bodyFiles(main, new TextDecoder().decode(bytes), deps);
|
|
43
|
-
|
|
60
|
+
const files = body.files.filter(isFragment);
|
|
61
|
+
return [{ dir, main, files, missing: body.missing }];
|
|
44
62
|
});
|
|
45
63
|
}
|
|
46
64
|
|
|
@@ -299,7 +299,7 @@ describe("paperlint lint — the paper over its preset, and the project's own pr
|
|
|
299
299
|
const r = await lint(root);
|
|
300
300
|
expect(r.code).toBe(1);
|
|
301
301
|
expect(r.out).toMatch(
|
|
302
|
-
/acm-sigconf, agenticdev, aidc, aisec, ieee-conference, realm/,
|
|
302
|
+
/acm-sigconf, agenticdev, aidc, aisec, ieee-conference, msr, realm/,
|
|
303
303
|
);
|
|
304
304
|
});
|
|
305
305
|
});
|