paperlint 5.0.0 → 6.0.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 (147) 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 +1 -0
  6. package/dist/adapters/latex/index.d.ts.map +1 -1
  7. package/dist/adapters/latex/index.js +3 -0
  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/cli.d.ts.map +1 -1
  20. package/dist/cli.js +25 -5
  21. package/dist/cli.js.map +1 -1
  22. package/dist/domain/anonymity.d.ts +68 -0
  23. package/dist/domain/anonymity.d.ts.map +1 -0
  24. package/dist/domain/anonymity.js +94 -0
  25. package/dist/domain/anonymity.js.map +1 -0
  26. package/dist/domain/body-pages.d.ts +69 -0
  27. package/dist/domain/body-pages.d.ts.map +1 -0
  28. package/dist/domain/body-pages.js +141 -0
  29. package/dist/domain/body-pages.js.map +1 -0
  30. package/dist/domain/tex-document.d.ts +9 -0
  31. package/dist/domain/tex-document.d.ts.map +1 -1
  32. package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
  33. package/dist/eslint-rules/pdf-last-page-balance.js +2 -2
  34. package/dist/facts-file.d.ts +28 -2
  35. package/dist/facts-file.d.ts.map +1 -1
  36. package/dist/facts-file.js +54 -9
  37. package/dist/facts-file.js.map +1 -1
  38. package/dist/lib/paper-config.d.ts.map +1 -1
  39. package/dist/lib/paper-config.js +2 -1
  40. package/dist/lib/paper-config.js.map +1 -1
  41. package/dist/paper-settings.d.ts +11 -0
  42. package/dist/paper-settings.d.ts.map +1 -1
  43. package/dist/paper-settings.js +38 -4
  44. package/dist/paper-settings.js.map +1 -1
  45. package/dist/pdf-content.d.ts +40 -0
  46. package/dist/pdf-content.d.ts.map +1 -0
  47. package/dist/pdf-content.js +97 -0
  48. package/dist/pdf-content.js.map +1 -0
  49. package/dist/pdf-facts.d.ts +30 -0
  50. package/dist/pdf-facts.d.ts.map +1 -1
  51. package/dist/pdf-facts.js +37 -1
  52. package/dist/pdf-facts.js.map +1 -1
  53. package/dist/ports/latex.d.ts +3 -1
  54. package/dist/ports/latex.d.ts.map +1 -1
  55. package/dist/presets.d.ts +2 -0
  56. package/dist/presets.d.ts.map +1 -1
  57. package/dist/presets.js +4 -0
  58. package/dist/presets.js.map +1 -1
  59. package/dist/tex-requirements.d.ts +4 -0
  60. package/dist/tex-requirements.d.ts.map +1 -1
  61. package/dist/tex-requirements.js +3 -0
  62. package/dist/tex-requirements.js.map +1 -1
  63. package/dist/tex-venue-rules.d.ts +7 -1
  64. package/dist/tex-venue-rules.d.ts.map +1 -1
  65. package/dist/tex-venue-rules.js +40 -0
  66. package/dist/tex-venue-rules.js.map +1 -1
  67. package/dist/types.d.ts +2 -0
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/venue-rules.d.ts +38 -8
  70. package/dist/venue-rules.d.ts.map +1 -1
  71. package/dist/venue-rules.js +179 -41
  72. package/dist/venue-rules.js.map +1 -1
  73. package/docs/configuration.md +18 -16
  74. package/docs/e2e.md +32 -11
  75. package/docs/rules/anonymity/identity.md +111 -0
  76. package/docs/rules/format/layout-override.md +61 -0
  77. package/docs/rules/format/page-limit.md +108 -0
  78. package/docs/rules/pdf/body-size.md +87 -0
  79. package/docs/rules.md +44 -39
  80. package/eslint-rules/pdf-last-page-balance.harness.mjs +6 -1
  81. package/eslint-rules/pdf-last-page-balance.ts +2 -2
  82. package/fixtures/build-e2e/aidc/expect.json +1 -1
  83. package/fixtures/build-e2e/aidc/paperlint.json +1 -1
  84. package/fixtures/build-e2e/anon-ack/PIPELINE-STATUS.md +3 -0
  85. package/fixtures/build-e2e/anon-ack/expect.json +19 -0
  86. package/fixtures/build-e2e/anon-ack/paper.tex +31 -0
  87. package/fixtures/build-e2e/anon-ack/paperlint.json +1 -0
  88. package/fixtures/build-e2e/anon-author/PIPELINE-STATUS.md +3 -0
  89. package/fixtures/build-e2e/anon-author/expect.json +19 -0
  90. package/fixtures/build-e2e/anon-author/paper.tex +29 -0
  91. package/fixtures/build-e2e/anon-author/paperlint.json +1 -0
  92. package/fixtures/build-e2e/anon-bib/PIPELINE-STATUS.md +3 -0
  93. package/fixtures/build-e2e/anon-bib/expect.json +20 -0
  94. package/fixtures/build-e2e/anon-bib/paper.tex +35 -0
  95. package/fixtures/build-e2e/anon-bib/paperlint.json +1 -0
  96. package/fixtures/build-e2e/anon-href/PIPELINE-STATUS.md +3 -0
  97. package/fixtures/build-e2e/anon-href/expect.json +19 -0
  98. package/fixtures/build-e2e/anon-href/paper.tex +30 -0
  99. package/fixtures/build-e2e/anon-href/paperlint.json +1 -0
  100. package/fixtures/build-e2e/anon-macro/PIPELINE-STATUS.md +3 -0
  101. package/fixtures/build-e2e/anon-macro/expect.json +19 -0
  102. package/fixtures/build-e2e/anon-macro/paper.tex +30 -0
  103. package/fixtures/build-e2e/anon-macro/paperlint.json +1 -0
  104. package/fixtures/build-e2e/anon-pdfauthor/PIPELINE-STATUS.md +3 -0
  105. package/fixtures/build-e2e/anon-pdfauthor/expect.json +19 -0
  106. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +31 -0
  107. package/fixtures/build-e2e/anon-pdfauthor/paperlint.json +1 -0
  108. package/fixtures/build-e2e/unbalanced/expect.json +2 -2
  109. package/lib/paper-config.ts +3 -1
  110. package/package.json +1 -1
  111. package/presets/acm-sigconf.jsonc +3 -1
  112. package/presets/aidc.jsonc +16 -17
  113. package/presets/ieee-conference.jsonc +8 -0
  114. package/presets/venue-profile.schema.json +9 -0
  115. package/skills/render-paper/extract-pdf-facts.harness.ts +3 -3
  116. package/src/adapters/latex/index.ts +3 -0
  117. package/src/adapters/latex/layout.ts +106 -0
  118. package/src/adapters/latex/nodes.ts +11 -3
  119. package/src/adapters/latex/parse.ts +5 -1
  120. package/src/build.harness.mjs +2 -2
  121. package/src/build.test.ts +5 -0
  122. package/src/cli.test.ts +13 -0
  123. package/src/cli.ts +36 -4
  124. package/src/domain/anonymity.test.ts +163 -0
  125. package/src/domain/anonymity.ts +129 -0
  126. package/src/domain/body-pages.test.ts +300 -0
  127. package/src/domain/body-pages.ts +223 -0
  128. package/src/domain/tex-document.ts +10 -0
  129. package/src/facts-file.test.ts +100 -4
  130. package/src/facts-file.ts +91 -9
  131. package/src/new-paper.test.ts +7 -1
  132. package/src/paper-settings.test.ts +63 -6
  133. package/src/paper-settings.ts +53 -5
  134. package/src/pdf-content.test.ts +71 -0
  135. package/src/pdf-content.ts +125 -0
  136. package/src/pdf-facts.test.ts +110 -2
  137. package/src/pdf-facts.ts +106 -8
  138. package/src/ports/latex.ts +3 -0
  139. package/src/presets.ts +8 -8
  140. package/src/rule-cases.test.ts +29 -3
  141. package/src/rule-docs.test.ts +0 -2
  142. package/src/tex-requirements.ts +9 -0
  143. package/src/tex-venue-rules.test.ts +110 -5
  144. package/src/tex-venue-rules.ts +57 -2
  145. package/src/types.ts +2 -0
  146. package/src/venue-rules.test.ts +351 -39
  147. package/src/venue-rules.ts +247 -47
@@ -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,
@@ -306,9 +314,26 @@ type Judge = (r: Reading) => readonly Located[];
306
314
  export type TexVenueRuleName =
307
315
  "template" | "required-section" | "venue-leftover";
308
316
 
317
+ /** Every rule this module builds: the `tex` plugin's three and the `format` plugin's one. */
318
+ type RuleName = TexVenueRuleName | "layout-override";
319
+
320
+ /** Each layout override, where it stands. Silent for a preset that names no template. */
321
+ export function judgeLayout(
322
+ overrides: readonly LayoutOverride[],
323
+ preset: Preset,
324
+ ): readonly Located[] {
325
+ const template = preset.template;
326
+ if (template === null) return [];
327
+ return overrides.map((o) => ({
328
+ messageId: "override",
329
+ data: { command: o.command, venue: preset.label, template: template.text },
330
+ at: spanOf(o.place),
331
+ }));
332
+ }
333
+
309
334
  const RULES: Readonly<
310
335
  Record<
311
- TexVenueRuleName,
336
+ RuleName,
312
337
  { readonly judge: Judge; readonly meta: TexRuleModule["meta"] }
313
338
  >
314
339
  > = {
@@ -357,6 +382,22 @@ const RULES: Readonly<
357
382
  },
358
383
  },
359
384
  },
385
+ "layout-override": {
386
+ judge: (r) => judgeLayout(r.latex.layoutOverrides(r.src), r.preset),
387
+ meta: {
388
+ type: "problem",
389
+ docs: {
390
+ description:
391
+ "the source changes the page layout the venue's template sets for the whole document: margins, text block, line spacing",
392
+ url: "https://github.com/zernie/paperlint/blob/main/docs/rules/format/layout-override.md",
393
+ },
394
+ schema: [],
395
+ messages: {
396
+ override:
397
+ "`{{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",
398
+ },
399
+ },
400
+ },
360
401
  "venue-leftover": {
361
402
  judge: (r) =>
362
403
  judgeLeftover(r.latex.renderedRuns(r.src), r.preset, r.others()),
@@ -385,6 +426,13 @@ export const TEX_VENUE_RULE_LEVELS: Readonly<
385
426
  "tex/venue-leftover": "warn",
386
427
  };
387
428
 
429
+ /** The `format` plugin's rule over the source and its level, on for every `paper.tex`. */
430
+ export const FORMAT_RULE_LEVELS: Readonly<
431
+ Record<"format/layout-override", "error">
432
+ > = {
433
+ "format/layout-override": "error",
434
+ };
435
+
388
436
  /**
389
437
  * Every shipped venue but the ones on this paper's own chain, each resolved the way a paper
390
438
  * extending it would be. A preset that does not resolve is skipped: `pdf/profile` owns that.
@@ -430,7 +478,7 @@ function readingOf(
430
478
  };
431
479
  }
432
480
 
433
- function rule(name: TexVenueRuleName, deps: TexVenueRuleDeps): TexRuleModule {
481
+ function rule(name: RuleName, deps: TexVenueRuleDeps): TexRuleModule {
434
482
  const { judge, meta } = RULES[name];
435
483
  return {
436
484
  meta,
@@ -459,3 +507,10 @@ export function texVenueRules(
459
507
  "venue-leftover": rule("venue-leftover", deps),
460
508
  };
461
509
  }
510
+
511
+ /** The `format` plugin's rule over the source (`page-limit`, over the PDF, is in `venue-rules.ts`). */
512
+ export function formatRules(
513
+ deps: TexVenueRuleDeps,
514
+ ): Readonly<Record<"layout-override", TexRuleModule>> {
515
+ return { "layout-override": rule("layout-override", deps) };
516
+ }
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
  }