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,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
+ };
package/eslint.config.mjs CHANGED
@@ -413,6 +413,40 @@ export const IO_GLOBALS = {
413
413
  },
414
414
  };
415
415
 
416
+ /**
417
+ * `paperlint lint` NEVER READS THE CLOCK. Its findings are a function of the files and the installed
418
+ * package: the same checkout lints the same tomorrow, and a deadline's distance belongs to a status
419
+ * view or a calendar, not to a gate (a lint that turns red on an untouched tree gets muted). The
420
+ * clock is an effect like the disk, so it is read where effects are — an adapter's `*.io.ts` and the
421
+ * root, `src/cli.ts` — and passed in. Matched on the AST: `Date.now` (called or passed along),
422
+ * `new Date()` and `Date()` without an argument. `new Date(ms)` converts a given time and passes.
423
+ * `toolchain.ts` and `build-engine.ts` keep `Date.now` as a default for their own timing (build and
424
+ * install only, neither runs under `lint`); that legacy I/O moves behind a port in #76.
425
+ */
426
+ const READS_THE_CLOCK = [
427
+ "MemberExpression[object.name='Date'][property.name='now']",
428
+ "NewExpression[callee.name='Date'][arguments.length=0]",
429
+ "CallExpression[callee.name='Date']",
430
+ ].map((selector) => ({
431
+ selector,
432
+ message:
433
+ "Lint never reads the clock: its findings are a function of the files. Take the time as an argument; read it in an adapter's *.io.ts or src/cli.ts.",
434
+ }));
435
+
436
+ /** Every source file the checks are made of, but the places effects are allowed. Same scope as axis B. */
437
+ export const NO_CLOCK = {
438
+ files: ["src/**/*.ts"],
439
+ ignores: [
440
+ ...IO_GLOBALS.ignores,
441
+ // Legacy I/O, moves behind a port in #76 (see READS_THE_CLOCK).
442
+ "src/toolchain.ts",
443
+ "src/build-engine.ts",
444
+ ],
445
+ rules: {
446
+ "no-restricted-syntax": ["error", AS_UNKNOWN_AS, ...READS_THE_CLOCK],
447
+ },
448
+ };
449
+
416
450
  /**
417
451
  * EVERY PRESET HAS A CARD, AND EVERY CARD'S RULES TABLE IS THE PRESET'S. Two checks, one block each:
418
452
  *
@@ -608,6 +642,7 @@ export default [
608
642
  },
609
643
  // Hexagonal layers, both axes (src/CLAUDE.md): knowledge by element, purity by file category.
610
644
  IO_GLOBALS,
645
+ NO_CLOCK,
611
646
  layerBoundaries(dirname(fileURLToPath(import.meta.url))),
612
647
  // The ratchet: per-file ceilings for the files written before the limits existed. See RATCHET.
613
648
  ...Object.entries(RATCHET).map(([file, max]) => ({
@@ -805,7 +840,12 @@ export default [
805
840
  files: ["eslint-rules/*.ts", "eslint-rules/*.mjs"],
806
841
  ignores: ["eslint-rules/*.test.*", "eslint-rules/*.harness.*"],
807
842
  rules: {
808
- "no-restricted-syntax": ["error", AS_UNKNOWN_AS, ...GIT_IN_A_RULE],
843
+ "no-restricted-syntax": [
844
+ "error",
845
+ AS_UNKNOWN_AS,
846
+ ...GIT_IN_A_RULE,
847
+ ...READS_THE_CLOCK,
848
+ ],
809
849
  },
810
850
  },
811
851
  // LAST, so no block above can replace its `no-restricted-syntax` options for these files (none
@@ -0,0 +1,3 @@
1
+ # Fixture
2
+
3
+ Stage: draft
@@ -0,0 +1,18 @@
1
+ {
2
+ "$comment": "A real IEEEtran [10pt,conference] paper for MSR whose ten pages of main text include an appendix before the references, with two pages of only references: format/page-limit, tex/template and anonymity/identity have nothing to say. banal's own count (body 9, references 3) would fail it.",
3
+ "build": {
4
+ "outcome": "pdf",
5
+ "pages": 12
6
+ },
7
+ "fonts": {
8
+ "some": "NimbusRomNo9L",
9
+ "none": "^(CMR|CMBX|CMTI|CMTT|CMSS|LMRoman)"
10
+ },
11
+ "lint": {
12
+ "rules": {
13
+ "format/page-limit": [],
14
+ "tex/template": [],
15
+ "anonymity/identity": []
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,49 @@
1
+ % A paper in the class MSR's call names, built by the e2e on paperlint's own TeX Live, that MEETS
2
+ % MSR's limit: ten pages of main text, its appendix included, and two pages holding only references
3
+ % (rendered and looked at, 2026-10-05: the appendix heading stands mid-column on page 9, appendix
4
+ % text then «REFERENCES» on page 10, nothing but entries on pages 11 and 12). banal's count says body
5
+ % 9, references 3 — a page whose first heading reads «Appendix» is typed as bibliography — and would
6
+ % fail it; format/page-limit counts from the text and must not.
7
+ %
8
+ % The prose is one paragraph set many times; each copy carries its own number in words on every
9
+ % line, so no line repeats from page to page as a running header would.
10
+ \documentclass[10pt,conference]{IEEEtran}
11
+ \usepackage{hyperref}
12
+ \newcount\paracount
13
+ \newcommand{\n}{\romannumeral\paracount}
14
+ \newcommand{\para}{\global\advance\paracount by 1
15
+ Paragraph \n{} studies the history of a repository, and \n{} counts its commits by author \n{}
16
+ and by month \n{}, so that the measurement \n{} has a body of prose \n{} set in the two columns
17
+ \n{} of the conference format. Paragraph \n{} says nothing new, and \n{} does not need to: the
18
+ fixture \n{} is about the class, \n{} the fonts and \n{} the page count, and \n{} not about its
19
+ content. A reviewer of \n{} would find it dull, \n{} which is what a measurement \n{} should be.\par}
20
+ \newcount\loopcount
21
+ \newcommand{\paras}[1]{\loopcount=0 \loop\para\advance\loopcount by 1 \ifnum\loopcount<#1\repeat}
22
+ \newcount\bibcount
23
+ \newcommand{\entries}[1]{\bibcount=0 \loop\advance\bibcount by 1
24
+ \bibitem{r\the\bibcount} A. Author, ``Mining study \romannumeral\bibcount,'' in \emph{Proc.
25
+ Workshop}, 2024, pp. 1--10.\ifnum\bibcount<#1\repeat}
26
+ \begin{document}
27
+ \title{A Fixture Typeset in the Class the Venue Requires}
28
+ \author{\IEEEauthorblockN{Anonymous Author}\IEEEauthorblockA{Anonymous Institution}}
29
+ \maketitle
30
+ \begin{abstract}
31
+ The abstract exists so the build has the front matter an IEEE paper carries.
32
+ \end{abstract}
33
+ \section{Introduction}
34
+ \paras{24}
35
+ \section{Part Two}
36
+ \paras{24}
37
+ \section{Part Three}
38
+ \paras{24}
39
+ \section{Part Four}
40
+ \paras{24}
41
+ \section{Part Five}
42
+ \paras{12}
43
+ \appendix
44
+ \section{Detailed Results}
45
+ \paras{12}
46
+ \begin{thebibliography}{999}
47
+ \entries{290}
48
+ \end{thebibliography}
49
+ \end{document}
@@ -0,0 +1 @@
1
+ {"extends": "paperlint:msr", "kind": "technical", "identity": ["Ada Example", "adaexample", "ada@example.org", "Example University", "exampletool"]}
@@ -0,0 +1,3 @@
1
+ # Fixture
2
+
3
+ Stage: draft
@@ -0,0 +1,20 @@
1
+ {
2
+ "$comment": "MSR: an appendix after the references brings the main text to eleven pages. Exactly one format/page-limit finding, naming where the appendix starts. banal's own count (body 9, references 2) would pass it.",
3
+ "build": {
4
+ "outcome": "pdf",
5
+ "pages": 11
6
+ },
7
+ "lint": {
8
+ "rules": {
9
+ "format/page-limit": [
10
+ {
11
+ "severity": 2,
12
+ "says": [
13
+ "body pages (up to the references on page 10, and the appendix from page 10): 11, over the limit 10 for MSR/technical"
14
+ ]
15
+ }
16
+ ],
17
+ "tex/template": []
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,48 @@
1
+ % A paper in the class MSR's call names, built by the e2e on paperlint's own TeX Live, that is ONE
2
+ % PAGE OVER MSR's limit through its appendix: the body ends on page 10, the references follow it
3
+ % there, and the appendix after them runs from the foot of page 10 through page 11 (rendered and
4
+ % looked at, 2026-10-05) — eleven pages holding main text. banal's count says body 9, references 2,
5
+ % and would pass it; format/page-limit counts the appendix as body and must not.
6
+ %
7
+ % The prose is one paragraph set many times; each copy carries its own number in words on every
8
+ % line, so no line repeats from page to page as a running header would.
9
+ \documentclass[10pt,conference]{IEEEtran}
10
+ \usepackage{hyperref}
11
+ \newcount\paracount
12
+ \newcommand{\n}{\romannumeral\paracount}
13
+ \newcommand{\para}{\global\advance\paracount by 1
14
+ Paragraph \n{} studies the history of a repository, and \n{} counts its commits by author \n{}
15
+ and by month \n{}, so that the measurement \n{} has a body of prose \n{} set in the two columns
16
+ \n{} of the conference format. Paragraph \n{} says nothing new, and \n{} does not need to: the
17
+ fixture \n{} is about the class, \n{} the fonts and \n{} the page count, and \n{} not about its
18
+ content. A reviewer of \n{} would find it dull, \n{} which is what a measurement \n{} should be.\par}
19
+ \newcount\loopcount
20
+ \newcommand{\paras}[1]{\loopcount=0 \loop\para\advance\loopcount by 1 \ifnum\loopcount<#1\repeat}
21
+ \newcount\bibcount
22
+ \newcommand{\entries}[1]{\bibcount=0 \loop\advance\bibcount by 1
23
+ \bibitem{r\the\bibcount} A. Author, ``Mining study \romannumeral\bibcount,'' in \emph{Proc.
24
+ Workshop}, 2024, pp. 1--10.\ifnum\bibcount<#1\repeat}
25
+ \begin{document}
26
+ \title{A Fixture Typeset in the Class the Venue Requires}
27
+ \author{\IEEEauthorblockN{Anonymous Author}\IEEEauthorblockA{Anonymous Institution}}
28
+ \maketitle
29
+ \begin{abstract}
30
+ The abstract exists so the build has the front matter an IEEE paper carries.
31
+ \end{abstract}
32
+ \section{Introduction}
33
+ \paras{24}
34
+ \section{Part Two}
35
+ \paras{24}
36
+ \section{Part Three}
37
+ \paras{24}
38
+ \section{Part Four}
39
+ \paras{24}
40
+ \section{Part Five}
41
+ \paras{24}
42
+ \begin{thebibliography}{999}
43
+ \entries{60}
44
+ \end{thebibliography}
45
+ \appendix
46
+ \section{Detailed Results}
47
+ \paras{12}
48
+ \end{document}
@@ -0,0 +1 @@
1
+ {"extends": "paperlint:msr", "kind": "technical", "identity": ["Ada Example", "adaexample", "ada@example.org", "Example University", "exampletool"]}
@@ -143,6 +143,8 @@ export const SETTINGS_KEYS: Readonly<Record<string, string>> = Object.freeze({
143
143
  talk: 'how this paper is presented: { "mode": "remote-video", "dir"?, "files"?, "one_slide"? } — the talk/ rules. A paper\'s own; refused in the root file',
144
144
  submission:
145
145
  "which submission on the venue's portal is this paper's: { \"id\": <number> } — paperlint submission, with the preset's \"portal\". A paper's own; refused in the root file",
146
+ cycles:
147
+ "the paper's attempts at venues, oldest first: [{ id, venue, kind, opened, phase?, deadlines, submission?, outcome }] — extends, kind and submission are then the current attempt's, and the cycle/ rules judge the record. A paper's own; refused in the root file",
146
148
  scripts: "how skill prose names the scripts — consumer.mjs",
147
149
  timezone: "the zone deadline anchors are written in — consumer.mjs",
148
150
  contactEmail: "the polite-pool address for citation lookups — consumer.mjs",
@@ -155,6 +157,7 @@ export const SETTINGS_KEYS: Readonly<Record<string, string>> = Object.freeze({
155
157
  export const PAPER_ONLY_KEYS: readonly string[] = Object.freeze([
156
158
  "talk",
157
159
  "submission",
160
+ "cycles",
158
161
  ]);
159
162
 
160
163
  /** The keys that only mean something for the whole project: refused in a paper's `paperlint.json`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "7.0.0",
3
+ "version": "7.2.0",
4
4
  "description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,10 +13,12 @@
13
13
  // and appendices.»
14
14
  // «Submissions must be properly anonymized for double-blind review.»
15
15
  //
16
- // THE DEADLINE PEOPLE GET WRONG (not a lint fact): the HotCRP portal closes 2026-10-02 11:59:59 UTC
16
+ // THE DEADLINE PEOPLE GET WRONG: the HotCRP portal closed 2026-10-02 11:59:59 UTC first
17
17
  // (`"sub":1790942399` on https://aidc.submit.acsac.org/deadlines, recorded in paperlint#90), while
18
- // the workshop page says «October 2, 2026 (Anywhere-on-Earth, AoE)» — a day later. The portal is
19
- // what refuses the upload.
18
+ // the workshop page said «October 2, 2026 (Anywhere-on-Earth, AoE)» — a day later; by 2026-10-05
19
+ // the portal had moved it a day (`"sub":1791028799`). The portal is what refuses the upload, so its
20
+ // readings are `deadlines` below, rewritten by the scheduled refresh in paperlint's repository
21
+ // (.github/workflows/deadlines.yml) when the portal changes them.
20
22
  //
21
23
  // THE REVIEW IS DOUBLE-BLIND: `"blind":true` in the same portal data, and the call above.
22
24
  //
@@ -38,6 +40,25 @@
38
40
  // the HotCRP API, there.
39
41
  "portal": { "kind": "hotcrp", "url": "https://aidc.submit.acsac.org" },
40
42
 
43
+ // Read from the portal's public deadlines page; the camera-ready only in its status object
44
+ // (`"final":{"done":1793447999}`).
45
+ "deadlines": [
46
+ {
47
+ "what": "submission",
48
+ "at": "2026-10-03T11:59:59Z",
49
+ "source": "portal",
50
+ "url": "https://aidc.submit.acsac.org/deadlines",
51
+ "read": "2026-10-05",
52
+ },
53
+ {
54
+ "what": "camera-ready",
55
+ "at": "2026-10-31T11:59:59Z",
56
+ "source": "portal",
57
+ "url": "https://aidc.submit.acsac.org/deadlines",
58
+ "read": "2026-10-05",
59
+ },
60
+ ],
61
+
41
62
  // What the venue is called in a paper's text (tex/venue-leftover): the workshop, and the
42
63
  // conference it is held at.
43
64
  "aliases": ["AIDC", "ACSAC"],
package/presets/aidc.md CHANGED
@@ -21,9 +21,12 @@ from it. The `submit-paper` skill reads this card for the venue's specifics.
21
21
  - **Double-blind**: «Submissions must be properly anonymized for double-blind review.», and
22
22
  `"blind":true` in the portal's data.
23
23
  - **Portal**: HotCRP at `https://aidc.submit.acsac.org`.
24
- - 🔴 **The deadline people get wrong**: the HotCRP portal closes 2026-10-02 11:59:59 UTC
25
- (`"sub":1790942399` on `https://aidc.submit.acsac.org/deadlines`), while the workshop page says
26
- «October 2, 2026 (Anywhere-on-Earth, AoE)» — a day later. The portal is what refuses the upload.
24
+ - 🔴 **The deadline people get wrong**: the HotCRP portal closed 2026-10-02 11:59:59 UTC first
25
+ (`"sub":1790942399` on `https://aidc.submit.acsac.org/deadlines`), while the workshop page said
26
+ «October 2, 2026 (Anywhere-on-Earth, AoE)» — a day later; on 2026-10-05 the portal read
27
+ 2026-10-03 11:59:59 UTC (`"sub":1791028799`). The portal is what refuses the upload. The preset
28
+ carries its readings as `deadlines`, refreshed by paperlint's scheduled workflow when the portal
29
+ changes them.
27
30
 
28
31
  ## Paper kinds and limits
29
32
 
@@ -30,7 +30,7 @@ versions. `compsoc` changes the text block and the headings, and nothing the `pd
30
30
  <!-- prettier-ignore-start -->
31
31
  <!-- Generated from ieee-conference.jsonc by `npx eslint --fix` (rule preset/card-rules). Edit the preset, not this section. -->
32
32
 
33
- **`paperlint:ieee-conference`** — template family, no venue of its own · extended by `paperlint:aidc`
33
+ **`paperlint:ieee-conference`** — template family, no venue of its own · extended by `paperlint:aidc`, `paperlint:msr`
34
34
 
35
35
  No preset of this chain sets a rule: under it, every rule keeps paperlint's default.
36
36
 
@@ -0,0 +1,95 @@
1
+ // MSR 2027 Technical Papers (the 24th International Conference on Mining Software Repositories) — a
2
+ // VENUE preset over the IEEE conference family. Everything the IEEEtran template decides (page
3
+ // size, columns, fonts, body size, TeX packages) comes from ieee-conference.jsonc; this file adds
4
+ // what MSR's call for papers sets.
5
+ //
6
+ // Verbatim from https://2027.msrconf.org/track/msr-2027-technical-papers (fetched 2026-10-05):
7
+ // «All submissions must conform to the IEEE conference proceedings template, specified in the
8
+ // IEEE Conference Proceedings Formatting Guidelines (title in 24pt font and full text in 10pt
9
+ // type, LaTeX users must use \documentclass[10pt,conference]{IEEEtran} without including the
10
+ // compsoc or compsocconf options).»
11
+ // «Note that IEEE format is being used this year, whereas last year it was ACM format.»
12
+ // «All submissions must not exceed 10 pages for the main text, inclusive of all figures, tables,
13
+ // appendices, etc. Two more pages containing only references are permitted. All submissions
14
+ // must be in PDF. Accepted papers will be allowed one extra page for the main text of the
15
+ // camera-ready version.»
16
+ // «The MSR 2027 Technical Track will employ a double-anonymous review process.»
17
+ //
18
+ // THE DEADLINES ARE THE PORTAL'S, as read: `deadlines` below is rewritten by the scheduled refresh
19
+ // in paperlint's repository (.github/workflows/deadlines.yml) when the portal changes them, and a
20
+ // paper reaches the new dates through its normal update of paperlint. The call's own wording
21
+ // (abstract and paper days, AoE) is in the card, msr.md beside this file.
22
+ {
23
+ "type": "venue",
24
+ "name": "MSR",
25
+ // The call quoted above.
26
+ "url": "https://2027.msrconf.org/track/msr-2027-technical-papers",
27
+ "extends": "paperlint:ieee-conference",
28
+
29
+ // «The MSR 2027 Technical Track will employ a double-anonymous review process. Thus, no
30
+ // submission may reveal its authors' identities.» (the call above), and `"blind":true` in the
31
+ // portal's data (https://msr2027.hotcrp.com/deadlines, 2026-10-05). anonymity/identity then
32
+ // requires the paper's `identity`.
33
+ "blind": true,
34
+
35
+ // «Papers must be submitted through HotCRP: https://msr2027.hotcrp.com/» (the call above).
36
+ // Recorded 2026-10-05: `/api/whoami` on this site answers 401 "Missing credentials" without a
37
+ // token — the HotCRP API, there.
38
+ "portal": { "kind": "hotcrp", "url": "https://msr2027.hotcrp.com" },
39
+
40
+ // Read from the portal's public deadlines page (no account) by the adapter in src/adapters/hotcrp/.
41
+ // «Submissions must be completed by this deadline» is the `submission`: on the CALL's abstract
42
+ // day, 04:00 UTC — four days before the call's paper day. «Completed submissions may be updated
43
+ // until this deadline» is the `resubmission`.
44
+ "deadlines": [
45
+ {
46
+ "what": "submission",
47
+ "at": "2026-10-20T04:00:00Z",
48
+ "source": "portal",
49
+ "url": "https://msr2027.hotcrp.com/deadlines",
50
+ "read": "2026-10-05",
51
+ },
52
+ {
53
+ "what": "resubmission",
54
+ "at": "2026-10-23T04:00:00Z",
55
+ "source": "portal",
56
+ "url": "https://msr2027.hotcrp.com/deadlines",
57
+ "read": "2026-10-05",
58
+ },
59
+ ],
60
+
61
+ // What the venue is called in a paper's text (tex/venue-leftover) — and only beside a year
62
+ // («MSR 2027», «MSR'27»): «MSR» is also Microsoft Research, and an acronym papers define for
63
+ // themselves («Manual Speech Recognition (MSR)», in an accepted ACSAC paper of
64
+ // fixtures/accepted-papers/, warned six times when any whole word counted).
65
+ "aliases": ["MSR"],
66
+ "mentions": "with-year",
67
+
68
+ // The class line the call names, with the two options it forbids (tex/template). The family's
69
+ // `[conference]` text block (7.14 x 9.30 in) is the one this line builds, so no `format` override.
70
+ "template": "\\documentclass[10pt,conference]{IEEEtran}",
71
+ "template_forbids": ["compsoc", "compsocconf"],
72
+
73
+ "format": {
74
+ // 🔴 THE MAIN TEXT INCLUDES THE APPENDICES, AND THE REFERENCE PAGES ARE THE ONES WITH ONLY
75
+ // REFERENCES — not banal's count. Measured 2026-10-05 on IEEEtran [10pt,conference] builds:
76
+ // 10 pages of main text, its appendix before the references, 2 reference-only pages
77
+ // → banal: body 9, references 3 (a page whose first heading reads «Appendix» is typed as
78
+ // bibliography) — a correct paper fails
79
+ // 11 pages of main text, the appendix after the references
80
+ // → banal: body 10, references 2 (the appendix taken for more references) — an over-long
81
+ // paper passes
82
+ // So format/page-limit counts from the text (`appendix_in_body`): the body is every page up to
83
+ // the references, that page too when body text stands above the heading, plus every page from
84
+ // an appendix that follows the references; the pages between are the reference pages
85
+ // (docs/rules/format/page-limit.md). An appendix after the references is found by hyperref's
86
+ // anchor: load hyperref.
87
+ "body_ends_at": "references",
88
+ "appendix_in_body": true,
89
+
90
+ // One kind: the Technical Track's papers. The camera-ready's extra page is not this limit.
91
+ "kinds": {
92
+ "technical": { "body_pages_max": 10, "ref_pages_max": 2 },
93
+ },
94
+ },
95
+ }
package/presets/msr.md ADDED
@@ -0,0 +1,98 @@
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. The preset carries them as `deadlines` (`source: "portal"`, with the
32
+ day read), refreshed by paperlint's scheduled workflow when the portal changes them; a paper whose
33
+ current cycle names `paperlint:msr` derives them. Recording the call's days in the paper's own
34
+ cycle maps «abstract» to `registration` and «paper» to `submission` — and the portal's earlier
35
+ `submission` is then the one in force.
36
+ - **Format**, verbatim from the call: «LaTeX users must use \documentclass[10pt,conference]{IEEEtran}
37
+ without including the compsoc or compsocconf options», «title in 24pt font and full text in 10pt
38
+ type», and «Alterations of spacing, font size, and other changes that deviate from the
39
+ instructions may result in desk rejection without further review.»
40
+ - **Double-anonymous**: «The MSR 2027 Technical Track will employ a double-anonymous review process.
41
+ Thus, no submission may reveal its authors' identities.» The call adds: names omitted, prior work
42
+ cited in the third person, and a preprint must not say it was submitted to MSR. The portal's data
43
+ says `"blind":true`.
44
+ - **Attendance**: «If a submission is accepted, at least one author of the paper is required to
45
+ register for MSR 2027 and present the paper.» The call does not say whether a remote presentation
46
+ is allowed.
47
+ - **New in 2027**, from the call: a submission may be desk rejected for fabricated references
48
+ («citations to papers, authors, venues, datasets, tools, or other artifacts that do not exist»),
49
+ clear signs of unverified AI-generated text, or hidden instructions aimed at automated tools.
50
+
51
+ ## Paper kinds and limits
52
+
53
+ | kind | main-text pages | pages of only references | what the call says |
54
+ | ----------- | --------------: | -----------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
55
+ | `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.» |
56
+
57
+ «Accepted papers will be allowed one extra page for the main text of the camera-ready version.» —
58
+ not the submission's limit, so not in the preset.
59
+
60
+ 🔴 **Appendices count in the ten pages**, and a page that holds anything but references is a
61
+ main-text page. banal's count gets both wrong on an IEEEtran build (an appendix before the
62
+ references turns its pages into «bibliography»; one after them into «references»), so the preset
63
+ counts from the text (`format.appendix_in_body`): the main text is every page up to the references —
64
+ that page too when text stands above the heading — and every page from an appendix that follows the
65
+ references; the pages between hold only references. **Load hyperref**: an appendix after the
66
+ references is found by hyperref's anchor, and without it is counted as reference pages. See
67
+ `docs/rules/format/page-limit.md`.
68
+
69
+ ## What else the preset encodes
70
+
71
+ - **The IEEE template**, through `extends: paperlint:ieee-conference` — letter paper, two columns,
72
+ Times at 10 pt, IEEEtran's TeX packages ([`ieee-conference.md`](ieee-conference.md)) — with
73
+ `10pt` and `conference` required in the class line and `compsoc` and `compsocconf` forbidden
74
+ (`tex/template`, `template_forbids`).
75
+ - **Double-blind review** (`blind`), so `anonymity/identity` checks the PDF against the paper's
76
+ declared `identity`.
77
+ - **Where submissions go** (`portal`): `paperlint submission` reads and updates a paper's
78
+ submission through HotCRP's API on that site.
79
+ - **Names**: `MSR`, what `tex/venue-leftover` looks for in another venue's paper — only with a year
80
+ beside it (`mentions: with-year`: «MSR 2027», «MSR'27»), since «MSR» is also Microsoft Research
81
+ and an acronym papers define for themselves.
82
+ - **Not encoded**: the capitalization of the title and headings (`tex/heading-case` stays off: the
83
+ IEEE guidelines' rule has not been read and quoted here), and the register of the body (no
84
+ accepted MSR paper in the IEEE template exists yet to anchor `tex/contrast-frames` and its
85
+ siblings).
86
+
87
+ ## What the preset resolves to
88
+
89
+ <!-- paperlint:preset-rules -->
90
+ <!-- prettier-ignore-start -->
91
+ <!-- Generated from msr.jsonc by `npx eslint --fix` (rule preset/card-rules). Edit the preset, not this section. -->
92
+
93
+ **`paperlint:msr`** — venue preset for MSR · call for papers: <https://2027.msrconf.org/track/msr-2027-technical-papers> · extends `paperlint:ieee-conference`
94
+
95
+ No preset of this chain sets a rule: under it, every rule keeps paperlint's default.
96
+
97
+ <!-- prettier-ignore-end -->
98
+ <!-- /paperlint:preset-rules -->