paperlint 6.3.0 → 7.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 (185) hide show
  1. package/CONTRIBUTING.md +4 -2
  2. package/README.md +20 -13
  3. package/dist/adapters/latex/conditionals.d.ts +21 -0
  4. package/dist/adapters/latex/conditionals.d.ts.map +1 -0
  5. package/dist/adapters/latex/conditionals.js +75 -0
  6. package/dist/adapters/latex/conditionals.js.map +1 -0
  7. package/dist/adapters/latex/headings.d.ts +10 -0
  8. package/dist/adapters/latex/headings.d.ts.map +1 -0
  9. package/dist/adapters/latex/headings.js +129 -0
  10. package/dist/adapters/latex/headings.js.map +1 -0
  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/parse.d.ts +1 -1
  16. package/dist/adapters/latex/parse.d.ts.map +1 -1
  17. package/dist/adapters/latex/parse.js +3 -2
  18. package/dist/adapters/latex/parse.js.map +1 -1
  19. package/dist/build.d.ts +1 -1
  20. package/dist/build.d.ts.map +1 -1
  21. package/dist/build.js +1 -1
  22. package/dist/build.js.map +1 -1
  23. package/dist/cli.d.ts +1 -1
  24. package/dist/cli.d.ts.map +1 -1
  25. package/dist/cli.js +19 -5
  26. package/dist/cli.js.map +1 -1
  27. package/dist/domain/body-pages.d.ts +29 -0
  28. package/dist/domain/body-pages.d.ts.map +1 -1
  29. package/dist/domain/body-pages.js +25 -0
  30. package/dist/domain/body-pages.js.map +1 -1
  31. package/dist/domain/heading-case.d.ts +100 -0
  32. package/dist/domain/heading-case.d.ts.map +1 -0
  33. package/dist/domain/heading-case.js +365 -0
  34. package/dist/domain/heading-case.js.map +1 -0
  35. package/dist/domain/tex-document.d.ts +32 -0
  36. package/dist/domain/tex-document.d.ts.map +1 -1
  37. package/dist/domain/tex-document.js +11 -0
  38. package/dist/domain/tex-document.js.map +1 -1
  39. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  40. package/dist/eslint-rules/latex-language.js +16 -3
  41. package/dist/eslint-rules/latex-language.js.map +1 -1
  42. package/dist/facts-file.d.ts +3 -3
  43. package/dist/facts-file.d.ts.map +1 -1
  44. package/dist/facts-file.js +3 -3
  45. package/dist/facts-file.js.map +1 -1
  46. package/dist/folder-venue-rule.js +6 -6
  47. package/dist/folder-venue-rule.js.map +1 -1
  48. package/dist/heading-case.d.ts +48 -0
  49. package/dist/heading-case.d.ts.map +1 -0
  50. package/dist/heading-case.js +191 -0
  51. package/dist/heading-case.js.map +1 -0
  52. package/dist/paper-includes.d.ts +3 -0
  53. package/dist/paper-includes.d.ts.map +1 -1
  54. package/dist/paper-includes.js +17 -2
  55. package/dist/paper-includes.js.map +1 -1
  56. package/dist/ports/latex.d.ts +7 -1
  57. package/dist/ports/latex.d.ts.map +1 -1
  58. package/dist/preset-card-rule.d.ts +100 -0
  59. package/dist/preset-card-rule.d.ts.map +1 -0
  60. package/dist/preset-card-rule.js +221 -0
  61. package/dist/preset-card-rule.js.map +1 -0
  62. package/dist/presets.d.ts +29 -5
  63. package/dist/presets.d.ts.map +1 -1
  64. package/dist/presets.js +104 -68
  65. package/dist/presets.js.map +1 -1
  66. package/dist/submission.d.ts +8 -0
  67. package/dist/submission.d.ts.map +1 -1
  68. package/dist/submission.js +19 -7
  69. package/dist/submission.js.map +1 -1
  70. package/dist/tex-requirements.d.ts +42 -3
  71. package/dist/tex-requirements.d.ts.map +1 -1
  72. package/dist/tex-requirements.js +34 -5
  73. package/dist/tex-requirements.js.map +1 -1
  74. package/dist/tex-venue-rules.d.ts +6 -3
  75. package/dist/tex-venue-rules.d.ts.map +1 -1
  76. package/dist/tex-venue-rules.js +29 -10
  77. package/dist/tex-venue-rules.js.map +1 -1
  78. package/dist/venue-rules.d.ts +2 -1
  79. package/dist/venue-rules.d.ts.map +1 -1
  80. package/dist/venue-rules.js +55 -7
  81. package/dist/venue-rules.js.map +1 -1
  82. package/docs/configuration.md +13 -1
  83. package/docs/e2e.md +14 -12
  84. package/docs/optional-rules.md +4 -3
  85. package/docs/rules/format/page-limit.md +19 -2
  86. package/docs/rules/paper/folder-venue-leftover.md +1 -1
  87. package/docs/rules/talk/profile.md +1 -1
  88. package/docs/rules/tex/heading-case.md +424 -0
  89. package/docs/rules/tex/template.md +25 -5
  90. package/docs/rules/tex/venue-leftover.md +13 -0
  91. package/docs/rules.md +74 -57
  92. package/docs/submission.md +5 -1
  93. package/eslint-rules/latex-language.test.mjs +12 -0
  94. package/eslint-rules/latex-language.ts +17 -4
  95. package/eslint-rules/paper-typography.test.mjs +12 -0
  96. package/eslint.config.mjs +59 -1
  97. package/fixtures/build-e2e/acmart/paper.tex +1 -1
  98. package/fixtures/build-e2e/fallback/expect.json +3 -3
  99. package/fixtures/build-e2e/msr/PIPELINE-STATUS.md +3 -0
  100. package/fixtures/build-e2e/msr/expect.json +18 -0
  101. package/fixtures/build-e2e/msr/paper.tex +49 -0
  102. package/fixtures/build-e2e/msr/paperlint.json +1 -0
  103. package/fixtures/build-e2e/msr-appendix-after/PIPELINE-STATUS.md +3 -0
  104. package/fixtures/build-e2e/msr-appendix-after/expect.json +20 -0
  105. package/fixtures/build-e2e/msr-appendix-after/paper.tex +48 -0
  106. package/fixtures/build-e2e/msr-appendix-after/paperlint.json +1 -0
  107. package/lib/skill-checks.mjs +31 -0
  108. package/package.json +7 -1
  109. package/presets/acm-sigconf.jsonc +5 -0
  110. package/presets/acm-sigconf.md +43 -0
  111. package/presets/agenticdev.jsonc +27 -6
  112. package/presets/agenticdev.md +151 -0
  113. package/presets/aidc.jsonc +6 -0
  114. package/presets/aidc.md +75 -0
  115. package/presets/aisec.jsonc +5 -1
  116. package/presets/aisec.md +117 -0
  117. package/presets/ieee-conference.jsonc +5 -0
  118. package/presets/ieee-conference.md +38 -0
  119. package/presets/msr.jsonc +71 -0
  120. package/presets/msr.md +94 -0
  121. package/presets/realm.jsonc +7 -2
  122. package/presets/realm.md +166 -0
  123. package/presets/tex/agenticdev.tex +1 -1
  124. package/presets/tex-base.jsonc +6 -0
  125. package/presets/tex-base.md +30 -0
  126. package/presets/venue-profile.schema.json +65 -1
  127. package/skills/camera-ready/SKILL.md +8 -1
  128. package/skills/camera-ready/SKILL.md.spec.ts +7 -0
  129. package/skills/find-venue/SKILL.md +6 -5
  130. package/skills/find-venue/SKILL.md.spec.ts +5 -4
  131. package/skills/paper-pipeline/SKILL.md +5 -6
  132. package/skills/paper-pipeline/SKILL.md.spec.ts +4 -5
  133. package/skills/paper-pipeline/references/acl-venue-rules.md +1 -1
  134. package/skills/study-accepted-papers/SKILL.md +3 -3
  135. package/skills/study-accepted-papers/SKILL.md.spec.ts +2 -2
  136. package/skills/submission-portal/SKILL.md +2 -2
  137. package/skills/submission-portal/SKILL.md.spec.ts +1 -1
  138. package/skills/submit-paper/SKILL.md +34 -22
  139. package/skills/submit-paper/SKILL.md.spec.ts +33 -21
  140. package/skills/submit-paper/references/publishers/acm.md +1 -1
  141. package/src/adapters/latex/conditionals.ts +130 -0
  142. package/src/adapters/latex/headings.ts +163 -0
  143. package/src/adapters/latex/index.test.ts +65 -0
  144. package/src/adapters/latex/index.ts +3 -0
  145. package/src/adapters/latex/parse.ts +3 -2
  146. package/src/build.ts +2 -2
  147. package/src/cli.test.ts +50 -7
  148. package/src/cli.ts +28 -6
  149. package/src/domain/body-pages.test.ts +144 -0
  150. package/src/domain/body-pages.ts +61 -1
  151. package/src/domain/heading-case.test.ts +660 -0
  152. package/src/domain/heading-case.ts +538 -0
  153. package/src/domain/tex-document.ts +36 -0
  154. package/src/domain/venue-name.test.ts +13 -7
  155. package/src/facts-file.test.ts +4 -4
  156. package/src/facts-file.ts +6 -6
  157. package/src/folder-venue-rule.test.ts +10 -10
  158. package/src/folder-venue-rule.ts +6 -6
  159. package/src/heading-case.test.ts +500 -0
  160. package/src/heading-case.ts +275 -0
  161. package/src/init.test.ts +1 -1
  162. package/src/new-paper.test.ts +37 -6
  163. package/src/paper-includes.test.ts +26 -0
  164. package/src/paper-includes.ts +20 -2
  165. package/src/paper-settings-commands.test.ts +26 -11
  166. package/src/ports/latex.ts +7 -0
  167. package/src/preset-card-rule.test.ts +267 -0
  168. package/src/preset-card-rule.ts +384 -0
  169. package/src/presets.test.ts +259 -41
  170. package/src/presets.ts +161 -75
  171. package/src/rule-cases.test.ts +136 -50
  172. package/src/submission.test.ts +44 -4
  173. package/src/submission.ts +28 -7
  174. package/src/talk-rules.test.ts +6 -3
  175. package/src/tex-requirements.harness.ts +25 -11
  176. package/src/tex-requirements.test.ts +127 -1
  177. package/src/tex-requirements.ts +91 -12
  178. package/src/tex-venue-rules.test.ts +196 -21
  179. package/src/tex-venue-rules.ts +47 -16
  180. package/src/venue-rules.test.ts +180 -8
  181. package/src/venue-rules.ts +71 -10
  182. package/templates/paper/paper.tex +1 -1
  183. package/skills/submit-paper/references/venues/agenticdev.md +0 -171
  184. package/skills/submit-paper/references/venues/aisec.md +0 -105
  185. package/skills/submit-paper/references/venues/realm.md +0 -155
@@ -18,7 +18,7 @@ import { z } from "zod";
18
18
  import { run, rulePlugins } from "./cli.ts";
19
19
  import { sha256Hex } from "./domain/sha256.ts";
20
20
  import { bibHash } from "./references.ts";
21
- import { useTempDir, writeTree } from "../test/support.ts";
21
+ import { useTempDir, venuePreset, writeTree } from "../test/support.ts";
22
22
 
23
23
  const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
24
24
  const fixture = (p: string): string =>
@@ -39,19 +39,87 @@ const paper = (
39
39
  ...extra,
40
40
  });
41
41
 
42
+ // ── the venues: presets of the tree's own, never the shipped ones ──────────────────────────────
43
+ //
44
+ // A case judges a RULE, so every number it is sized against is written here, beside the facts and
45
+ // the bodies that meet or miss it. A shipped venue preset changes when its call for papers does;
46
+ // its own tests resolve and judge it (presets.test.ts, venue-rules.test.ts, tex-venue-rules.test.ts,
47
+ // register-bands.test.ts, heading-case.test.ts). The one exception is the leftover rules, below:
48
+ // they name SHIPPED venues by design, so their reporting cases need one shipped venue's name.
49
+ // The venues below extend a template family only for what the schema asks of a chain's root (its
50
+ // `tex`), and set every key a case reads themselves.
51
+
52
+ /** A short-paper venue on the ACM template: the format the facts of `goodFacts` meet. */
53
+ const EXAMPLECONF = venuePreset("ExampleConf", {
54
+ extends: "paperlint:acm-sigconf",
55
+ format: {
56
+ page_w_in: 8.5,
57
+ page_h_in: 11,
58
+ columns: 2,
59
+ body_pt: 9,
60
+ body_pt_tol: 0.5,
61
+ ref_pt_min: 7,
62
+ ref_pt_max: 9,
63
+ fonts_text: "LinLibertine",
64
+ fonts_title: "LinBiolinum",
65
+ kinds: { short: { body_pages_max: 5, ref_pages_max: 2 } },
66
+ },
67
+ // chicago-headline for the title and every heading (tex/heading-case).
68
+ rules: {
69
+ "tex/heading-case": [
70
+ "error",
71
+ { title: "chicago-headline", headings: "chicago-headline" },
72
+ ],
73
+ },
74
+ });
75
+ const IEEE = "\\documentclass[conference,compsoc]{IEEEtran}";
76
+ const STATEMENT_TITLE = "LLM Usage Statement";
77
+ /**
78
+ * A double-blind venue on the IEEE template: the class options it requires, the section it requires
79
+ * last, and one accepted paper of 10,000 words for the register bands. Its rates (contrast frames 4,
80
+ * claims in bold 4, relation markers 64 per 10,000 words), widened by two Poisson standard
81
+ * deviations, set the bands 0–8, 0–8 and 48–80.
82
+ */
83
+ const BLINDCONF = venuePreset("BlindConf", {
84
+ extends: "paperlint:ieee-conference",
85
+ blind: true,
86
+ template: IEEE,
87
+ required_sections: [{ title: STATEMENT_TITLE, position: "last" }],
88
+ register: {
89
+ anchors: [
90
+ {
91
+ paper: "an-accepted-paper",
92
+ words: 10_000,
93
+ contrast_frames: 4,
94
+ claim_emphasis: 4,
95
+ relation_markers: 64,
96
+ },
97
+ ],
98
+ },
99
+ });
100
+ /** The venue presets every built paper carries beside it, extended or not. */
101
+ const VENUES = {
102
+ [`${P}/exampleconf.jsonc`]: JSON.stringify(EXAMPLECONF),
103
+ [`${P}/blindconf.jsonc`]: JSON.stringify(BLINDCONF),
104
+ };
105
+ /** A paper folder on BlindConf: the preset beside its `paperlint.json`. */
106
+ const onBlindConf = (dir: string): Record<string, string> => ({
107
+ [`${dir}/blindconf.jsonc`]: JSON.stringify(BLINDCONF),
108
+ [`${dir}/paperlint.json`]: JSON.stringify({ extends: "./blindconf.jsonc" }),
109
+ });
110
+
42
111
  // ── the venue rules' inputs: a PDF and the facts `paperlint build` would write about it ─────────
43
112
 
44
113
  const PDF = "%PDF-1.5 a stand-in for the built PDF";
45
- const AGENTICDEV = { extends: "paperlint:agenticdev", kind: "short" };
114
+ const EXAMPLE = { extends: "./exampleconf.jsonc", kind: "short" };
46
115
  /** A double-blind venue's paper that declares who wrote it (`anonymity/*`). */
47
116
  const BLIND = {
48
- extends: "paperlint:aidc",
49
- kind: "regular",
117
+ extends: "./blindconf.jsonc",
50
118
  identity: ["Ada Example", "adaexample"],
51
119
  };
52
120
  const ACM_TEX = tex("Text.", "\\documentclass[sigconf]{acmart}");
53
121
 
54
- /** The facts of a short agenticdev paper that meets every number in its preset. */
122
+ /** The facts of a short ExampleConf paper that meets every number in its preset. */
55
123
  const goodFacts = (): Record<string, unknown> => ({
56
124
  schema: 3,
57
125
  pages_text: [],
@@ -61,7 +129,7 @@ const goodFacts = (): Record<string, unknown> => ({
61
129
  appendix_anchor_page: null,
62
130
  pdf: "paper.pdf",
63
131
  pdf_sha256: sha256Hex(new TextEncoder().encode(PDF)),
64
- venue: "agenticdev",
132
+ venue: "exampleconf",
65
133
  kind: "short",
66
134
  npages: 7,
67
135
  fonts_source: "pdfjs-drawn",
@@ -85,12 +153,13 @@ const goodFacts = (): Record<string, unknown> => ({
85
153
  pages_by_type: { body: 5, bib: 2 },
86
154
  });
87
155
 
88
- /** An agenticdev paper, built: its settings, the PDF, and facts with `patch` applied. */
156
+ /** An ExampleConf paper, built: its settings, the PDF, and facts with `patch` applied. */
89
157
  const built = (
90
158
  patch: Record<string, unknown> = {},
91
- settings: Record<string, unknown> = AGENTICDEV,
159
+ settings: Record<string, unknown> = EXAMPLE,
92
160
  ): Record<string, string> =>
93
161
  paper(ACM_TEX, {
162
+ ...VENUES,
94
163
  [`${P}/paperlint.json`]: JSON.stringify(settings),
95
164
  [`${P}/paper.pdf`]: PDF,
96
165
  [`${P}/_build/paper.facts.json`]: JSON.stringify({
@@ -129,23 +198,26 @@ const checked = (exists: string, authors: string) => ({
129
198
  entries: [{ key: "a", exists, authors }],
130
199
  });
131
200
 
132
- // ── an AIDC paper, for the LaTeX venue rules ──────────────────────────────────────────────────
201
+ // ── a BlindConf paper, for the LaTeX venue rules ──────────────────────────────────────────────
133
202
 
134
- const AIDC = {
135
- [`${P}/paperlint.json`]: JSON.stringify({
136
- extends: "paperlint:aidc",
137
- kind: "short",
138
- }),
203
+ const BLIND_PAPER = onBlindConf(P);
204
+ const ACM_CLASS = "\\documentclass[sigconf]{acmart}";
205
+ /** A paper on ExampleConf's preset: it turns `tex/heading-case` on, chicago-headline for the title and every heading. */
206
+ const ACM_PRESET = {
207
+ ...VENUES,
208
+ [`${P}/paperlint.json`]: JSON.stringify(EXAMPLE),
139
209
  };
140
- const IEEE = "\\documentclass[conference,compsoc]{IEEEtran}";
141
- const STATEMENT = "\\section*{LLM Usage Statement}\nNone.";
210
+ const STATEMENT = `\\section*{${STATEMENT_TITLE}}\nNone.`;
142
211
  /** Two `\\documentclass` lines behind a switch: `article` for one venue, `second` for this one. */
143
212
  const switched = (second: string): string =>
144
213
  `\\def\\venue{2}\n\\if\\venue1\n\\documentclass{article}\n\\fi\n\\if\\venue2\n${second}\n\\fi`;
145
- const aidc = (body: string, cls = IEEE) =>
146
- paper(tex(`\\section{Introduction}\n${body}\n${STATEMENT}`, cls), AIDC);
214
+ const blind = (body: string, cls = IEEE) =>
215
+ paper(
216
+ tex(`\\section{Introduction}\n${body}\n${STATEMENT}`, cls),
217
+ BLIND_PAPER,
218
+ );
147
219
 
148
- // ── a paper that declares a talk: a preset of its own over agenticdev, and synthetic media ───────
220
+ // ── a paper that declares a talk: a preset of its own over ExampleConf, and synthetic media ──────
149
221
 
150
222
  const media = (name: string): Uint8Array =>
151
223
  readFileSync(join(ROOT, "fixtures", "talk", name));
@@ -173,10 +245,13 @@ const talkPaper = (
173
245
  };
174
246
  return {
175
247
  ...paper(ACM_TEX, {
176
- [`${P}/talk-venue.jsonc`]: JSON.stringify({
177
- extends: "paperlint:agenticdev",
178
- talk: { ...TALK_VENUE, ...venue },
179
- }),
248
+ ...VENUES,
249
+ [`${P}/talk-venue.jsonc`]: JSON.stringify(
250
+ venuePreset("talk-venue", {
251
+ extends: "./exampleconf.jsonc",
252
+ talk: { ...TALK_VENUE, ...venue },
253
+ }),
254
+ ),
180
255
  [`${P}/paperlint.json`]: JSON.stringify({
181
256
  extends: "./talk-venue.jsonc",
182
257
  kind: "short",
@@ -226,7 +301,7 @@ const FORMAL = Array.from(
226
301
  /** FORMAL with `n` more sentences `s`: a body whose rate of one register measure is set. */
227
302
  const formalWith = (n: number, s: string): string =>
228
303
  `${FORMAL} ${Array.from({ length: n }, () => s).join(" ")}`;
229
- /** FORMAL with enough relation markers for AIDC's band (above 25.4 per 10,000 words). */
304
+ /** FORMAL with 20 relation markers, 68.7 per 10,000 words: inside BlindConf's band (48–80). */
230
305
  const LINKED = formalWith(
231
306
  10,
232
307
  "Therefore, the rule counts the sentence because the body holds it.",
@@ -373,49 +448,60 @@ const CASES: Readonly<Record<string, RuleCases>> = {
373
448
  // The class picked behind a TeX switch: two candidates, one of them the venue's or none.
374
449
  "tex/template": {
375
450
  reports: {
376
- tree: aidc("Text.", switched("\\documentclass[conference]{IEEEtran}")),
451
+ tree: blind("Text.", switched("\\documentclass[conference]{IEEEtran}")),
377
452
  file: TEX_FILE,
378
453
  severity: 2,
379
454
  line: 3,
380
455
  },
381
- silent: aidc("Text.", switched(IEEE)),
456
+ silent: blind("Text.", switched(IEEE)),
382
457
  },
383
458
  "tex/required-section": {
384
459
  reports: {
385
- tree: paper(tex("\\section{Introduction}\nText.", IEEE), AIDC),
460
+ tree: paper(tex("\\section{Introduction}\nText.", IEEE), BLIND_PAPER),
386
461
  file: TEX_FILE,
387
462
  severity: 2,
388
463
  line: 5,
389
464
  },
390
- silent: aidc("Text."),
465
+ silent: blind("Text."),
466
+ },
467
+ "tex/heading-case": {
468
+ reports: {
469
+ tree: paper(tex("\\section{Related work}", ACM_CLASS), ACM_PRESET),
470
+ file: TEX_FILE,
471
+ severity: 2,
472
+ line: 3,
473
+ },
474
+ silent: paper(tex("\\section{Related Work}", ACM_CLASS), ACM_PRESET),
391
475
  },
392
476
  "paper/folder-venue-leftover": {
393
477
  reports: {
394
- // An AIDC paper still in the folder it had when it targeted AISec.
478
+ // A BlindConf paper in a folder named for EMNLP. The rule knows only the SHIPPED venues'
479
+ // names (REALM's preset calls it by its conference, EMNLP), so the other venue is one of them.
395
480
  tree: {
396
- "papers/aisec-2026/PIPELINE-STATUS.md": STATUS,
397
- "papers/aisec-2026/paper.tex": tex("x"),
398
- "papers/aisec-2026/paperlint.json": AIDC[`${P}/paperlint.json`],
481
+ "papers/emnlp-2026/PIPELINE-STATUS.md": STATUS,
482
+ "papers/emnlp-2026/paper.tex": tex("x"),
483
+ ...onBlindConf("papers/emnlp-2026"),
399
484
  },
400
- file: "papers/aisec-2026/PIPELINE-STATUS.md",
485
+ file: "papers/emnlp-2026/PIPELINE-STATUS.md",
401
486
  severity: 1,
402
487
  line: 1,
403
488
  },
404
489
  // The folder names the paper's own venue.
405
490
  silent: {
406
- "papers/aidc-2026/PIPELINE-STATUS.md": STATUS,
407
- "papers/aidc-2026/paper.tex": tex("x"),
408
- "papers/aidc-2026/paperlint.json": AIDC[`${P}/paperlint.json`],
491
+ "papers/blindconf-2026/PIPELINE-STATUS.md": STATUS,
492
+ "papers/blindconf-2026/paper.tex": tex("x"),
493
+ ...onBlindConf("papers/blindconf-2026"),
409
494
  },
410
495
  },
496
+ // EMNLP, a shipped venue's name (REALM's conference), in a BlindConf paper's text.
411
497
  "tex/venue-leftover": {
412
498
  reports: {
413
- tree: aidc("First written for AISec."),
499
+ tree: blind("First written for EMNLP."),
414
500
  file: TEX_FILE,
415
501
  severity: 1,
416
502
  line: 4,
417
503
  },
418
- silent: aidc("Text."),
504
+ silent: blind("Text."),
419
505
  },
420
506
  "tex/claim-provenance": {
421
507
  reports: {
@@ -439,31 +525,31 @@ const CASES: Readonly<Record<string, RuleCases>> = {
439
525
  },
440
526
  silent: paper(tex(FORMAL)),
441
527
  },
442
- // The three register bands judge a body against the anchors of AIDC's preset; the body opens on
443
- // line 3, right after the \\section, where the finding about the whole body stands.
528
+ // The three register bands judge a body against the anchor of BlindConf's preset; the body opens
529
+ // on line 3, right after the \\section, where the finding about the whole body stands.
444
530
  "tex/contrast-frames": {
445
531
  reports: {
446
- tree: aidc(formalWith(5, "It reads the word, not the operation.")),
532
+ tree: blind(formalWith(5, "It reads the word, not the operation.")),
447
533
  file: TEX_FILE,
448
534
  severity: 1,
449
535
  line: 3,
450
536
  },
451
- silent: aidc(LINKED),
537
+ silent: blind(LINKED),
452
538
  },
453
539
  "tex/claim-emphasis": {
454
540
  reports: {
455
- tree: aidc(
541
+ tree: blind(
456
542
  formalWith(6, "We find that \\textbf{the rule counts 46 sentences}."),
457
543
  ),
458
544
  file: TEX_FILE,
459
545
  severity: 1,
460
546
  line: 3,
461
547
  },
462
- silent: aidc(LINKED),
548
+ silent: blind(LINKED),
463
549
  },
464
550
  "tex/relation-markers": {
465
- reports: { tree: aidc(FORMAL), file: TEX_FILE, severity: 1, line: 3 },
466
- silent: aidc(LINKED),
551
+ reports: { tree: blind(FORMAL), file: TEX_FILE, severity: 1, line: 3 },
552
+ silent: blind(LINKED),
467
553
  },
468
554
  "bib/reachable-entry": {
469
555
  reports: {
@@ -518,23 +604,23 @@ const CASES: Readonly<Record<string, RuleCases>> = {
518
604
  },
519
605
  "format/layout-override": {
520
606
  reports: {
521
- tree: aidc("\\linespread{0.9}Text."),
607
+ tree: blind("\\linespread{0.9}Text."),
522
608
  file: TEX_FILE,
523
609
  severity: 2,
524
610
  line: 4,
525
611
  },
526
- silent: aidc("\\vspace{-2mm}Text."),
612
+ silent: blind("\\vspace{-2mm}Text."),
527
613
  },
528
614
  "pdf/last-page-balance": {
529
615
  reports: onPdf(
530
616
  built(
531
617
  { last_page: { kind: "measured", columns_pt: [621.5, 264.8] } },
532
- { ...AGENTICDEV, rules: { "pdf/last-page-balance": "error" } },
618
+ { ...EXAMPLE, rules: { "pdf/last-page-balance": "error" } },
533
619
  ),
534
620
  ),
535
621
  silent: built(
536
622
  {},
537
- { ...AGENTICDEV, rules: { "pdf/last-page-balance": "error" } },
623
+ { ...EXAMPLE, rules: { "pdf/last-page-balance": "error" } },
538
624
  ),
539
625
  },
540
626
  "talk/profile": {
@@ -27,6 +27,7 @@ import {
27
27
  usageProblem,
28
28
  type SubmissionArgs,
29
29
  } from "./submission.ts";
30
+ import { venuePreset } from "../test/support.ts";
30
31
 
31
32
  const PAPER = "/w/papers/p";
32
33
  const SITE = "http://127.0.0.1:9";
@@ -41,7 +42,7 @@ const withPresets = (fs: Files): Files => ({
41
42
  });
42
43
 
43
44
  const venue = (portal: unknown): string =>
44
- JSON.stringify({ extends: "paperlint:aidc", portal });
45
+ JSON.stringify(venuePreset("venue", { extends: "paperlint:aidc", portal }));
45
46
  const files = (o: {
46
47
  readonly portal?: unknown;
47
48
  readonly paper?: Readonly<Record<string, unknown>>;
@@ -116,6 +117,10 @@ const fakePortal = (answers: {
116
117
  return { seen, portalFor };
117
118
  };
118
119
 
120
+ /** The line `update` prints before it sends `pdf`, naming `call`. */
121
+ const CHECK = (pdf: string, call: string): string =>
122
+ `before you rely on it, open ${pdf} yourself and check it against ${call}: the page count and what counts toward the limit, the template and its class options, anonymity, and that every figure, table and reference renders — paperlint passing is not the venue accepting the format`;
123
+
119
124
  const ARGS: SubmissionArgs = {
120
125
  sub: "show",
121
126
  paperDir: "papers/p",
@@ -286,6 +291,7 @@ test("update: a dry run by default — the paper's PDF sent, nothing saved, exit
286
291
  ],
287
292
  ]);
288
293
  assert.deepEqual(r.out, [
294
+ CHECK("papers/p/paper.pdf", "the call for papers, https://example.org/cfp"),
289
295
  "dry run — the portal checked the change and kept nothing; --save sends it for real",
290
296
  "HTTP 200",
291
297
  "valid yes",
@@ -321,7 +327,41 @@ test("update --save --submit --abstract --pdf: all of it sent, and said to be sa
321
327
  { save: true },
322
328
  ],
323
329
  ]);
324
- assert.equal(r.out[0], "saved");
330
+ // Guards: the PDF that is sent is the one named, and --save is told to check it too.
331
+ assert.deepEqual(r.out.slice(0, 2), [
332
+ CHECK("other.pdf", "the call for papers, https://example.org/cfp"),
333
+ "saved",
334
+ ]);
335
+ });
336
+
337
+ test("🔴 update: the check line names the venue's call from the preset's `url`; a preset that names no venue gets the generic line", async () => {
338
+ const family = memoryFiles({
339
+ "/w/package.json": "{}",
340
+ "/w/house.jsonc": JSON.stringify({
341
+ type: "family",
342
+ extends: "paperlint:ieee-conference",
343
+ portal: { kind: "hotcrp", url: `${SITE}/` },
344
+ }),
345
+ [`${PAPER}/paperlint.json`]: JSON.stringify({
346
+ extends: "../../house.jsonc",
347
+ submission: { id: 7 },
348
+ }),
349
+ [`${PAPER}/paper.pdf`]: PDF,
350
+ });
351
+ const r = await run({ sub: "update" }, { files: family });
352
+ assert.equal(r.code, 0);
353
+ assert.equal(
354
+ r.out[0],
355
+ CHECK("papers/p/paper.pdf", "the venue's call for papers"),
356
+ );
357
+ });
358
+
359
+ test("show sends nothing, so it asks for no check", async () => {
360
+ const r = await run({});
361
+ assert.equal(
362
+ r.out.some((l) => l.includes("call for papers")),
363
+ false,
364
+ );
325
365
  });
326
366
 
327
367
  test("update: valid:false, or a non-2xx status, exits 1 with the portal's messages", async () => {
@@ -337,7 +377,7 @@ test("update: valid:false, or a non-2xx status, exits 1 with the portal's messag
337
377
  { portal: fakePortal({ update: invalid }) },
338
378
  );
339
379
  assert.equal(r1.code, 1);
340
- assert.deepEqual(r1.out.slice(2), [
380
+ assert.deepEqual(r1.out.slice(3), [
341
381
  "valid NO",
342
382
  "changes —",
343
383
  `local PDF papers/p/paper.pdf sha2-${HEX}`,
@@ -379,7 +419,7 @@ test("update --save that the portal withheld says so", async () => {
379
419
  { sub: "update", save: true },
380
420
  { portal: fakePortal({ update: withheld }) },
381
421
  );
382
- assert.equal(r.out[0], "SAVE WITHHELD by the portal — nothing changed");
422
+ assert.equal(r.out[1], "SAVE WITHHELD by the portal — nothing changed");
383
423
  });
384
424
 
385
425
  test("update: no PDF to send, or no abstract file, is refused before the portal is asked", async () => {
package/src/submission.ts CHANGED
@@ -80,6 +80,8 @@ export interface SubmissionTarget {
80
80
  readonly id: number;
81
81
  /** The paper's built PDF, absolute. */
82
82
  readonly pdf: string;
83
+ /** The venue's call for papers (the preset's `url`); null when the preset names no venue. */
84
+ readonly call: string | null;
83
85
  }
84
86
 
85
87
  const configOf = (dir: string): string => join(dir, CONFIG_FILE);
@@ -109,7 +111,9 @@ export function submissionTarget(
109
111
  const portal = supportedPortal(declared);
110
112
  if (!portal.ok) return portal;
111
113
  const pdf = join(paperDir, p.settings.pdf ?? "paper.pdf");
112
- return ok({ portal: portal.value, id: sub.id, pdf });
114
+ const identity = p.preset.identity;
115
+ const call = identity.type === "venue" ? identity.url : null;
116
+ return ok({ portal: portal.value, id: sub.id, pdf, call });
113
117
  }
114
118
 
115
119
  /** The token from the environment, or the line naming the variable and where to get one. */
@@ -182,6 +186,14 @@ export function showLines(
182
186
  ];
183
187
  }
184
188
 
189
+ /**
190
+ * What `update` says before it sends a PDF, dry run or not: the author opens that file and checks it
191
+ * against the venue's call. paperlint's checks are an aid; the venue's reading of the PDF is what
192
+ * counts. Pure.
193
+ */
194
+ export const checkPdfLine = (shown: string, call: string | null): string =>
195
+ `before you rely on it, open ${shown} yourself and check it against ${call === null ? "the venue's call for papers" : `the call for papers, ${call}`}: the page count and what counts toward the limit, the template and its class options, anonymity, and that every figure, table and reference renders — paperlint passing is not the venue accepting the format`;
196
+
185
197
  /** `update`'s report. Pure. */
186
198
  export function updateLines(
187
199
  o: UpdateOutcome,
@@ -243,6 +255,17 @@ function abstractOf(
243
255
  : ok(new TextDecoder().decode(bytes).trim());
244
256
  }
245
257
 
258
+ /** What `update` sends: the PDF's bytes, the abstract's text, and whether to mark it submitted. */
259
+ const changeOf = (
260
+ a: SubmissionArgs & { readonly pdfPath: string },
261
+ bytes: Uint8Array,
262
+ abstract: string | null,
263
+ ) => ({
264
+ pdf: { name: basename(a.pdfPath), bytes },
265
+ abstract,
266
+ submit: a.submit,
267
+ });
268
+
246
269
  /** `update`: send the PDF (and the abstract, and "submitted" when asked); a dry run unless saving. */
247
270
  async function runUpdate(
248
271
  t: SubmissionTarget,
@@ -259,12 +282,10 @@ async function runUpdate(
259
282
  );
260
283
  const abstract = abstractOf(a, deps);
261
284
  if (!abstract.ok) return (deps.err(abstract.error), 2);
262
- const change = {
263
- pdf: { name: basename(a.pdfPath), bytes },
264
- abstract: abstract.value,
265
- submit: a.submit,
266
- };
267
- const r = await portal.update(t.id, change, { save: a.save });
285
+ deps.log(checkPdfLine(shown, t.call));
286
+ const r = await portal.update(t.id, changeOf(a, bytes, abstract.value), {
287
+ save: a.save,
288
+ });
268
289
  if (!r.ok) return (deps.err(failureText(r.error)), 1);
269
290
  const local = { shown, sha256: sha256Hex(bytes) };
270
291
  updateLines(r.value, local, a.save).forEach((l) => {
@@ -23,7 +23,7 @@ import {
23
23
  talkRules,
24
24
  type TalkRuleName,
25
25
  } from "./talk-rules.ts";
26
- import { useTempDir, writeTree } from "../test/support.ts";
26
+ import { useTempDir, venuePreset, writeTree } from "../test/support.ts";
27
27
 
28
28
  const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
29
29
  const media = (name: string): Uint8Array =>
@@ -62,8 +62,11 @@ function run(
62
62
  const dir = useTempDir("paperlint-talk-rules-");
63
63
  const venue =
64
64
  o.venue === null
65
- ? { extends: "paperlint:acm-sigconf" }
66
- : { extends: "paperlint:agenticdev", talk: { ...VENUE, ...o.venue } };
65
+ ? { type: "family", extends: "paperlint:acm-sigconf" }
66
+ : venuePreset("venue", {
67
+ extends: "paperlint:agenticdev",
68
+ talk: { ...VENUE, ...o.venue },
69
+ });
67
70
  writeTree(dir, {
68
71
  "package.json": "{}",
69
72
  "p/paper.tex": "x",
@@ -130,32 +130,40 @@ copyFileSync(join(VENUES, R.SCHEMA_FILE), join(tmp, R.SCHEMA_FILE));
130
130
  const bad = (text: string) =>
131
131
  throws(() => R.parseProfile(text, "bad.jsonc", tmp));
132
132
  const BAD: [string, string, string][] = [
133
- ["no `tex` block at all", `{ "template": "acmart" }`, "tex"],
133
+ [
134
+ "no `tex` block at all",
135
+ `{ "type": "family", "template": "acmart" }`,
136
+ "tex",
137
+ ],
134
138
  // Guards: typo detection — `templat` would be accepted and the field silently unread.
135
139
  [
136
140
  "an unknown top-level key (a typo)",
137
- `{ "tex": { "packages": { "a": ["a.sty"] } }, "templat": "x" }`,
141
+ `{ "type": "family", "tex": { "packages": { "a": ["a.sty"] } }, "templat": "x" }`,
138
142
  "additional properties",
139
143
  ],
140
144
  [
141
145
  "a package with NO proof file",
142
- `{ "tex": { "packages": { "acmart": [] } } }`,
146
+ `{ "type": "family", "tex": { "packages": { "acmart": [] } } }`,
143
147
  ".tex.packages['acmart']",
144
148
  ],
145
149
  [
146
150
  "a package name tlmgr would not know (upper case)",
147
- `{ "tex": { "packages": { "Acmart": ["acmart.cls"] } } }`,
151
+ `{ "type": "family", "tex": { "packages": { "Acmart": ["acmart.cls"] } } }`,
148
152
  "property name",
149
153
  ],
150
154
  [
151
155
  "a proof that is a path, not a file name",
152
- `{ "tex": { "packages": { "a": ["tex/a.sty"] } } }`,
156
+ `{ "type": "family", "tex": { "packages": { "a": ["tex/a.sty"] } } }`,
153
157
  "pattern",
154
158
  ],
155
- ["an empty packages map", `{ "tex": { "packages": {} } }`, "fewer than 1"],
159
+ [
160
+ "an empty packages map",
161
+ `{ "type": "family", "tex": { "packages": {} } }`,
162
+ "fewer than 1",
163
+ ],
156
164
  [
157
165
  "an unknown key inside tex",
158
- `{ "tex": { "packages": { "a": ["a.sty"] }, "pkgs": {} } }`,
166
+ `{ "type": "family", "tex": { "packages": { "a": ["a.sty"] }, "pkgs": {} } }`,
159
167
  "additional properties",
160
168
  ],
161
169
  ];
@@ -174,7 +182,9 @@ check(
174
182
  );
175
183
  check(
176
184
  "comments and trailing commas ARE JSONC",
177
- bad(`// quote\n{ "tex": { "packages": { "a": ["a.sty"], }, }, }`) === "",
185
+ bad(
186
+ `// quote\n{ "type": "family", "tex": { "packages": { "a": ["a.sty"], }, }, }`,
187
+ ) === "",
178
188
  );
179
189
  rmSync(tmp, { recursive: true, force: true });
180
190
 
@@ -239,7 +249,7 @@ rmSync(tmp, { recursive: true, force: true });
239
249
  // ── 5. the parser's tolerances: an optional block left out, and Ajv's optional fields ───
240
250
  {
241
251
  const bare = R.parsePreset(
242
- '{ "extends": "paperlint:aisec" }',
252
+ '{ "type": "venue", "name": "X", "url": "https://x.org/", "extends": "paperlint:aisec" }',
243
253
  "bare.jsonc",
244
254
  VENUES,
245
255
  );
@@ -254,9 +264,13 @@ rmSync(tmp, { recursive: true, force: true });
254
264
  check(
255
265
  "a tex block without `packages` is refused by the schema, before anything reads it",
256
266
  /tex\.jsonc: \.tex should have required property 'packages'/.test(
257
- throws(() => R.parsePreset('{ "tex": {} }', "tex.jsonc", VENUES)),
267
+ throws(() =>
268
+ R.parsePreset('{ "type": "family", "tex": {} }', "tex.jsonc", VENUES),
269
+ ),
270
+ ),
271
+ throws(() =>
272
+ R.parsePreset('{ "type": "family", "tex": {} }', "tex.jsonc", VENUES),
258
273
  ),
259
- throws(() => R.parsePreset('{ "tex": {} }', "tex.jsonc", VENUES)),
260
274
  );
261
275
  check(
262
276
  "violations: Ajv's null `errors` is no violation; a message Ajv left out is an empty one",