paperlint 2.0.1 → 2.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 (103) hide show
  1. package/CONTRIBUTING.md +21 -0
  2. package/README.md +56 -14
  3. package/dist/build-engine.d.ts.map +1 -1
  4. package/dist/build-engine.js +5 -2
  5. package/dist/build-engine.js.map +1 -1
  6. package/dist/build.d.ts +1 -1
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +4 -3
  9. package/dist/build.js.map +1 -1
  10. package/dist/cli.d.ts +15 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +138 -24
  13. package/dist/cli.js.map +1 -1
  14. package/dist/doctor.d.ts.map +1 -1
  15. package/dist/doctor.js +21 -4
  16. package/dist/doctor.js.map +1 -1
  17. package/dist/facts-file.d.ts +33 -9
  18. package/dist/facts-file.d.ts.map +1 -1
  19. package/dist/facts-file.js +117 -17
  20. package/dist/facts-file.js.map +1 -1
  21. package/dist/init.d.ts +11 -0
  22. package/dist/init.d.ts.map +1 -1
  23. package/dist/init.js +67 -3
  24. package/dist/init.js.map +1 -1
  25. package/dist/new-paper.d.ts +2 -2
  26. package/dist/new-paper.d.ts.map +1 -1
  27. package/dist/new-paper.js +17 -2
  28. package/dist/new-paper.js.map +1 -1
  29. package/dist/paper-settings.d.ts +68 -0
  30. package/dist/paper-settings.d.ts.map +1 -0
  31. package/dist/paper-settings.js +144 -0
  32. package/dist/paper-settings.js.map +1 -0
  33. package/dist/presets.d.ts +84 -0
  34. package/dist/presets.d.ts.map +1 -0
  35. package/dist/presets.js +186 -0
  36. package/dist/presets.js.map +1 -0
  37. package/dist/rules-config.d.ts +7 -0
  38. package/dist/rules-config.d.ts.map +1 -1
  39. package/dist/rules-config.js +11 -6
  40. package/dist/rules-config.js.map +1 -1
  41. package/dist/structure.d.ts.map +1 -1
  42. package/dist/structure.js +2 -1
  43. package/dist/structure.js.map +1 -1
  44. package/dist/tex-requirements.d.ts +63 -7
  45. package/dist/tex-requirements.d.ts.map +1 -1
  46. package/dist/tex-requirements.js +91 -24
  47. package/dist/tex-requirements.js.map +1 -1
  48. package/dist/venue-rules.d.ts +109 -0
  49. package/dist/venue-rules.d.ts.map +1 -0
  50. package/dist/venue-rules.js +387 -0
  51. package/dist/venue-rules.js.map +1 -0
  52. package/docs/configuration.md +67 -7
  53. package/docs/optional-rules.md +25 -24
  54. package/docs/rules.md +131 -2
  55. package/eslint-rules/papers.mjs +6 -2
  56. package/eslint-rules/pdf-last-page-balance.mjs +23 -2
  57. package/fixtures/build-e2e/acmart/paperlint.json +1 -0
  58. package/lib/paper-config.d.mts +7 -0
  59. package/lib/paper-config.mjs +31 -0
  60. package/package.json +1 -1
  61. package/skills/find-venue/SKILL.md +5 -1
  62. package/skills/find-venue/SKILL.md.spec.ts +4 -0
  63. package/skills/paper-pipeline/PIPELINE-MAP.md +14 -4
  64. package/skills/paper-pipeline/SKILL.md +4 -3
  65. package/skills/paper-pipeline/SKILL.md.spec.ts +3 -2
  66. package/skills/render-paper/extract-pdf-facts.harness.mjs +5 -5
  67. package/skills/render-paper/extract-pdf-facts.mjs +4 -4
  68. package/skills/study-accepted-papers/SKILL.md +2 -1
  69. package/skills/study-accepted-papers/SKILL.md.spec.ts +1 -0
  70. package/skills/submit-paper/SKILL.md +9 -1
  71. package/skills/submit-paper/SKILL.md.spec.ts +8 -0
  72. package/skills/submit-paper/references/venues/acm-sigconf.jsonc +86 -0
  73. package/skills/submit-paper/references/venues/agenticdev.jsonc +34 -102
  74. package/skills/submit-paper/references/venues/aisec.jsonc +21 -82
  75. package/skills/submit-paper/references/venues/realm.jsonc +45 -43
  76. package/skills/submit-paper/references/venues/tex-base.jsonc +1 -1
  77. package/skills/submit-paper/references/venues/venue-profile.schema.json +119 -29
  78. package/src/build-engine.ts +10 -2
  79. package/src/build.harness.mjs +23 -18
  80. package/src/build.ts +5 -4
  81. package/src/cli.harness.mjs +4 -2
  82. package/src/cli.ts +158 -29
  83. package/src/doctor.ts +28 -3
  84. package/src/facts-file.test.ts +54 -7
  85. package/src/facts-file.ts +145 -24
  86. package/src/init.ts +81 -3
  87. package/src/new-paper.harness.mjs +9 -6
  88. package/src/new-paper.test.ts +145 -0
  89. package/src/new-paper.ts +22 -2
  90. package/src/paper-settings-commands.test.ts +311 -0
  91. package/src/paper-settings.test.ts +220 -0
  92. package/src/paper-settings.ts +206 -0
  93. package/src/presets.test.ts +236 -0
  94. package/src/presets.ts +307 -0
  95. package/src/rules-config.ts +12 -7
  96. package/src/structure.harness.mjs +3 -3
  97. package/src/structure.ts +2 -1
  98. package/src/tex-requirements.harness.mjs +11 -17
  99. package/src/tex-requirements.ts +179 -29
  100. package/src/venue-rules.test.ts +545 -0
  101. package/src/venue-rules.ts +573 -0
  102. package/templates/paper/paperlint.json +4 -0
  103. package/fixtures/build-e2e/acmart/venue.json +0 -1
@@ -20,12 +20,13 @@
20
20
  * allowed to know where this package is installed (rule 10).
21
21
  */
22
22
  // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
23
- import { existsSync, readFileSync, readdirSync } from "node:fs";
23
+ import { readFileSync, readdirSync } from "node:fs";
24
24
  import { createRequire } from "node:module";
25
25
  import { join } from "node:path";
26
26
  // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
27
27
  import Ajv from "ajv";
28
28
  import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
29
+ import { PAPER_SETTINGS_FILE } from "../lib/paper-config.mjs";
29
30
 
30
31
  /** CTAN package name → the names that prove it is installed. */
31
32
  export type PackageProofs = Readonly<Record<string, readonly string[]>>;
@@ -78,18 +79,133 @@ function violations(file: string, v: Validate): string[] {
78
79
  );
79
80
  }
80
81
 
82
+ /** One kind of paper a venue takes (`short`, `research`, …) and its page limits; null = not limited. */
83
+ export interface KindLimits {
84
+ readonly bodyPagesMax: number | null;
85
+ readonly refPagesMax: number | null;
86
+ }
87
+
88
+ /**
89
+ * The format a venue's call for papers sets — what the `pdf/*` venue rules judge a built PDF
90
+ * against. Every field the profile leaves out is null, and a rule that finds null checks nothing:
91
+ * a profile that does not name a number never makes one up.
92
+ */
93
+ export interface VenueFormat {
94
+ readonly pageWidthIn: number | null;
95
+ readonly pageHeightIn: number | null;
96
+ readonly columns: number | null;
97
+ readonly bodyPt: number | null;
98
+ readonly bodyPtTol: number | null;
99
+ readonly refPtMin: number | null;
100
+ readonly refPtMax: number | null;
101
+ /** The prefix a font name of the body text starts with (`LinLibertine`). */
102
+ readonly fontsText: string | null;
103
+ readonly fontsTitle: string | null;
104
+ readonly kinds: ReadonlyMap<string, KindLimits>;
105
+ }
106
+
107
+ /** Every format field a preset left out is null; a merge fills it from the parent. */
108
+ export const NO_FORMAT: VenueFormat = {
109
+ pageWidthIn: null,
110
+ pageHeightIn: null,
111
+ columns: null,
112
+ bodyPt: null,
113
+ bodyPtTol: null,
114
+ refPtMin: null,
115
+ refPtMax: null,
116
+ fontsText: null,
117
+ fontsTitle: null,
118
+ kinds: new Map(),
119
+ };
120
+
121
+ /**
122
+ * One preset FILE, parsed — before its `extends` chain is resolved (`src/presets.ts` does that).
123
+ * A preset is a venue or a template family: `{ extends?, name?, template?, format?, tex?, rules? }`.
124
+ */
125
+ export interface PresetFile {
126
+ /** The preset this one builds on: `paperlint:<name>` or a relative path. */
127
+ readonly extends: string | null;
128
+ /** A display name for messages; the file name otherwise. */
129
+ readonly name: string | null;
130
+ readonly template: string | null;
131
+ /** Null when the file declares no `tex` block (allowed only with `extends`). */
132
+ readonly tex: TexRequirements | null;
133
+ readonly format: VenueFormat;
134
+ /** rule id → ESLint entry, checked against the shipped rules where a config is built. */
135
+ readonly rules: Readonly<Record<string, unknown>>;
136
+ }
137
+
138
+ type KindsJson = Readonly<
139
+ Record<string, { body_pages_max?: number; ref_pages_max?: number }>
140
+ >;
141
+
142
+ /** A preset's `format` block after the schema accepted it. */
143
+ interface FormatJson {
144
+ readonly page_w_in?: number;
145
+ readonly page_h_in?: number;
146
+ readonly columns?: number;
147
+ readonly body_pt?: number;
148
+ readonly body_pt_tol?: number;
149
+ readonly ref_pt_min?: number;
150
+ readonly ref_pt_max?: number;
151
+ readonly fonts_text?: string;
152
+ readonly fonts_title?: string;
153
+ readonly kinds?: KindsJson;
154
+ }
155
+
156
+ /** The preset's JSON after the schema accepted it — the shape `venue-profile.schema.json` allows. */
157
+ interface PresetJson {
158
+ readonly extends?: string;
159
+ readonly name?: string;
160
+ readonly template?: string;
161
+ readonly tex?: Partial<TexRequirements>;
162
+ readonly format?: FormatJson;
163
+ readonly rules?: Readonly<Record<string, unknown>>;
164
+ }
165
+
166
+ /** An optional field as the typed preset holds it: absent is null. */
167
+ const orNull = <T>(v: T | undefined): T | null => (v === undefined ? null : v);
168
+
169
+ function kindsOf(k: KindsJson | undefined): ReadonlyMap<string, KindLimits> {
170
+ return new Map(
171
+ Object.entries(k ?? {}).map(([name, v]) => [
172
+ name,
173
+ {
174
+ bodyPagesMax: orNull(v.body_pages_max),
175
+ refPagesMax: orNull(v.ref_pages_max),
176
+ },
177
+ ]),
178
+ );
179
+ }
180
+
181
+ function formatOf(j: FormatJson = {}): VenueFormat {
182
+ return {
183
+ pageWidthIn: orNull(j.page_w_in),
184
+ pageHeightIn: orNull(j.page_h_in),
185
+ columns: orNull(j.columns),
186
+ bodyPt: orNull(j.body_pt),
187
+ bodyPtTol: orNull(j.body_pt_tol),
188
+ refPtMin: orNull(j.ref_pt_min),
189
+ refPtMax: orNull(j.ref_pt_max),
190
+ fontsText: orNull(j.fonts_text),
191
+ fontsTitle: orNull(j.fonts_title),
192
+ kinds: kindsOf(j.kinds),
193
+ };
194
+ }
195
+
81
196
  /**
82
- * The text of one profile → its typed requirements, or an Error naming every problem.
197
+ * The text of one preset file → the typed file, or an Error naming every problem. The ONE parser
198
+ * of a preset: the toolchain reads its `tex`, the venue rules its `format`, the config its `rules`.
83
199
  *
84
200
  * @param text the file's contents
85
201
  * @param file the name to put in messages
86
202
  * @param dir the directory holding the schema (the shipped venues directory by default)
87
203
  */
88
- export function parseProfile(
204
+ export function parsePreset(
89
205
  text: string,
90
206
  file: string,
91
207
  dir: string = packageVenuesDir(),
92
- ): TexRequirements {
208
+ ): PresetFile {
93
209
  const { config, error } = typescript().parseConfigFileTextToJson(file, text);
94
210
  if (error)
95
211
  throw new Error(
@@ -100,8 +216,38 @@ export function parseProfile(
100
216
  throw new Error(
101
217
  `${file} does not match ${SCHEMA_FILE}:\n ${violations(file, validate).join("\n ")}`,
102
218
  );
103
- const tex = (config as { tex: Partial<TexRequirements> }).tex;
104
- return { packages: tex.packages ?? {}, tools: tex.tools ?? {} };
219
+ const j = config as PresetJson;
220
+ return {
221
+ extends: orNull(j.extends),
222
+ name: orNull(j.name),
223
+ template: orNull(j.template),
224
+ tex: j.tex
225
+ ? { packages: j.tex.packages ?? {}, tools: j.tex.tools ?? {} }
226
+ : null,
227
+ format: formatOf(j.format),
228
+ rules: j.rules ?? {},
229
+ };
230
+ }
231
+
232
+ /** The text of one preset → the TeX requirements it declares itself (none without a `tex` block). */
233
+ export function parseProfile(
234
+ text: string,
235
+ file: string,
236
+ dir: string = packageVenuesDir(),
237
+ ): TexRequirements {
238
+ return parsePreset(text, file, dir).tex ?? NO_REQUIREMENTS;
239
+ }
240
+
241
+ /**
242
+ * The file a venue's profile lives in, or null when the name cannot be a venue (the base set is a
243
+ * profile file but not a venue). Whether the file exists is the caller's question.
244
+ */
245
+ export function profileFileOf(venue: string): string | null {
246
+ return venue === BASE_PROFILE.slice(0, -PROFILE_EXT.length) ||
247
+ venue.includes("/") ||
248
+ venue.includes("\\")
249
+ ? null
250
+ : `${venue}${PROFILE_EXT}`;
105
251
  }
106
252
 
107
253
  function readProfile(dir: string, file: string): TexRequirements {
@@ -141,41 +287,45 @@ export function packageNames(tex: TexRequirements): string[] {
141
287
  }
142
288
 
143
289
  /**
144
- * What ONE paper needs: the base set plus its venue's block. A paper with no venue, or a venue this
145
- * package has no profile for, gets the base set — and the source says which, so an ACM paper built
146
- * without `venue.json` is visibly running on the base set rather than silently.
290
+ * What ONE paper needs: the base set plus its venue preset's `tex` (the union over the preset's
291
+ * `extends` chain, resolved by `src/presets.ts`). A paper with no preset gets the base set, and the
292
+ * source says so, so an ACM paper built without a preset is visibly running on the base set.
147
293
  */
148
294
  export function requirementsFor(
149
- venue: string | null,
295
+ preset: { readonly label: string; readonly tex: TexRequirements } | null,
150
296
  dir: string = packageVenuesDir(),
151
297
  ): PaperRequirements {
152
298
  const base = readProfile(dir, BASE_PROFILE);
153
- if (venue === null)
154
- return { source: "the base set (no venue.json)", tex: base };
155
- const file = `${venue}${PROFILE_EXT}`;
156
- if (
157
- venue === BASE_PROFILE.slice(0, -PROFILE_EXT.length) ||
158
- !existsSync(join(dir, file))
159
- )
160
- return {
161
- source: `the base set (venue ${venue} has no profile in paperlint)`,
162
- tex: base,
163
- };
164
- return {
165
- source: `venue ${venue}`,
166
- tex: mergeRequirements(base, readProfile(dir, file)),
167
- };
299
+ return preset === null
300
+ ? {
301
+ source: `the base set (no venue preset in ${PAPER_SETTINGS_FILE})`,
302
+ tex: base,
303
+ }
304
+ : {
305
+ source: `venue ${preset.label}`,
306
+ tex: mergeRequirements(base, preset.tex),
307
+ };
168
308
  }
169
309
 
170
- /** Everything any profile declares — the set `paperlint toolchain` installs, so one tree builds any paper. */
171
- export function declaredUnion(dir: string = packageVenuesDir()): {
310
+ /**
311
+ * Everything any shipped preset declares, plus `extra` — the resolved presets of the project's own
312
+ * papers, which may live outside the package. The set `paperlint toolchain` installs, so one tree
313
+ * builds any paper.
314
+ */
315
+ export function declaredUnion(
316
+ dir: string = packageVenuesDir(),
317
+ extra: readonly TexRequirements[] = [],
318
+ ): {
172
319
  readonly tex: TexRequirements;
173
320
  readonly profiles: number;
174
321
  } {
175
322
  const venues = venueNames(dir);
176
- const tex = venues.reduce(
323
+ const shipped = venues.reduce(
177
324
  (acc, v) => mergeRequirements(acc, readProfile(dir, `${v}${PROFILE_EXT}`)),
178
325
  readProfile(dir, BASE_PROFILE),
179
326
  );
180
- return { tex, profiles: venues.length + 1 };
327
+ return {
328
+ tex: extra.reduce(mergeRequirements, shipped),
329
+ profiles: venues.length + 1,
330
+ };
181
331
  }