paperlint 5.0.0 → 6.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.
Files changed (177) hide show
  1. package/CLAUDE.md +13 -0
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +8 -8
  4. package/bin/paperlint.mjs +5 -1
  5. package/dist/adapters/latex/index.d.ts +2 -1
  6. package/dist/adapters/latex/index.d.ts.map +1 -1
  7. package/dist/adapters/latex/index.js +6 -2
  8. package/dist/adapters/latex/index.js.map +1 -1
  9. package/dist/adapters/latex/layout.d.ts +27 -0
  10. package/dist/adapters/latex/layout.d.ts.map +1 -0
  11. package/dist/adapters/latex/layout.js +62 -0
  12. package/dist/adapters/latex/layout.js.map +1 -0
  13. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  14. package/dist/adapters/latex/nodes.js +11 -1
  15. package/dist/adapters/latex/nodes.js.map +1 -1
  16. package/dist/adapters/latex/parse.d.ts.map +1 -1
  17. package/dist/adapters/latex/parse.js +5 -1
  18. package/dist/adapters/latex/parse.js.map +1 -1
  19. package/dist/adapters/latex/prose.d.ts +6 -1
  20. package/dist/adapters/latex/prose.d.ts.map +1 -1
  21. package/dist/adapters/latex/prose.js +142 -31
  22. package/dist/adapters/latex/prose.js.map +1 -1
  23. package/dist/cli.d.ts.map +1 -1
  24. package/dist/cli.js +35 -18
  25. package/dist/cli.js.map +1 -1
  26. package/dist/domain/anonymity.d.ts +68 -0
  27. package/dist/domain/anonymity.d.ts.map +1 -0
  28. package/dist/domain/anonymity.js +94 -0
  29. package/dist/domain/anonymity.js.map +1 -0
  30. package/dist/domain/body-pages.d.ts +69 -0
  31. package/dist/domain/body-pages.d.ts.map +1 -0
  32. package/dist/domain/body-pages.js +141 -0
  33. package/dist/domain/body-pages.js.map +1 -0
  34. package/dist/domain/register.d.ts +96 -0
  35. package/dist/domain/register.d.ts.map +1 -0
  36. package/dist/domain/register.js +136 -0
  37. package/dist/domain/register.js.map +1 -0
  38. package/dist/domain/tex-document.d.ts +28 -0
  39. package/dist/domain/tex-document.d.ts.map +1 -1
  40. package/dist/domain/tex-document.js +8 -3
  41. package/dist/domain/tex-document.js.map +1 -1
  42. package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
  43. package/dist/eslint-rules/pdf-last-page-balance.js +2 -2
  44. package/dist/facts-file.d.ts +28 -2
  45. package/dist/facts-file.d.ts.map +1 -1
  46. package/dist/facts-file.js +54 -9
  47. package/dist/facts-file.js.map +1 -1
  48. package/dist/lib/paper-config.d.ts.map +1 -1
  49. package/dist/lib/paper-config.js +2 -1
  50. package/dist/lib/paper-config.js.map +1 -1
  51. package/dist/paper-settings.d.ts +11 -0
  52. package/dist/paper-settings.d.ts.map +1 -1
  53. package/dist/paper-settings.js +38 -4
  54. package/dist/paper-settings.js.map +1 -1
  55. package/dist/pdf-content.d.ts +40 -0
  56. package/dist/pdf-content.d.ts.map +1 -0
  57. package/dist/pdf-content.js +97 -0
  58. package/dist/pdf-content.js.map +1 -0
  59. package/dist/pdf-facts.d.ts +30 -0
  60. package/dist/pdf-facts.d.ts.map +1 -1
  61. package/dist/pdf-facts.js +37 -1
  62. package/dist/pdf-facts.js.map +1 -1
  63. package/dist/ports/latex.d.ts +8 -1
  64. package/dist/ports/latex.d.ts.map +1 -1
  65. package/dist/presets.d.ts +5 -0
  66. package/dist/presets.d.ts.map +1 -1
  67. package/dist/presets.js +8 -1
  68. package/dist/presets.js.map +1 -1
  69. package/dist/register-bands.d.ts +37 -0
  70. package/dist/register-bands.d.ts.map +1 -0
  71. package/dist/register-bands.js +196 -0
  72. package/dist/register-bands.js.map +1 -0
  73. package/dist/register.d.ts +4 -2
  74. package/dist/register.d.ts.map +1 -1
  75. package/dist/register.js +6 -45
  76. package/dist/register.js.map +1 -1
  77. package/dist/tex-requirements.d.ts +10 -0
  78. package/dist/tex-requirements.d.ts.map +1 -1
  79. package/dist/tex-requirements.js +29 -6
  80. package/dist/tex-requirements.js.map +1 -1
  81. package/dist/tex-venue-rules.d.ts +8 -2
  82. package/dist/tex-venue-rules.d.ts.map +1 -1
  83. package/dist/tex-venue-rules.js +40 -0
  84. package/dist/tex-venue-rules.js.map +1 -1
  85. package/dist/types.d.ts +2 -0
  86. package/dist/types.d.ts.map +1 -1
  87. package/dist/venue-rules.d.ts +38 -8
  88. package/dist/venue-rules.d.ts.map +1 -1
  89. package/dist/venue-rules.js +179 -41
  90. package/dist/venue-rules.js.map +1 -1
  91. package/docs/configuration.md +18 -16
  92. package/docs/e2e.md +32 -11
  93. package/docs/rules/anonymity/identity.md +111 -0
  94. package/docs/rules/format/layout-override.md +61 -0
  95. package/docs/rules/format/page-limit.md +108 -0
  96. package/docs/rules/pdf/body-size.md +87 -0
  97. package/docs/rules/tex/claim-emphasis.md +71 -0
  98. package/docs/rules/tex/contrast-frames.md +100 -0
  99. package/docs/rules/tex/relation-markers.md +72 -0
  100. package/docs/rules.md +59 -50
  101. package/eslint-rules/pdf-last-page-balance.harness.mjs +6 -1
  102. package/eslint-rules/pdf-last-page-balance.ts +2 -2
  103. package/fixtures/build-e2e/aidc/expect.json +1 -1
  104. package/fixtures/build-e2e/aidc/paperlint.json +1 -1
  105. package/fixtures/build-e2e/anon-ack/PIPELINE-STATUS.md +3 -0
  106. package/fixtures/build-e2e/anon-ack/expect.json +19 -0
  107. package/fixtures/build-e2e/anon-ack/paper.tex +31 -0
  108. package/fixtures/build-e2e/anon-ack/paperlint.json +1 -0
  109. package/fixtures/build-e2e/anon-author/PIPELINE-STATUS.md +3 -0
  110. package/fixtures/build-e2e/anon-author/expect.json +19 -0
  111. package/fixtures/build-e2e/anon-author/paper.tex +29 -0
  112. package/fixtures/build-e2e/anon-author/paperlint.json +1 -0
  113. package/fixtures/build-e2e/anon-bib/PIPELINE-STATUS.md +3 -0
  114. package/fixtures/build-e2e/anon-bib/expect.json +20 -0
  115. package/fixtures/build-e2e/anon-bib/paper.tex +35 -0
  116. package/fixtures/build-e2e/anon-bib/paperlint.json +1 -0
  117. package/fixtures/build-e2e/anon-href/PIPELINE-STATUS.md +3 -0
  118. package/fixtures/build-e2e/anon-href/expect.json +19 -0
  119. package/fixtures/build-e2e/anon-href/paper.tex +30 -0
  120. package/fixtures/build-e2e/anon-href/paperlint.json +1 -0
  121. package/fixtures/build-e2e/anon-macro/PIPELINE-STATUS.md +3 -0
  122. package/fixtures/build-e2e/anon-macro/expect.json +19 -0
  123. package/fixtures/build-e2e/anon-macro/paper.tex +30 -0
  124. package/fixtures/build-e2e/anon-macro/paperlint.json +1 -0
  125. package/fixtures/build-e2e/anon-pdfauthor/PIPELINE-STATUS.md +3 -0
  126. package/fixtures/build-e2e/anon-pdfauthor/expect.json +19 -0
  127. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +31 -0
  128. package/fixtures/build-e2e/anon-pdfauthor/paperlint.json +1 -0
  129. package/fixtures/build-e2e/unbalanced/expect.json +2 -2
  130. package/lib/paper-config.ts +3 -1
  131. package/package.json +1 -1
  132. package/presets/acm-sigconf.jsonc +3 -1
  133. package/presets/aidc.jsonc +66 -17
  134. package/presets/ieee-conference.jsonc +8 -0
  135. package/presets/venue-profile.schema.json +57 -1
  136. package/skills/grade-paper-writing/SKILL.md +59 -27
  137. package/skills/grade-paper-writing/SKILL.md.spec.ts +58 -26
  138. package/skills/render-paper/extract-pdf-facts.harness.ts +3 -3
  139. package/src/adapters/latex/index.test.ts +91 -0
  140. package/src/adapters/latex/index.ts +6 -2
  141. package/src/adapters/latex/layout.ts +106 -0
  142. package/src/adapters/latex/nodes.ts +11 -3
  143. package/src/adapters/latex/parse.ts +5 -1
  144. package/src/adapters/latex/prose.ts +188 -58
  145. package/src/build.harness.mjs +2 -2
  146. package/src/build.test.ts +5 -0
  147. package/src/cli.test.ts +13 -0
  148. package/src/cli.ts +50 -17
  149. package/src/domain/anonymity.test.ts +163 -0
  150. package/src/domain/anonymity.ts +129 -0
  151. package/src/domain/body-pages.test.ts +300 -0
  152. package/src/domain/body-pages.ts +223 -0
  153. package/src/domain/register.test.ts +153 -0
  154. package/src/domain/register.ts +261 -0
  155. package/src/domain/tex-document.ts +47 -3
  156. package/src/facts-file.test.ts +100 -4
  157. package/src/facts-file.ts +91 -9
  158. package/src/new-paper.test.ts +7 -1
  159. package/src/paper-settings.test.ts +63 -6
  160. package/src/paper-settings.ts +53 -5
  161. package/src/pdf-content.test.ts +71 -0
  162. package/src/pdf-content.ts +125 -0
  163. package/src/pdf-facts.test.ts +110 -2
  164. package/src/pdf-facts.ts +106 -8
  165. package/src/ports/latex.ts +9 -0
  166. package/src/presets.ts +15 -9
  167. package/src/register-bands.test.ts +327 -0
  168. package/src/register-bands.ts +291 -0
  169. package/src/register.ts +7 -37
  170. package/src/rule-cases.test.ts +96 -20
  171. package/src/rule-docs.test.ts +0 -2
  172. package/src/tex-requirements.ts +56 -8
  173. package/src/tex-venue-rules.test.ts +110 -5
  174. package/src/tex-venue-rules.ts +64 -3
  175. package/src/types.ts +2 -0
  176. package/src/venue-rules.test.ts +351 -39
  177. package/src/venue-rules.ts +247 -47
@@ -28,6 +28,7 @@ import Ajv from "ajv";
28
28
  import { presetsDir } from "./package-dirs.ts";
29
29
  import { CONFIG_FILE } from "#lib/paper-config";
30
30
  import { fieldOf } from "./domain/record.ts";
31
+ import type { RegisterAnchor, RegisterMeasureName } from "./domain/register.ts";
31
32
 
32
33
  /** CTAN package name → the names that prove it is installed. */
33
34
  export type PackageProofs = Readonly<Record<string, readonly string[]>>;
@@ -131,6 +132,8 @@ export interface VenueFormat {
131
132
  /** The prefix a font name of the body text starts with (`LinLibertine`). */
132
133
  readonly fontsText: string | null;
133
134
  readonly fontsTitle: string | null;
135
+ /** How the body is counted: null is banal's count; `references`, the pages before the references. */
136
+ readonly bodyEndsAt: "references" | null;
134
137
  readonly kinds: ReadonlyMap<string, KindLimits>;
135
138
  }
136
139
 
@@ -145,6 +148,7 @@ export const NO_FORMAT: VenueFormat = {
145
148
  refPtMax: null,
146
149
  fontsText: null,
147
150
  fontsTitle: null,
151
+ bodyEndsAt: null,
148
152
  kinds: new Map(),
149
153
  };
150
154
 
@@ -168,6 +172,8 @@ export interface PresetFile {
168
172
  readonly template: string | null;
169
173
  /** Other names the venue goes by in a paper's text; empty when the file names none. */
170
174
  readonly aliases: readonly string[];
175
+ /** Whether the venue reviews double-blind; null when the file does not say. */
176
+ readonly blind: boolean | null;
171
177
  /** Sections the venue requires; null when the file names none (a child's list replaces its parent's). */
172
178
  readonly requiredSections: readonly RequiredSection[] | null;
173
179
  /** Null when the file declares no `tex` block (allowed only with `extends`). */
@@ -175,6 +181,11 @@ export interface PresetFile {
175
181
  readonly format: VenueFormat;
176
182
  /** rule id → ESLint entry, checked against the shipped rules where a config is built. */
177
183
  readonly rules: Readonly<Record<string, unknown>>;
184
+ /**
185
+ * The accepted papers the register rules measure a body against; null when the file names none
186
+ * (a child's list replaces its parent's).
187
+ */
188
+ readonly registerAnchors: readonly RegisterAnchor[] | null;
178
189
  }
179
190
 
180
191
  type KindsJson = Readonly<
@@ -192,6 +203,7 @@ interface FormatJson {
192
203
  readonly ref_pt_max?: number;
193
204
  readonly fonts_text?: string;
194
205
  readonly fonts_title?: string;
206
+ readonly body_ends_at?: "references";
195
207
  readonly kinds?: KindsJson;
196
208
  }
197
209
 
@@ -201,6 +213,7 @@ interface PresetJson {
201
213
  readonly name?: string;
202
214
  readonly template?: string;
203
215
  readonly aliases?: readonly string[];
216
+ readonly blind?: boolean;
204
217
  readonly required_sections?: readonly {
205
218
  readonly title: string;
206
219
  readonly position?: "last";
@@ -210,6 +223,12 @@ interface PresetJson {
210
223
  Partial<Pick<TexRequirements, "tools">>;
211
224
  readonly format?: FormatJson;
212
225
  readonly rules?: Readonly<Record<string, unknown>>;
226
+ readonly register?: {
227
+ readonly anchors: readonly ({
228
+ readonly paper: string;
229
+ readonly words: number;
230
+ } & Readonly<Record<RegisterMeasureName, number>>)[];
231
+ };
213
232
  }
214
233
 
215
234
  /** An optional field as the typed preset holds it: absent is null. */
@@ -238,6 +257,7 @@ function formatOf(j: FormatJson = {}): VenueFormat {
238
257
  refPtMax: orNull(j.ref_pt_max),
239
258
  fontsText: orNull(j.fonts_text),
240
259
  fontsTitle: orNull(j.fonts_title),
260
+ bodyEndsAt: orNull(j.body_ends_at),
241
261
  kinds: kindsOf(j.kinds),
242
262
  };
243
263
  }
@@ -250,6 +270,38 @@ const sectionsOf = (
250
270
  ? null
251
271
  : r.map((x) => ({ title: x.title, position: orNull(x.position) }));
252
272
 
273
+ /** A preset's JSONC text → its value, or an Error naming the syntax problem. */
274
+ function jsoncOf(text: string, file: string): unknown {
275
+ const parsed = typescript().parseConfigFileTextToJson(file, text);
276
+ const config: unknown = parsed.config;
277
+ const error = parsed.error;
278
+ if (error)
279
+ throw new Error(
280
+ `${file}: not valid JSONC — ${typescript().flattenDiagnosticMessageText(error.messageText, " ")}`,
281
+ );
282
+ return config;
283
+ }
284
+
285
+ /** A preset's `tex` block, `tools` defaulted; null when the file declares none. */
286
+ const texOf = (t: PresetJson["tex"]): TexRequirements | null =>
287
+ t ? { packages: t.packages, tools: t.tools ?? {} } : null;
288
+
289
+ /** A preset's `register.anchors`, each with its counts; null when the file names none. */
290
+ const anchorsOf = (
291
+ r: PresetJson["register"],
292
+ ): readonly RegisterAnchor[] | null =>
293
+ r === undefined
294
+ ? null
295
+ : r.anchors.map((a) => ({
296
+ paper: a.paper,
297
+ words: a.words,
298
+ counts: {
299
+ contrast_frames: a.contrast_frames,
300
+ claim_emphasis: a.claim_emphasis,
301
+ relation_markers: a.relation_markers,
302
+ },
303
+ }));
304
+
253
305
  /**
254
306
  * The text of one preset file → the typed file, or an Error naming every problem. The ONE parser
255
307
  * of a preset: the toolchain reads its `tex`, the venue rules its `format`, the config its `rules`.
@@ -263,13 +315,7 @@ export function parsePreset(
263
315
  file: string,
264
316
  dir: string = presetsDir(),
265
317
  ): PresetFile {
266
- const parsed = typescript().parseConfigFileTextToJson(file, text);
267
- const config: unknown = parsed.config;
268
- const error = parsed.error;
269
- if (error)
270
- throw new Error(
271
- `${file}: not valid JSONC — ${typescript().flattenDiagnosticMessageText(error.messageText, " ")}`,
272
- );
318
+ const config = jsoncOf(text, file);
273
319
  const validate = validatorFor(dir);
274
320
  if (!matchesSchema(config, validate))
275
321
  throw new Error(
@@ -281,10 +327,12 @@ export function parsePreset(
281
327
  name: orNull(j.name),
282
328
  template: orNull(j.template),
283
329
  aliases: j.aliases ?? [],
330
+ blind: orNull(j.blind),
284
331
  requiredSections: sectionsOf(j.required_sections),
285
- tex: j.tex ? { packages: j.tex.packages, tools: j.tex.tools ?? {} } : null,
332
+ tex: texOf(j.tex),
286
333
  format: formatOf(j.format),
287
334
  rules: j.rules ?? {},
335
+ registerAnchors: anchorsOf(j.register),
288
336
  };
289
337
  }
290
338
 
@@ -13,6 +13,8 @@ import {
13
13
  judgeRequiredSections,
14
14
  judgeTemplate,
15
15
  otherVenues,
16
+ FORMAT_RULE_LEVELS,
17
+ formatRules,
16
18
  TEX_VENUE_RULE_LEVELS,
17
19
  texVenueRules,
18
20
  } from "./tex-venue-rules.ts";
@@ -66,9 +68,15 @@ function lint(
66
68
  ...extra,
67
69
  });
68
70
  const out: Finding[] = [];
69
- for (const [name, rule] of Object.entries(
70
- texVenueRules({ files, venuesDir: VENUES, latex: latexReader }),
71
- )) {
71
+ const deps = { files, venuesDir: VENUES, latex: latexReader };
72
+ for (const [id, rule] of [
73
+ ...Object.entries(texVenueRules(deps)).map(
74
+ ([n, r]) => [`tex/${n}`, r] as const,
75
+ ),
76
+ ...Object.entries(formatRules(deps)).map(
77
+ ([n, r]) => [`format/${n}`, r] as const,
78
+ ),
79
+ ]) {
72
80
  const visitor = rule.create({
73
81
  filename,
74
82
  sourceCode: {
@@ -78,7 +86,7 @@ function lint(
78
86
  },
79
87
  report: (d) =>
80
88
  out.push({
81
- rule: `tex/${name}`,
89
+ rule: id,
82
90
  // A finding from an included file arrives as a whole message, its file at the front.
83
91
  messageId: "messageId" in d ? d.messageId : "(message)",
84
92
  message:
@@ -720,7 +728,7 @@ describe("otherVenues", () => {
720
728
  });
721
729
 
722
730
  describe("paperlint's own config", () => {
723
- it.each(Object.entries(TEX_VENUE_RULE_LEVELS))(
731
+ it.each(Object.entries({ ...TEX_VENUE_RULE_LEVELS, ...FORMAT_RULE_LEVELS }))(
724
732
  "turns %s on at %s for every paper.tex, and ships it",
725
733
  (id, level) => {
726
734
  const tex = buildConfig({}, { sentinel: "tex" }).find((b) =>
@@ -738,4 +746,101 @@ describe("paperlint's own config", () => {
738
746
  "tex/venue-leftover": "warn",
739
747
  });
740
748
  });
749
+
750
+ it("format/layout-override is an error: the layout it guards is the template's", () => {
751
+ expect(FORMAT_RULE_LEVELS).toEqual({ "format/layout-override": "error" });
752
+ });
753
+ });
754
+
755
+ /** A conforming AIDC paper with `preamble` after its class line, and `body` before its statement. */
756
+ const withPreamble = (preamble: string, body = "Text.") =>
757
+ paper(
758
+ `\\documentclass[conference,compsoc]{IEEEtran}\n${preamble}`,
759
+ `${body}\n\\section*{LLM Usage Statement}\nNone.`,
760
+ );
761
+ const overrides = (tex: string, settings: object | undefined = AIDC) =>
762
+ lint(tex, settings).filter((f) => f.rule === "format/layout-override");
763
+
764
+ describe("format/layout-override reports", () => {
765
+ it("each command that changes the template's layout, on its own line", () => {
766
+ const tex = withPreamble(
767
+ [
768
+ "\\usepackage[margin=1in]{geometry}",
769
+ "\\usepackage{amsmath,geometry}",
770
+ "\\setlength{\\textheight}{9.5in}",
771
+ "\\addtolength\\textwidth{1in}",
772
+ "\\setlength{\\columnsep}{0.1in}",
773
+ "\\linespread{0.95}",
774
+ "\\renewcommand{\\baselinestretch}{0.9}",
775
+ "\\geometry{left=1cm}",
776
+ ].join("\n"),
777
+ "Text.\n\\newgeometry{top=1cm}\nMore text.",
778
+ );
779
+ expect(
780
+ overrides(tex).map((f) => [f.line, f.message.split("`")[1]]),
781
+ ).toEqual([
782
+ [3, "\\usepackage{geometry}"],
783
+ [4, "\\usepackage{geometry}"],
784
+ [5, "\\setlength{\\textheight}"],
785
+ [6, "\\addtolength{\\textwidth}"],
786
+ [7, "\\setlength{\\columnsep}"],
787
+ [8, "\\linespread{0.95}"],
788
+ [9, "\\renewcommand{\\baselinestretch}"],
789
+ [10, "\\geometry"],
790
+ [13, "\\newgeometry"],
791
+ ]);
792
+ expect(overrides(tex)[0]?.message).toBe(
793
+ "`\\usepackage{geometry}` changes the page layout aidc's template sets (`\\documentclass[conference,compsoc]{IEEEtran}`) — a desk-reject reason at venues that check the format. Remove it; if the venue allows it, disable this line with a comment saying so",
794
+ );
795
+ });
796
+ it("a definition's body counts: the length it sets is set wherever it is used", () => {
797
+ const tex = withPreamble(
798
+ "\\newcommand{\\tall}{\\setlength{\\textheight}{10in}}",
799
+ );
800
+ expect(overrides(tex).map((f) => f.line)).toEqual([3]);
801
+ });
802
+ });
803
+
804
+ describe("format/layout-override stays silent", () => {
805
+ it("silent on what leaves the layout alone", () => {
806
+ const tex = withPreamble(
807
+ [
808
+ "\\usepackage{amsmath}",
809
+ "\\setlength{\\parindent}{0pt}",
810
+ "\\renewcommand{\\thesection}{\\arabic{section}}",
811
+ "% \\usepackage{geometry} in a comment",
812
+ "\\begin{comment}\\linespread{0.9}\\end{comment}",
813
+ // A length command with no arguments at all names no length.
814
+ "{\\setlength}",
815
+ ].join("\n"),
816
+ "A \\vspace{2mm} B \\vskip 3pt C \\vskip\\baselineskip D.",
817
+ );
818
+ expect(overrides(tex)).toEqual([]);
819
+ });
820
+
821
+ it("silent on local space pulled back: no venue's call names a negative skip", () => {
822
+ const tex = withPreamble(
823
+ "\\newcommand{\\tight}{\\vspace{-3pt}}",
824
+ "A \\vspace{-2mm} B \\vspace*{-1em} C \\vskip -3pt D.",
825
+ );
826
+ expect(lint(tex, AIDC)).toEqual([]);
827
+ });
828
+
829
+ it("silent without a preset, or with one that names no template", () => {
830
+ const tex = withPreamble("\\usepackage{geometry}");
831
+ expect(
832
+ lint(tex, undefined).filter((f) => f.rule === "format/layout-override"),
833
+ ).toEqual([]);
834
+ expect(overrides(tex, { extends: null })).toEqual([]);
835
+ const house = {
836
+ [`${PAPER}/house.jsonc`]: JSON.stringify({
837
+ tex: { packages: { ieeetran: ["IEEEtran.cls"] } },
838
+ }),
839
+ };
840
+ expect(
841
+ lint(tex, { extends: "./house.jsonc" }, { extra: house }).filter(
842
+ (f) => f.rule === "format/layout-override",
843
+ ),
844
+ ).toEqual([]);
845
+ });
741
846
  });
@@ -8,6 +8,13 @@
8
8
  * tex/venue-leftover warn the text a reader sees names ANOTHER shipped venue — its `name`
9
9
  * or one of its `aliases` — than the one the paper extends
10
10
  *
11
+ * and, in the `format` plugin (named for the requirement, not the input — paperlint#151):
12
+ *
13
+ * format/layout-override error the source changes the page layout the venue's template sets for
14
+ * the whole document — margins, text block, line spacing
15
+ *
16
+ * The `format` plugin's other rule, `format/page-limit`, judges the built PDF (`venue-rules.ts`).
17
+ *
11
18
  * ── WHY THEY LIVE HERE AND NOT IN `eslint-rules/` ────────────────────────────────
12
19
  * Like the `pdf/*` venue rules (`venue-rules.ts`), they need the paper's resolved preset —
13
20
  * `paperPreset`, the one answer to "which venue is this paper judged against" — and read it through
@@ -32,6 +39,7 @@ import {
32
39
  type ClassLine,
33
40
  type DocumentClass,
34
41
  type Heading,
42
+ type LayoutOverride,
35
43
  type Outline,
36
44
  type Place,
37
45
  type Span,
@@ -290,7 +298,13 @@ export function judgeLeftover(
290
298
 
291
299
  /** Where a rule's page lives: `docs/rules/tex/<name>.md` on the default branch. */
292
300
  export const ruleDocsUrl = (
293
- name: TexVenueRuleName | "claim-provenance" | "register",
301
+ name:
302
+ | TexVenueRuleName
303
+ | "claim-provenance"
304
+ | "register"
305
+ | "contrast-frames"
306
+ | "claim-emphasis"
307
+ | "relation-markers",
294
308
  ): string =>
295
309
  `https://github.com/zernie/paperlint/blob/main/docs/rules/tex/${name}.md`;
296
310
 
@@ -306,9 +320,26 @@ type Judge = (r: Reading) => readonly Located[];
306
320
  export type TexVenueRuleName =
307
321
  "template" | "required-section" | "venue-leftover";
308
322
 
323
+ /** Every rule this module builds: the `tex` plugin's three and the `format` plugin's one. */
324
+ type RuleName = TexVenueRuleName | "layout-override";
325
+
326
+ /** Each layout override, where it stands. Silent for a preset that names no template. */
327
+ export function judgeLayout(
328
+ overrides: readonly LayoutOverride[],
329
+ preset: Preset,
330
+ ): readonly Located[] {
331
+ const template = preset.template;
332
+ if (template === null) return [];
333
+ return overrides.map((o) => ({
334
+ messageId: "override",
335
+ data: { command: o.command, venue: preset.label, template: template.text },
336
+ at: spanOf(o.place),
337
+ }));
338
+ }
339
+
309
340
  const RULES: Readonly<
310
341
  Record<
311
- TexVenueRuleName,
342
+ RuleName,
312
343
  { readonly judge: Judge; readonly meta: TexRuleModule["meta"] }
313
344
  >
314
345
  > = {
@@ -357,6 +388,22 @@ const RULES: Readonly<
357
388
  },
358
389
  },
359
390
  },
391
+ "layout-override": {
392
+ judge: (r) => judgeLayout(r.latex.layoutOverrides(r.src), r.preset),
393
+ meta: {
394
+ type: "problem",
395
+ docs: {
396
+ description:
397
+ "the source changes the page layout the venue's template sets for the whole document: margins, text block, line spacing",
398
+ url: "https://github.com/zernie/paperlint/blob/main/docs/rules/format/layout-override.md",
399
+ },
400
+ schema: [],
401
+ messages: {
402
+ override:
403
+ "`{{command}}` changes the page layout {{venue}}'s template sets (`{{template}}`) — a desk-reject reason at venues that check the format. Remove it; if the venue allows it, disable this line with a comment saying so",
404
+ },
405
+ },
406
+ },
360
407
  "venue-leftover": {
361
408
  judge: (r) =>
362
409
  judgeLeftover(r.latex.renderedRuns(r.src), r.preset, r.others()),
@@ -385,6 +432,13 @@ export const TEX_VENUE_RULE_LEVELS: Readonly<
385
432
  "tex/venue-leftover": "warn",
386
433
  };
387
434
 
435
+ /** The `format` plugin's rule over the source and its level, on for every `paper.tex`. */
436
+ export const FORMAT_RULE_LEVELS: Readonly<
437
+ Record<"format/layout-override", "error">
438
+ > = {
439
+ "format/layout-override": "error",
440
+ };
441
+
388
442
  /**
389
443
  * Every shipped venue but the ones on this paper's own chain, each resolved the way a paper
390
444
  * extending it would be. A preset that does not resolve is skipped: `pdf/profile` owns that.
@@ -430,7 +484,7 @@ function readingOf(
430
484
  };
431
485
  }
432
486
 
433
- function rule(name: TexVenueRuleName, deps: TexVenueRuleDeps): TexRuleModule {
487
+ function rule(name: RuleName, deps: TexVenueRuleDeps): TexRuleModule {
434
488
  const { judge, meta } = RULES[name];
435
489
  return {
436
490
  meta,
@@ -459,3 +513,10 @@ export function texVenueRules(
459
513
  "venue-leftover": rule("venue-leftover", deps),
460
514
  };
461
515
  }
516
+
517
+ /** The `format` plugin's rule over the source (`page-limit`, over the PDF, is in `venue-rules.ts`). */
518
+ export function formatRules(
519
+ deps: TexVenueRuleDeps,
520
+ ): Readonly<Record<"layout-override", TexRuleModule>> {
521
+ return { "layout-override": rule("layout-override", deps) };
522
+ }
package/src/types.ts CHANGED
@@ -26,6 +26,8 @@ export interface PaperlintConfig {
26
26
  kind?: string;
27
27
  /** The default path of the built PDF, relative to each paper. */
28
28
  pdf?: string;
29
+ /** What identifies the authors, for every paper (joined with a paper's own list). */
30
+ identity?: readonly string[];
29
31
  /** A note for humans; ignored. */
30
32
  $comment?: unknown;
31
33
  }