paperlint 4.2.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 (251) hide show
  1. package/CLAUDE.md +15 -2
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +23 -12
  4. package/action.harness.mjs +54 -36
  5. package/action.yml +12 -27
  6. package/bin/paperlint.mjs +5 -1
  7. package/dist/adapters/latex/includes.d.ts +5 -1
  8. package/dist/adapters/latex/includes.d.ts.map +1 -1
  9. package/dist/adapters/latex/includes.js +8 -2
  10. package/dist/adapters/latex/includes.js.map +1 -1
  11. package/dist/adapters/latex/index.d.ts +1 -0
  12. package/dist/adapters/latex/index.d.ts.map +1 -1
  13. package/dist/adapters/latex/index.js +3 -0
  14. package/dist/adapters/latex/index.js.map +1 -1
  15. package/dist/adapters/latex/layout.d.ts +27 -0
  16. package/dist/adapters/latex/layout.d.ts.map +1 -0
  17. package/dist/adapters/latex/layout.js +62 -0
  18. package/dist/adapters/latex/layout.js.map +1 -0
  19. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  20. package/dist/adapters/latex/nodes.js +11 -1
  21. package/dist/adapters/latex/nodes.js.map +1 -1
  22. package/dist/adapters/latex/parse.d.ts.map +1 -1
  23. package/dist/adapters/latex/parse.js +5 -1
  24. package/dist/adapters/latex/parse.js.map +1 -1
  25. package/dist/build.d.ts.map +1 -1
  26. package/dist/build.js +7 -3
  27. package/dist/build.js.map +1 -1
  28. package/dist/cli.d.ts +2 -8
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +92 -42
  31. package/dist/cli.js.map +1 -1
  32. package/dist/domain/anonymity.d.ts +68 -0
  33. package/dist/domain/anonymity.d.ts.map +1 -0
  34. package/dist/domain/anonymity.js +94 -0
  35. package/dist/domain/anonymity.js.map +1 -0
  36. package/dist/domain/body-pages.d.ts +69 -0
  37. package/dist/domain/body-pages.d.ts.map +1 -0
  38. package/dist/domain/body-pages.js +141 -0
  39. package/dist/domain/body-pages.js.map +1 -0
  40. package/dist/domain/paper-source.js +2 -2
  41. package/dist/domain/tex-document.d.ts +9 -0
  42. package/dist/domain/tex-document.d.ts.map +1 -1
  43. package/dist/eslint-rules/latex-language.d.ts +5 -1
  44. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  45. package/dist/eslint-rules/latex-language.js +6 -3
  46. package/dist/eslint-rules/latex-language.js.map +1 -1
  47. package/dist/eslint-rules/paper-context.d.ts +20 -0
  48. package/dist/eslint-rules/paper-context.d.ts.map +1 -0
  49. package/dist/eslint-rules/paper-context.js +44 -0
  50. package/dist/eslint-rules/paper-context.js.map +1 -0
  51. package/dist/eslint-rules/paper-typography.d.ts +1 -1
  52. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  53. package/dist/eslint-rules/paper-typography.js +32 -18
  54. package/dist/eslint-rules/paper-typography.js.map +1 -1
  55. package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
  56. package/dist/eslint-rules/pdf-last-page-balance.js +2 -2
  57. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  58. package/dist/eslint-rules/tex-build.js +7 -1
  59. package/dist/eslint-rules/tex-build.js.map +1 -1
  60. package/dist/facts-file.d.ts +28 -2
  61. package/dist/facts-file.d.ts.map +1 -1
  62. package/dist/facts-file.js +54 -9
  63. package/dist/facts-file.js.map +1 -1
  64. package/dist/init.d.ts +4 -19
  65. package/dist/init.d.ts.map +1 -1
  66. package/dist/init.js +24 -29
  67. package/dist/init.js.map +1 -1
  68. package/dist/lib/paper-config.d.ts.map +1 -1
  69. package/dist/lib/paper-config.js +2 -1
  70. package/dist/lib/paper-config.js.map +1 -1
  71. package/dist/package-dirs.d.ts +2 -0
  72. package/dist/package-dirs.d.ts.map +1 -1
  73. package/dist/package-dirs.js +8 -0
  74. package/dist/package-dirs.js.map +1 -1
  75. package/dist/paper-files.d.ts +2 -2
  76. package/dist/paper-includes.d.ts +17 -0
  77. package/dist/paper-includes.d.ts.map +1 -0
  78. package/dist/paper-includes.js +51 -0
  79. package/dist/paper-includes.js.map +1 -0
  80. package/dist/paper-settings.d.ts +11 -0
  81. package/dist/paper-settings.d.ts.map +1 -1
  82. package/dist/paper-settings.js +38 -4
  83. package/dist/paper-settings.js.map +1 -1
  84. package/dist/pdf-content.d.ts +40 -0
  85. package/dist/pdf-content.d.ts.map +1 -0
  86. package/dist/pdf-content.js +97 -0
  87. package/dist/pdf-content.js.map +1 -0
  88. package/dist/pdf-facts.d.ts +30 -0
  89. package/dist/pdf-facts.d.ts.map +1 -1
  90. package/dist/pdf-facts.js +37 -1
  91. package/dist/pdf-facts.js.map +1 -1
  92. package/dist/ports/latex.d.ts +3 -1
  93. package/dist/ports/latex.d.ts.map +1 -1
  94. package/dist/presets.d.ts +2 -0
  95. package/dist/presets.d.ts.map +1 -1
  96. package/dist/presets.js +4 -0
  97. package/dist/presets.js.map +1 -1
  98. package/dist/rules-config.d.ts +1 -0
  99. package/dist/rules-config.d.ts.map +1 -1
  100. package/dist/rules-config.js.map +1 -1
  101. package/dist/structure.js +1 -1
  102. package/dist/structure.js.map +1 -1
  103. package/dist/tex-paper.d.ts +17 -3
  104. package/dist/tex-paper.d.ts.map +1 -1
  105. package/dist/tex-paper.js +36 -38
  106. package/dist/tex-paper.js.map +1 -1
  107. package/dist/tex-requirements.d.ts +4 -0
  108. package/dist/tex-requirements.d.ts.map +1 -1
  109. package/dist/tex-requirements.js +3 -0
  110. package/dist/tex-requirements.js.map +1 -1
  111. package/dist/tex-venue-rules.d.ts +7 -1
  112. package/dist/tex-venue-rules.d.ts.map +1 -1
  113. package/dist/tex-venue-rules.js +40 -0
  114. package/dist/tex-venue-rules.js.map +1 -1
  115. package/dist/types.d.ts +2 -0
  116. package/dist/types.d.ts.map +1 -1
  117. package/dist/venue-rules.d.ts +38 -8
  118. package/dist/venue-rules.d.ts.map +1 -1
  119. package/dist/venue-rules.js +179 -41
  120. package/dist/venue-rules.js.map +1 -1
  121. package/docs/configuration.md +23 -20
  122. package/docs/e2e.md +32 -11
  123. package/docs/install.md +8 -8
  124. package/docs/prior-art/paper-folder-scaffolding.md +4 -7
  125. package/docs/rules/anonymity/identity.md +111 -0
  126. package/docs/rules/format/layout-override.md +61 -0
  127. package/docs/rules/format/page-limit.md +108 -0
  128. package/docs/rules/pdf/body-size.md +87 -0
  129. package/docs/rules.md +55 -46
  130. package/eslint-rules/latex-language.ts +10 -4
  131. package/eslint-rules/paper-context.test.ts +47 -0
  132. package/eslint-rules/paper-context.ts +57 -0
  133. package/eslint-rules/paper-typography.ts +67 -23
  134. package/eslint-rules/pdf-last-page-balance.harness.mjs +6 -1
  135. package/eslint-rules/pdf-last-page-balance.ts +2 -2
  136. package/eslint-rules/tex-build.ts +8 -1
  137. package/fixtures/build-e2e/aidc/expect.json +1 -1
  138. package/fixtures/build-e2e/aidc/paperlint.json +1 -1
  139. package/fixtures/build-e2e/anon-ack/PIPELINE-STATUS.md +3 -0
  140. package/fixtures/build-e2e/anon-ack/expect.json +19 -0
  141. package/fixtures/build-e2e/anon-ack/paper.tex +31 -0
  142. package/fixtures/build-e2e/anon-ack/paperlint.json +1 -0
  143. package/fixtures/build-e2e/anon-author/PIPELINE-STATUS.md +3 -0
  144. package/fixtures/build-e2e/anon-author/expect.json +19 -0
  145. package/fixtures/build-e2e/anon-author/paper.tex +29 -0
  146. package/fixtures/build-e2e/anon-author/paperlint.json +1 -0
  147. package/fixtures/build-e2e/anon-bib/PIPELINE-STATUS.md +3 -0
  148. package/fixtures/build-e2e/anon-bib/expect.json +20 -0
  149. package/fixtures/build-e2e/anon-bib/paper.tex +35 -0
  150. package/fixtures/build-e2e/anon-bib/paperlint.json +1 -0
  151. package/fixtures/build-e2e/anon-href/PIPELINE-STATUS.md +3 -0
  152. package/fixtures/build-e2e/anon-href/expect.json +19 -0
  153. package/fixtures/build-e2e/anon-href/paper.tex +30 -0
  154. package/fixtures/build-e2e/anon-href/paperlint.json +1 -0
  155. package/fixtures/build-e2e/anon-macro/PIPELINE-STATUS.md +3 -0
  156. package/fixtures/build-e2e/anon-macro/expect.json +19 -0
  157. package/fixtures/build-e2e/anon-macro/paper.tex +30 -0
  158. package/fixtures/build-e2e/anon-macro/paperlint.json +1 -0
  159. package/fixtures/build-e2e/anon-pdfauthor/PIPELINE-STATUS.md +3 -0
  160. package/fixtures/build-e2e/anon-pdfauthor/expect.json +19 -0
  161. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +31 -0
  162. package/fixtures/build-e2e/anon-pdfauthor/paperlint.json +1 -0
  163. package/fixtures/build-e2e/unbalanced/expect.json +2 -2
  164. package/fixtures/real-markdown-paper/baseline.json +1 -2
  165. package/lib/paper-config.ts +3 -1
  166. package/package.json +1 -1
  167. package/presets/acm-sigconf.jsonc +3 -1
  168. package/presets/aidc.jsonc +16 -17
  169. package/presets/ieee-conference.jsonc +8 -0
  170. package/presets/venue-profile.schema.json +9 -0
  171. package/scripts/check.ts +2 -2
  172. package/scripts/eslint-report-guard.mjs +5 -2
  173. package/scripts/harness-api.frozen.json +0 -1
  174. package/skills/paper-pipeline/references/pipeline-status-template.md +2 -16
  175. package/skills/render-paper/extract-pdf-facts.harness.ts +3 -3
  176. package/src/adapters/latex/includes.ts +10 -2
  177. package/src/adapters/latex/index.test.ts +8 -0
  178. package/src/adapters/latex/index.ts +3 -0
  179. package/src/adapters/latex/layout.ts +106 -0
  180. package/src/adapters/latex/nodes.ts +11 -3
  181. package/src/adapters/latex/parse.ts +5 -1
  182. package/src/build.harness.mjs +6 -4
  183. package/src/build.test.ts +23 -1
  184. package/src/build.ts +7 -3
  185. package/src/cli.harness.mjs +22 -54
  186. package/src/cli.test.ts +15 -18
  187. package/src/cli.ts +114 -46
  188. package/src/domain/anonymity.test.ts +163 -0
  189. package/src/domain/anonymity.ts +129 -0
  190. package/src/domain/body-pages.test.ts +300 -0
  191. package/src/domain/body-pages.ts +223 -0
  192. package/src/domain/paper-source.ts +2 -2
  193. package/src/domain/tex-document.ts +10 -0
  194. package/src/facts-file.test.ts +100 -4
  195. package/src/facts-file.ts +91 -9
  196. package/src/init.test.ts +33 -13
  197. package/src/init.ts +25 -48
  198. package/src/linted-set.test.ts +11 -1
  199. package/src/new-paper.harness.mjs +1 -5
  200. package/src/new-paper.test.ts +7 -1
  201. package/src/package-dirs.ts +9 -0
  202. package/src/paper-files.ts +2 -2
  203. package/src/paper-includes.test.ts +161 -0
  204. package/src/paper-includes.ts +77 -0
  205. package/src/paper-settings.test.ts +63 -6
  206. package/src/paper-settings.ts +53 -5
  207. package/src/pdf-content.test.ts +71 -0
  208. package/src/pdf-content.ts +125 -0
  209. package/src/pdf-facts.test.ts +110 -2
  210. package/src/pdf-facts.ts +106 -8
  211. package/src/ports/latex.ts +3 -0
  212. package/src/presets.ts +8 -8
  213. package/src/rule-cases.test.ts +29 -33
  214. package/src/rule-docs.test.ts +0 -3
  215. package/src/rules-config.ts +1 -0
  216. package/src/structure.harness.ts +1 -3
  217. package/src/structure.ts +1 -1
  218. package/src/tex-paper.test.ts +78 -61
  219. package/src/tex-paper.ts +57 -48
  220. package/src/tex-requirements.ts +9 -0
  221. package/src/tex-venue-rules.test.ts +111 -6
  222. package/src/tex-venue-rules.ts +57 -2
  223. package/src/types.ts +2 -0
  224. package/src/venue-rules.test.ts +351 -39
  225. package/src/venue-rules.ts +247 -47
  226. package/templates/paper/PIPELINE-STATUS.md +0 -4
  227. package/dist/action-ref.d.ts +0 -12
  228. package/dist/action-ref.d.ts.map +0 -1
  229. package/dist/action-ref.js +0 -16
  230. package/dist/action-ref.js.map +0 -1
  231. package/dist/eslint-rules/paper-research-question.d.ts +0 -31
  232. package/dist/eslint-rules/paper-research-question.d.ts.map +0 -1
  233. package/dist/eslint-rules/paper-research-question.js +0 -188
  234. package/dist/eslint-rules/paper-research-question.js.map +0 -1
  235. package/docs/rules/tex/missing-input.md +0 -58
  236. package/eslint-rules/paper-research-question.harness.mjs +0 -212
  237. package/eslint-rules/paper-research-question.ts +0 -199
  238. package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +0 -9
  239. package/fixtures/paper-research-question/comment-only/paper.tex +0 -7
  240. package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +0 -10
  241. package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +0 -6
  242. package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +0 -6
  243. package/fixtures/paper-research-question/draft/paper.tex +0 -2
  244. package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +0 -9
  245. package/fixtures/paper-research-question/markdown-no-rq/paper.md +0 -4
  246. package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +0 -12
  247. package/fixtures/paper-research-question/shipped-no-rq/paper.tex +0 -3
  248. package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +0 -10
  249. package/fixtures/paper-research-question/shipped-with-rq/paper.tex +0 -2
  250. package/src/action-ref.test.ts +0 -26
  251. package/src/action-ref.ts +0 -15
package/src/tex-paper.ts CHANGED
@@ -7,11 +7,9 @@
7
7
  * file at the include in `paper.tex` that brought it in — the one place ESLint can point to in this
8
8
  * file — with the included file, line and column at the front of the message.
9
9
  *
10
- * tex/missing-input warn an `\input`, `\include` or `\subfile` names a file that is not there
11
- *
12
- * A missing file is a finding, not a silent skip: the rules would otherwise report a clean body for
13
- * text they never read, and TeX stops on it too. Warn, not error: a file a build step writes (a
14
- * table of generated numbers) is legitimately absent before that step has run.
10
+ * An include is looked for where the build tells TeX to look (`texSearchPath`): the paper's own
11
+ * directory, then paperlint's inputs. `bodyFiles` names the files of the paper's BODY the author
12
+ * wrote — what the rules over ESLint's own LaTeX text read one by one, each at its own path.
15
13
  */
16
14
  import { basename, dirname, join } from "node:path";
17
15
  import {
@@ -23,11 +21,9 @@ import type { Span } from "./domain/tex-document.ts";
23
21
  import type { LatexReader } from "./ports/latex.ts";
24
22
  import type { Files } from "./ports/files.ts";
25
23
  import { callerPath } from "./caller-path.ts";
26
- import type {
27
- Located,
28
- TexRuleContext,
29
- TexRuleModule,
30
- } from "./tex-venue-rules.ts";
24
+ import type { AbsolutePath } from "./domain/paths.ts";
25
+ import { texSearchPath } from "./package-dirs.ts";
26
+ import type { Located, TexRuleContext } from "./tex-venue-rules.ts";
31
27
 
32
28
  /** What reading a paper needs: the disk, and the LaTeX reader. */
33
29
  export interface PaperDeps {
@@ -46,12 +42,62 @@ export function readPaper(
46
42
  includes: deps.latex.includes,
47
43
  documentBody: deps.latex.documentBody,
48
44
  read: (rel) => {
49
- const b = deps.files.readBytes(callerPath(join(dir, rel)));
45
+ const at = located(dir, rel, deps.files);
46
+ const b = at === null ? null : deps.files.readBytes(at);
50
47
  return b === null ? null : new TextDecoder().decode(b);
51
48
  },
52
49
  });
53
50
  }
54
51
 
52
+ /** `rel` in the first directory of the paper's TeX search path that holds it, or null. */
53
+ const located = (dir: string, rel: string, files: Files): AbsolutePath | null =>
54
+ texSearchPath(dir)
55
+ .map((d) => callerPath(join(d, rel)))
56
+ .find((p) => files.isFile(p)) ?? null;
57
+
58
+ /** An include that resolved nowhere: the file that wrote it, and the path as written. */
59
+ export interface Unread {
60
+ readonly file: string;
61
+ readonly target: string;
62
+ }
63
+
64
+ /**
65
+ * The files of the body of the paper whose main file is `filename`: every file an include inside its
66
+ * `document` environment brings in, nested ones too, found in the paper's own directory. Not a
67
+ * preamble include (macros are not the body), and not a file found only in paperlint's inputs (not
68
+ * the author's text). `missing` is every include, anywhere, that resolved nowhere.
69
+ */
70
+ export function bodyFiles(
71
+ filename: string,
72
+ src: string,
73
+ deps: PaperDeps,
74
+ ): {
75
+ readonly files: readonly AbsolutePath[];
76
+ readonly missing: readonly Unread[];
77
+ } {
78
+ const dir = dirname(filename);
79
+ const paper = readPaper(filename, src, deps);
80
+ const body = deps.latex.documentBody(src);
81
+ const inBody = (via: Span | null): boolean =>
82
+ via !== null &&
83
+ (body === null || (via.start >= body.start && via.end <= body.end));
84
+ const own = (rel: string): AbsolutePath | null => {
85
+ const at = located(dir, rel, deps.files);
86
+ return at === callerPath(join(dir, rel)) ? at : null;
87
+ };
88
+ return {
89
+ files: [
90
+ ...new Set(
91
+ paper.segments
92
+ .filter((s) => s.file !== paper.main && inBody(s.via))
93
+ .map((s) => own(s.file))
94
+ .filter((f): f is AbsolutePath => f !== null),
95
+ ),
96
+ ],
97
+ missing: paper.missing.map(({ file, target }) => ({ file, target })),
98
+ };
99
+ }
100
+
55
101
  /** Line and column (1-based) of an offset in a text. */
56
102
  function lineColumn(text: string, at: number): string {
57
103
  const line = text.slice(0, at).split("\n").length;
@@ -113,40 +159,3 @@ export function reportInPaper(
113
159
  });
114
160
  });
115
161
  }
116
-
117
- const MISSING_META: TexRuleModule["meta"] = {
118
- type: "problem",
119
- docs: {
120
- description:
121
- "an \\input, \\include or \\subfile names a file that is not there, so no rule read its text",
122
- url: "https://github.com/zernie/paperlint/blob/main/docs/rules/tex/missing-input.md",
123
- },
124
- schema: [],
125
- messages: {
126
- missing:
127
- "`{{target}}` is not there (tried `{{target}}.tex` and `{{target}}`, from the paper's directory): TeX stops on it, and no rule read what it should hold",
128
- missingIn:
129
- "`{{target}}`, which {{file}} includes, is not there (tried `{{target}}.tex` and `{{target}}`, from the paper's directory): TeX stops on it, and no rule read what it should hold",
130
- },
131
- };
132
-
133
- /** The includes that name no file, each reported where it stands in the main file. */
134
- export const missingInputRule = (deps: PaperDeps): TexRuleModule => ({
135
- meta: MISSING_META,
136
- create(context) {
137
- return {
138
- root() {
139
- const sc = context.sourceCode;
140
- const paper = readPaper(context.filename, sc.raw ?? sc.text, deps);
141
- const loc = (i: number) => sc.getLocFromIndex(i);
142
- paper.missing.forEach((m) => {
143
- context.report({
144
- loc: { start: loc(m.via.start), end: loc(m.via.end) },
145
- messageId: m.file === paper.main ? "missing" : "missingIn",
146
- data: { target: m.target, file: m.file },
147
- });
148
- });
149
- },
150
- };
151
- },
152
- });
@@ -131,6 +131,8 @@ export interface VenueFormat {
131
131
  /** The prefix a font name of the body text starts with (`LinLibertine`). */
132
132
  readonly fontsText: string | null;
133
133
  readonly fontsTitle: string | null;
134
+ /** How the body is counted: null is banal's count; `references`, the pages before the references. */
135
+ readonly bodyEndsAt: "references" | null;
134
136
  readonly kinds: ReadonlyMap<string, KindLimits>;
135
137
  }
136
138
 
@@ -145,6 +147,7 @@ export const NO_FORMAT: VenueFormat = {
145
147
  refPtMax: null,
146
148
  fontsText: null,
147
149
  fontsTitle: null,
150
+ bodyEndsAt: null,
148
151
  kinds: new Map(),
149
152
  };
150
153
 
@@ -168,6 +171,8 @@ export interface PresetFile {
168
171
  readonly template: string | null;
169
172
  /** Other names the venue goes by in a paper's text; empty when the file names none. */
170
173
  readonly aliases: readonly string[];
174
+ /** Whether the venue reviews double-blind; null when the file does not say. */
175
+ readonly blind: boolean | null;
171
176
  /** Sections the venue requires; null when the file names none (a child's list replaces its parent's). */
172
177
  readonly requiredSections: readonly RequiredSection[] | null;
173
178
  /** Null when the file declares no `tex` block (allowed only with `extends`). */
@@ -192,6 +197,7 @@ interface FormatJson {
192
197
  readonly ref_pt_max?: number;
193
198
  readonly fonts_text?: string;
194
199
  readonly fonts_title?: string;
200
+ readonly body_ends_at?: "references";
195
201
  readonly kinds?: KindsJson;
196
202
  }
197
203
 
@@ -201,6 +207,7 @@ interface PresetJson {
201
207
  readonly name?: string;
202
208
  readonly template?: string;
203
209
  readonly aliases?: readonly string[];
210
+ readonly blind?: boolean;
204
211
  readonly required_sections?: readonly {
205
212
  readonly title: string;
206
213
  readonly position?: "last";
@@ -238,6 +245,7 @@ function formatOf(j: FormatJson = {}): VenueFormat {
238
245
  refPtMax: orNull(j.ref_pt_max),
239
246
  fontsText: orNull(j.fonts_text),
240
247
  fontsTitle: orNull(j.fonts_title),
248
+ bodyEndsAt: orNull(j.body_ends_at),
241
249
  kinds: kindsOf(j.kinds),
242
250
  };
243
251
  }
@@ -281,6 +289,7 @@ export function parsePreset(
281
289
  name: orNull(j.name),
282
290
  template: orNull(j.template),
283
291
  aliases: j.aliases ?? [],
292
+ blind: orNull(j.blind),
284
293
  requiredSections: sectionsOf(j.required_sections),
285
294
  tex: j.tex ? { packages: j.tex.packages, tools: j.tex.tools ?? {} } : null,
286
295
  format: formatOf(j.format),
@@ -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:
@@ -199,7 +207,7 @@ describe("the venue rules read the whole paper: the files it \\inputs, spliced w
199
207
  "\\section*{LLM Usage Statement}\nNone.\n",
200
208
  };
201
209
  expect(lint(tex, AIDC, { extra })).toEqual([]);
202
- // Without the file, the section is missing — and tex/missing-input (not a venue rule) says why.
210
+ // Without the file, the section is missing.
203
211
  expect(ids(lint(tex, AIDC))).toEqual(["tex/required-section:missing"]);
204
212
  });
205
213
 
@@ -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
  }