paperlint 6.2.3 → 7.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 (234) hide show
  1. package/CONTRIBUTING.md +4 -2
  2. package/README.md +10 -3
  3. package/dist/adapters/hotcrp/index.d.ts +7 -0
  4. package/dist/adapters/hotcrp/index.d.ts.map +1 -0
  5. package/dist/adapters/hotcrp/index.js +7 -0
  6. package/dist/adapters/hotcrp/index.js.map +1 -0
  7. package/dist/adapters/hotcrp/portal.io.d.ts +20 -0
  8. package/dist/adapters/hotcrp/portal.io.d.ts.map +1 -0
  9. package/dist/adapters/hotcrp/portal.io.js +90 -0
  10. package/dist/adapters/hotcrp/portal.io.js.map +1 -0
  11. package/dist/adapters/hotcrp/response.d.ts +13 -0
  12. package/dist/adapters/hotcrp/response.d.ts.map +1 -0
  13. package/dist/adapters/hotcrp/response.js +138 -0
  14. package/dist/adapters/hotcrp/response.js.map +1 -0
  15. package/dist/adapters/latex/conditionals.d.ts +21 -0
  16. package/dist/adapters/latex/conditionals.d.ts.map +1 -0
  17. package/dist/adapters/latex/conditionals.js +75 -0
  18. package/dist/adapters/latex/conditionals.js.map +1 -0
  19. package/dist/adapters/latex/headings.d.ts +10 -0
  20. package/dist/adapters/latex/headings.d.ts.map +1 -0
  21. package/dist/adapters/latex/headings.js +129 -0
  22. package/dist/adapters/latex/headings.js.map +1 -0
  23. package/dist/adapters/latex/index.d.ts +1 -0
  24. package/dist/adapters/latex/index.d.ts.map +1 -1
  25. package/dist/adapters/latex/index.js +3 -0
  26. package/dist/adapters/latex/index.js.map +1 -1
  27. package/dist/adapters/latex/parse.d.ts +1 -1
  28. package/dist/adapters/latex/parse.d.ts.map +1 -1
  29. package/dist/adapters/latex/parse.js +3 -2
  30. package/dist/adapters/latex/parse.js.map +1 -1
  31. package/dist/build.d.ts +1 -1
  32. package/dist/build.d.ts.map +1 -1
  33. package/dist/build.js +1 -1
  34. package/dist/build.js.map +1 -1
  35. package/dist/cli.d.ts +4 -2
  36. package/dist/cli.d.ts.map +1 -1
  37. package/dist/cli.js +126 -37
  38. package/dist/cli.js.map +1 -1
  39. package/dist/domain/heading-case.d.ts +100 -0
  40. package/dist/domain/heading-case.d.ts.map +1 -0
  41. package/dist/domain/heading-case.js +365 -0
  42. package/dist/domain/heading-case.js.map +1 -0
  43. package/dist/domain/result.d.ts +4 -0
  44. package/dist/domain/result.d.ts.map +1 -1
  45. package/dist/domain/result.js +2 -0
  46. package/dist/domain/result.js.map +1 -1
  47. package/dist/domain/submission.d.ts +106 -0
  48. package/dist/domain/submission.d.ts.map +1 -0
  49. package/dist/domain/submission.js +39 -0
  50. package/dist/domain/submission.js.map +1 -0
  51. package/dist/domain/tex-document.d.ts +32 -0
  52. package/dist/domain/tex-document.d.ts.map +1 -1
  53. package/dist/domain/tex-document.js +11 -0
  54. package/dist/domain/tex-document.js.map +1 -1
  55. package/dist/domain/venue-name.d.ts +38 -0
  56. package/dist/domain/venue-name.d.ts.map +1 -0
  57. package/dist/domain/venue-name.js +71 -0
  58. package/dist/domain/venue-name.js.map +1 -0
  59. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  60. package/dist/eslint-rules/latex-language.js +16 -3
  61. package/dist/eslint-rules/latex-language.js.map +1 -1
  62. package/dist/facts-file.d.ts +3 -3
  63. package/dist/facts-file.d.ts.map +1 -1
  64. package/dist/facts-file.js +3 -3
  65. package/dist/facts-file.js.map +1 -1
  66. package/dist/folder-venue-rule.d.ts +51 -0
  67. package/dist/folder-venue-rule.d.ts.map +1 -0
  68. package/dist/folder-venue-rule.js +96 -0
  69. package/dist/folder-venue-rule.js.map +1 -0
  70. package/dist/heading-case.d.ts +48 -0
  71. package/dist/heading-case.d.ts.map +1 -0
  72. package/dist/heading-case.js +191 -0
  73. package/dist/heading-case.js.map +1 -0
  74. package/dist/init.d.ts +6 -0
  75. package/dist/init.d.ts.map +1 -1
  76. package/dist/init.js +3 -3
  77. package/dist/init.js.map +1 -1
  78. package/dist/lib/paper-config.d.ts.map +1 -1
  79. package/dist/lib/paper-config.js +5 -1
  80. package/dist/lib/paper-config.js.map +1 -1
  81. package/dist/new-paper.d.ts +19 -5
  82. package/dist/new-paper.d.ts.map +1 -1
  83. package/dist/new-paper.js +23 -4
  84. package/dist/new-paper.js.map +1 -1
  85. package/dist/paper-settings.d.ts +3 -0
  86. package/dist/paper-settings.d.ts.map +1 -1
  87. package/dist/paper-settings.js +20 -4
  88. package/dist/paper-settings.js.map +1 -1
  89. package/dist/ports/latex.d.ts +7 -1
  90. package/dist/ports/latex.d.ts.map +1 -1
  91. package/dist/ports/submission-portal.d.ts +19 -0
  92. package/dist/ports/submission-portal.d.ts.map +1 -0
  93. package/dist/ports/submission-portal.js +2 -0
  94. package/dist/ports/submission-portal.js.map +1 -0
  95. package/dist/preset-card-rule.d.ts +100 -0
  96. package/dist/preset-card-rule.d.ts.map +1 -0
  97. package/dist/preset-card-rule.js +221 -0
  98. package/dist/preset-card-rule.js.map +1 -0
  99. package/dist/presets.d.ts +43 -2
  100. package/dist/presets.d.ts.map +1 -1
  101. package/dist/presets.js +130 -63
  102. package/dist/presets.js.map +1 -1
  103. package/dist/rules-config.js +1 -1
  104. package/dist/submission.d.ts +61 -0
  105. package/dist/submission.d.ts.map +1 -0
  106. package/dist/submission.js +211 -0
  107. package/dist/submission.js.map +1 -0
  108. package/dist/tex-requirements.d.ts +36 -3
  109. package/dist/tex-requirements.d.ts.map +1 -1
  110. package/dist/tex-requirements.js +30 -2
  111. package/dist/tex-requirements.js.map +1 -1
  112. package/dist/tex-venue-rules.d.ts +7 -8
  113. package/dist/tex-venue-rules.d.ts.map +1 -1
  114. package/dist/tex-venue-rules.js +10 -14
  115. package/dist/tex-venue-rules.js.map +1 -1
  116. package/dist/types.d.ts +13 -0
  117. package/dist/types.d.ts.map +1 -1
  118. package/docs/configuration.md +30 -11
  119. package/docs/optional-rules.md +4 -3
  120. package/docs/rules/format/page-limit.md +1 -1
  121. package/docs/rules/paper/folder-venue-leftover.md +104 -0
  122. package/docs/rules/talk/captions-cover.md +1 -1
  123. package/docs/rules/talk/duration-floor.md +1 -1
  124. package/docs/rules/talk/profile.md +1 -1
  125. package/docs/rules/tex/heading-case.md +424 -0
  126. package/docs/rules.md +64 -53
  127. package/docs/skills.md +5 -4
  128. package/docs/submission.md +64 -0
  129. package/docs/talk.md +1 -1
  130. package/eslint-rules/latex-language.test.mjs +12 -0
  131. package/eslint-rules/latex-language.ts +17 -4
  132. package/eslint-rules/paper-typography.test.mjs +12 -0
  133. package/eslint.config.mjs +59 -1
  134. package/fixtures/build-e2e/acmart/paper.tex +1 -1
  135. package/fixtures/build-e2e/fallback/expect.json +3 -3
  136. package/lib/paper-config.ts +6 -1
  137. package/lib/skill-checks.d.mts +7 -0
  138. package/lib/skill-checks.mjs +31 -0
  139. package/lib/skill-trigger-cases.mjs +11 -0
  140. package/package.json +8 -2
  141. package/presets/acm-sigconf.jsonc +5 -0
  142. package/presets/acm-sigconf.md +43 -0
  143. package/presets/agenticdev.jsonc +27 -6
  144. package/presets/agenticdev.md +151 -0
  145. package/presets/aidc.jsonc +12 -0
  146. package/presets/aidc.md +75 -0
  147. package/presets/aisec.jsonc +5 -1
  148. package/presets/aisec.md +117 -0
  149. package/presets/ieee-conference.jsonc +5 -0
  150. package/presets/ieee-conference.md +38 -0
  151. package/presets/realm.jsonc +7 -2
  152. package/presets/realm.md +166 -0
  153. package/presets/tex/agenticdev.tex +1 -1
  154. package/presets/tex-base.jsonc +6 -0
  155. package/presets/tex-base.md +30 -0
  156. package/presets/venue-profile.schema.json +57 -2
  157. package/skills/find-venue/SKILL.md +6 -5
  158. package/skills/find-venue/SKILL.md.spec.ts +5 -4
  159. package/skills/paper-pipeline/SKILL.md +5 -6
  160. package/skills/paper-pipeline/SKILL.md.spec.ts +4 -5
  161. package/skills/paper-pipeline/paper-pipeline.harness.mjs +4 -0
  162. package/skills/paper-pipeline/references/acl-venue-rules.md +1 -1
  163. package/skills/study-accepted-papers/SKILL.md +3 -3
  164. package/skills/study-accepted-papers/SKILL.md.spec.ts +2 -2
  165. package/skills/submission-portal/SKILL.md +69 -0
  166. package/skills/submission-portal/SKILL.md.spec.ts +71 -0
  167. package/skills/submission-portal/submission-portal.eval.mjs +15 -0
  168. package/skills/submission-portal/submission-portal.test.ts +81 -0
  169. package/skills/submit-paper/SKILL.md +27 -22
  170. package/skills/submit-paper/SKILL.md.spec.ts +26 -21
  171. package/skills/submit-paper/references/publishers/acm.md +1 -1
  172. package/src/adapters/hotcrp/cassette/README.md +24 -0
  173. package/src/adapters/hotcrp/cassette/get-noauth.json +10 -0
  174. package/src/adapters/hotcrp/cassette/get-paper.json +39 -0
  175. package/src/adapters/hotcrp/cassette/post-dot.json +21 -0
  176. package/src/adapters/hotcrp/cassette/post-ok.json +12 -0
  177. package/src/adapters/hotcrp/index.ts +11 -0
  178. package/src/adapters/hotcrp/portal.io.ts +130 -0
  179. package/src/adapters/hotcrp/portal.test.ts +503 -0
  180. package/src/adapters/hotcrp/response.ts +181 -0
  181. package/src/adapters/latex/conditionals.ts +130 -0
  182. package/src/adapters/latex/headings.ts +163 -0
  183. package/src/adapters/latex/index.test.ts +65 -0
  184. package/src/adapters/latex/index.ts +3 -0
  185. package/src/adapters/latex/parse.ts +3 -2
  186. package/src/build.ts +2 -2
  187. package/src/cli.test.ts +155 -3
  188. package/src/cli.ts +161 -25
  189. package/src/domain/heading-case.test.ts +660 -0
  190. package/src/domain/heading-case.ts +538 -0
  191. package/src/domain/result.test.ts +9 -1
  192. package/src/domain/result.ts +5 -0
  193. package/src/domain/submission.test.ts +45 -0
  194. package/src/domain/submission.ts +141 -0
  195. package/src/domain/tex-document.ts +36 -0
  196. package/src/domain/venue-name.test.ts +121 -0
  197. package/src/domain/venue-name.ts +129 -0
  198. package/src/facts-file.test.ts +4 -4
  199. package/src/facts-file.ts +6 -6
  200. package/src/folder-venue-rule.test.ts +182 -0
  201. package/src/folder-venue-rule.ts +141 -0
  202. package/src/heading-case.test.ts +500 -0
  203. package/src/heading-case.ts +275 -0
  204. package/src/init.test.ts +22 -0
  205. package/src/init.ts +9 -2
  206. package/src/new-paper.harness.mjs +11 -9
  207. package/src/new-paper.test.ts +135 -10
  208. package/src/new-paper.ts +38 -7
  209. package/src/paper-settings-commands.test.ts +25 -10
  210. package/src/paper-settings.test.ts +12 -1
  211. package/src/paper-settings.ts +25 -5
  212. package/src/parse-args.test.ts +19 -0
  213. package/src/ports/latex.ts +7 -0
  214. package/src/ports/submission-portal.ts +26 -0
  215. package/src/preset-card-rule.test.ts +266 -0
  216. package/src/preset-card-rule.ts +384 -0
  217. package/src/presets.test.ts +212 -37
  218. package/src/presets.ts +183 -71
  219. package/src/rule-cases.test.ts +147 -42
  220. package/src/rules-config.ts +1 -1
  221. package/src/submission.test.ts +497 -0
  222. package/src/submission.ts +311 -0
  223. package/src/talk-rules.test.ts +6 -3
  224. package/src/tex-requirements.harness.ts +25 -11
  225. package/src/tex-requirements.test.ts +127 -1
  226. package/src/tex-requirements.ts +78 -8
  227. package/src/tex-venue-rules.test.ts +36 -22
  228. package/src/tex-venue-rules.ts +14 -26
  229. package/src/types.ts +13 -0
  230. package/src/venue-rules.test.ts +7 -6
  231. package/templates/paper/paper.tex +1 -1
  232. package/skills/submit-paper/references/venues/agenticdev.md +0 -171
  233. package/skills/submit-paper/references/venues/aisec.md +0 -105
  234. package/skills/submit-paper/references/venues/realm.md +0 -155
@@ -7,7 +7,7 @@
7
7
  * resolving shows here.
8
8
  */
9
9
  import { readdirSync, readFileSync } from "node:fs";
10
- import { join } from "node:path";
10
+ import { join, relative } from "node:path";
11
11
  import { describe, expect, it } from "vitest";
12
12
  import { memoryFiles } from "./adapters/memory/index.ts";
13
13
  import { presetsDir } from "./package-dirs.ts";
@@ -16,6 +16,7 @@ import {
16
16
  MAX_PRESET_DEPTH,
17
17
  SHIPPED_PREFIX,
18
18
  shippedPresets,
19
+ shippedVenueNames,
19
20
  labelOf,
20
21
  presetProblemText,
21
22
  resolvePreset,
@@ -34,6 +35,17 @@ const deps = (extra: Record<string, string> = {}) => ({
34
35
  });
35
36
  const resolve = (spec: string, extra: Record<string, string> = {}) =>
36
37
  resolvePreset(spec, PAPER_FILE, deps(extra));
38
+ /** A venue preset of a project's own: the identity every venue declares, then `body`. */
39
+ const venue = (body: Record<string, unknown>, name = "Our Workshop"): string =>
40
+ JSON.stringify({
41
+ type: "venue",
42
+ name,
43
+ url: "https://example.org/cfp",
44
+ ...body,
45
+ });
46
+ /** A template family of a project's own. */
47
+ const family = (body: Record<string, unknown>): string =>
48
+ JSON.stringify({ type: "family", ...body });
37
49
 
38
50
  describe("shipped presets", () => {
39
51
  it("paperlint:agenticdev → agenticdev over acm-sigconf: the family's format, the venue's kinds", () => {
@@ -53,7 +65,15 @@ describe("shipped presets", () => {
53
65
  file: join(VENUES, "acm-sigconf.jsonc"),
54
66
  });
55
67
  expect("acmart" in r.value.tex.packages).toBe(true);
56
- expect(r.value.label).toBe("agenticdev");
68
+ // The label is the venue's own name, from its file's `name`; the id stays the file name — the
69
+ // stable key the facts file and the build plan carry.
70
+ expect(r.value.label).toBe("AgenticDev");
71
+ expect(r.value.id).toBe("agenticdev");
72
+ expect(r.value.identity).toEqual({
73
+ type: "venue",
74
+ name: "AgenticDev",
75
+ url: "https://conf.researchr.org/home/ase-2026/agenticdev-2026",
76
+ });
57
77
  });
58
78
 
59
79
  it("a paper may extend a family directly: format and fonts, and no kinds", () => {
@@ -77,19 +97,72 @@ describe("shipped presets", () => {
77
97
  });
78
98
  });
79
99
 
80
- describe("the naming convention of shipped presets (docs/rules.md)", () => {
81
- // Each shipped preset, parsed: its file name and what it extends. The families are DERIVED — a
82
- // shipped preset another shipped preset extends — so a new family is checked the day it ships.
83
- const presets = shippedPresets(VENUES).map((name) => {
84
- const file = join(VENUES, `${name}.jsonc`);
85
- return {
86
- name,
87
- extends: parsePreset(readFileSync(file, "utf8"), file, VENUES).extends,
88
- };
100
+ // Each shipped preset, parsed: its file name and what it extends. The families are DERIVED — a
101
+ // shipped preset another shipped preset extends — so a new family is checked the day it ships.
102
+ const PARSED = shippedPresets(VENUES).map((name) => {
103
+ const file = join(VENUES, `${name}.jsonc`);
104
+ return {
105
+ name,
106
+ preset: parsePreset(readFileSync(file, "utf8"), file, VENUES),
107
+ };
108
+ });
109
+ const FAMILIES = PARSED.filter((p) =>
110
+ PARSED.some((q) => q.preset.extends === `${SHIPPED_PREFIX}${p.name}`),
111
+ );
112
+
113
+ describe("what each shipped preset declares itself to be", () => {
114
+ it("every shipped preset loads and declares what it is; a venue links its call", () => {
115
+ const declared = Object.fromEntries(
116
+ readdirSync(VENUES)
117
+ .filter((f) => f.endsWith(".jsonc"))
118
+ .map((f) => [
119
+ f,
120
+ parsePreset(readFileSync(join(VENUES, f), "utf8"), f, VENUES)
121
+ .identity,
122
+ ]),
123
+ );
124
+ expect(declared).toEqual({
125
+ "acm-sigconf.jsonc": { type: "family" },
126
+ "agenticdev.jsonc": {
127
+ type: "venue",
128
+ name: "AgenticDev",
129
+ url: "https://conf.researchr.org/home/ase-2026/agenticdev-2026",
130
+ },
131
+ "aidc.jsonc": {
132
+ type: "venue",
133
+ name: "AIDC",
134
+ url: "https://aidcworkshop.github.io/",
135
+ },
136
+ "aisec.jsonc": {
137
+ type: "venue",
138
+ name: "AISec",
139
+ url: "https://aisec.cc/",
140
+ },
141
+ "ieee-conference.jsonc": { type: "family" },
142
+ "realm.jsonc": {
143
+ type: "venue",
144
+ name: "REALM",
145
+ url: "https://realm-workshop.github.io/call_for_papers",
146
+ },
147
+ "tex-base.jsonc": { type: "base" },
148
+ });
89
149
  });
90
- const families = presets.filter((p) =>
91
- presets.some((q) => q.extends === `${SHIPPED_PREFIX}${p.name}`),
92
- );
150
+
151
+ it("the families another preset extends are exactly the ones that declare `family`", () => {
152
+ expect(
153
+ PARSED.filter((p) => p.preset.identity.type === "family").map(
154
+ (p) => p.name,
155
+ ),
156
+ ).toEqual(FAMILIES.map((f) => f.name));
157
+ });
158
+ });
159
+
160
+ describe("the naming convention of shipped presets (docs/rules.md)", () => {
161
+ const families = FAMILIES;
162
+ const presets = PARSED.map((p) => ({
163
+ name: p.name,
164
+ extends: p.preset.extends,
165
+ }));
93
166
  const publishers = families.map((f) => f.name.slice(0, f.name.indexOf("-")));
94
167
 
95
168
  it("there are families to check (else the checks below see nothing)", () => {
@@ -117,16 +190,18 @@ describe("the naming convention of shipped presets (docs/rules.md)", () => {
117
190
  });
118
191
 
119
192
  describe("a project's own preset, by relative path", () => {
120
- const usenix = JSON.stringify({
121
- extends: "paperlint:acm-sigconf",
122
- name: "USENIX Security",
123
- format: {
124
- columns: 1,
125
- kinds: { full: { body_pages_max: 13, ref_pages_max: 0 } },
193
+ const usenix = venue(
194
+ {
195
+ extends: "paperlint:acm-sigconf",
196
+ format: {
197
+ columns: 1,
198
+ kinds: { full: { body_pages_max: 13, ref_pages_max: 0 } },
199
+ },
200
+ tex: { packages: { usenix: ["usenix.sty"] } },
201
+ rules: { "pdf/body-size": "off" },
126
202
  },
127
- tex: { packages: { usenix: ["usenix.sty"] } },
128
- rules: { "pdf/body-size": "off" },
129
- });
203
+ "USENIX Security",
204
+ );
130
205
 
131
206
  it("./ is relative to the file that says it; merged per block, child wins", () => {
132
207
  const r = resolve("../../venues/usenix-sec.jsonc", {
@@ -146,18 +221,21 @@ describe("a project's own preset, by relative path", () => {
146
221
  expect(r.value.label).toBe("USENIX Security");
147
222
  });
148
223
 
149
- it("without `name`, the label is the file name without its extension", () => {
224
+ it("a family names no venue: its label is the file name without its extension", () => {
150
225
  const r = resolve("./venues/usenix-sec.jsonc", {
151
- "/work/papers/p/venues/usenix-sec.jsonc": JSON.stringify({
226
+ "/work/papers/p/venues/usenix-sec.jsonc": family({
152
227
  extends: "paperlint:acm-sigconf",
153
228
  }),
154
229
  });
155
230
  expect(r.ok && r.value.label).toBe("usenix-sec");
231
+ expect(r.ok && r.value.identity).toEqual({ type: "family" });
156
232
  });
233
+ });
157
234
 
235
+ describe("a project's own preset: what its children replace", () => {
158
236
  it("a child kind replaces that kind wholesale; other kinds are kept", () => {
159
237
  const r = resolve("./mine.jsonc", {
160
- "/work/papers/p/mine.jsonc": JSON.stringify({
238
+ "/work/papers/p/mine.jsonc": venue({
161
239
  extends: "paperlint:agenticdev",
162
240
  format: { kinds: { short: { body_pages_max: 6 } } },
163
241
  }),
@@ -173,13 +251,40 @@ describe("a project's own preset, by relative path", () => {
173
251
 
174
252
  it("rules: the leaf wins over its parent, per rule id", () => {
175
253
  const r = resolve("./mine.jsonc", {
176
- "/work/papers/p/mine.jsonc": JSON.stringify({
254
+ "/work/papers/p/mine.jsonc": venue({
177
255
  extends: "paperlint:agenticdev",
178
256
  rules: { "pdf/last-page-balance": "off" },
179
257
  }),
180
258
  });
181
259
  expect(r.ok && r.value.rules["pdf/last-page-balance"]).toBe("off");
182
260
  });
261
+
262
+ it("ruleOrigins: each rule's entry names the file of the chain that set it", () => {
263
+ const r = resolve("./mine.jsonc", {
264
+ "/work/papers/p/mine.jsonc": venue({
265
+ extends: "paperlint:agenticdev",
266
+ rules: { "pdf/body-size": "off" },
267
+ }),
268
+ });
269
+ // Guards: an own rule is the leaf's, an inherited one names the parent that set it.
270
+ expect(r.ok && r.value.ruleOrigins).toEqual({
271
+ "pdf/last-page-balance": join(VENUES, "agenticdev.jsonc"),
272
+ "tex/heading-case": join(VENUES, "agenticdev.jsonc"),
273
+ "pdf/body-size": "/work/papers/p/mine.jsonc",
274
+ });
275
+ });
276
+
277
+ it("ruleOrigins: a rule the leaf sets again is the leaf's, not its parent's", () => {
278
+ const r = resolve("./mine.jsonc", {
279
+ "/work/papers/p/mine.jsonc": venue({
280
+ extends: "paperlint:agenticdev",
281
+ rules: { "tex/heading-case": "off" },
282
+ }),
283
+ });
284
+ expect(r.ok && r.value.ruleOrigins["tex/heading-case"]).toBe(
285
+ "/work/papers/p/mine.jsonc",
286
+ );
287
+ });
183
288
  });
184
289
 
185
290
  describe("what does not resolve, and says why", () => {
@@ -216,8 +321,8 @@ describe("what does not resolve, and says why", () => {
216
321
  describe("chains that do not resolve", () => {
217
322
  it("a cycle is refused, naming the chain", () => {
218
323
  const r = resolve("./a.jsonc", {
219
- "/work/papers/p/a.jsonc": JSON.stringify({ extends: "./b.jsonc" }),
220
- "/work/papers/p/b.jsonc": JSON.stringify({ extends: "./a.jsonc" }),
324
+ "/work/papers/p/a.jsonc": family({ extends: "./b.jsonc" }),
325
+ "/work/papers/p/b.jsonc": family({ extends: "./a.jsonc" }),
221
326
  });
222
327
  expect(r.ok).toBe(false);
223
328
  if (r.ok) return;
@@ -230,7 +335,7 @@ describe("chains that do not resolve", () => {
230
335
  it(`a chain longer than ${String(MAX_PRESET_DEPTH)} is refused, printing it`, () => {
231
336
  const files: Record<string, string> = {};
232
337
  for (let i = 1; i <= 5; i++)
233
- files[`/work/papers/p/p${String(i)}.jsonc`] = JSON.stringify({
338
+ files[`/work/papers/p/p${String(i)}.jsonc`] = venue({
234
339
  extends: i < 5 ? `./p${String(i + 1)}.jsonc` : "paperlint:acm-sigconf",
235
340
  });
236
341
  const r = resolve("./p1.jsonc", files);
@@ -242,17 +347,17 @@ describe("chains that do not resolve", () => {
242
347
 
243
348
  it("a chain of exactly the limit resolves", () => {
244
349
  const r = resolve("./p1.jsonc", {
245
- "/work/papers/p/p1.jsonc": JSON.stringify({ extends: "./p2.jsonc" }),
246
- "/work/papers/p/p2.jsonc": JSON.stringify({
247
- extends: "paperlint:agenticdev",
248
- }),
350
+ "/work/papers/p/p1.jsonc": venue({ extends: "./p2.jsonc" }),
351
+ "/work/papers/p/p2.jsonc": venue({ extends: "paperlint:agenticdev" }),
249
352
  });
250
353
  expect(r.ok && r.value.chain.length).toBe(MAX_PRESET_DEPTH);
251
354
  });
355
+ });
252
356
 
357
+ describe("chains that do not resolve: the files themselves", () => {
253
358
  it("a preset that fails the schema is named with its file", () => {
254
359
  const r = resolve("./bad.jsonc", {
255
- "/work/papers/p/bad.jsonc": JSON.stringify({
360
+ "/work/papers/p/bad.jsonc": family({
256
361
  extends: "paperlint:acm-sigconf",
257
362
  colums: 2,
258
363
  }),
@@ -265,16 +370,51 @@ describe("chains that do not resolve", () => {
265
370
 
266
371
  it("a preset with no `extends` must carry `tex` (it is a root)", () => {
267
372
  const r = resolve("./root.jsonc", {
268
- "/work/papers/p/root.jsonc": JSON.stringify({ format: { columns: 1 } }),
373
+ "/work/papers/p/root.jsonc": family({ format: { columns: 1 } }),
269
374
  });
270
375
  expect(r.ok).toBe(false);
271
376
  });
377
+
378
+ it("a family that extends a venue is refused, naming both files", () => {
379
+ const r = resolve("./fam.jsonc", {
380
+ "/work/papers/p/fam.jsonc": family({ extends: "paperlint:aisec" }),
381
+ });
382
+ expect(r.ok).toBe(false);
383
+ if (r.ok) return;
384
+ expect(presetProblemText(r.error)).toBe(
385
+ `the preset /work/papers/p/fam.jsonc is a template family and extends ${join(VENUES, "aisec.jsonc")}, a venue — a family builds only on a family; a venue extends a family or another venue`,
386
+ );
387
+ });
388
+
389
+ it("nothing extends the TeX base set, even by a relative path", () => {
390
+ const r = resolve("./mine.jsonc", {
391
+ "/work/papers/p/mine.jsonc": venue({
392
+ extends: relative("/work/papers/p", join(VENUES, "tex-base.jsonc")),
393
+ }),
394
+ });
395
+ expect(r.ok).toBe(false);
396
+ if (r.ok) return;
397
+ expect(presetProblemText(r.error)).toBe(
398
+ `the preset /work/papers/p/mine.jsonc extends ${join(VENUES, "tex-base.jsonc")}, the TeX base set — every paper gets that set already, and no preset extends it`,
399
+ );
400
+ });
401
+
402
+ it("a venue may extend a venue, and the resolved identity is the leaf's", () => {
403
+ const r = resolve("./mine.jsonc", {
404
+ "/work/papers/p/mine.jsonc": venue({ extends: "paperlint:aisec" }),
405
+ });
406
+ expect(r.ok && r.value.identity).toEqual({
407
+ type: "venue",
408
+ name: "Our Workshop",
409
+ url: "https://example.org/cfp",
410
+ });
411
+ });
272
412
  });
273
413
 
274
414
  describe("a preset's template: kept as its file spells it, with that file", () => {
275
415
  it("a child's template replaces its parent's, and names its own file", () => {
276
416
  const r = resolve("./own.jsonc", {
277
- "/work/papers/p/own.jsonc": JSON.stringify({
417
+ "/work/papers/p/own.jsonc": venue({
278
418
  extends: "paperlint:aidc",
279
419
  template: "\\documentclass[conference]{IEEEtran}",
280
420
  }),
@@ -297,3 +437,38 @@ describe("labelOf — display only, never used to resolve", () => {
297
437
  expect(labelOf(spec)).toBe(label);
298
438
  });
299
439
  });
440
+
441
+ describe("shippedVenueNames — what each shipped venue is called", () => {
442
+ it("every shipped preset, by its label and the names along its chain", () => {
443
+ const names = shippedVenueNames(deps());
444
+ // A venue is labelled by its `name`, a family by its file name.
445
+ expect(names.map((n) => n.label)).toEqual([
446
+ "acm-sigconf",
447
+ "AgenticDev",
448
+ "AIDC",
449
+ "AISec",
450
+ "ieee-conference",
451
+ "REALM",
452
+ ]);
453
+ expect(names.find((n) => n.label === "AISec")?.aliases).toEqual(
454
+ expect.arrayContaining(["AISec", "ACM CCS"]),
455
+ );
456
+ });
457
+
458
+ it("a preset that does not resolve is left out", () => {
459
+ const files = memoryFiles(
460
+ Object.fromEntries(
461
+ Object.entries(shipped).filter(([f]) => !f.endsWith("realm.jsonc")),
462
+ ),
463
+ );
464
+ expect(
465
+ shippedVenueNames({ files, venuesDir: VENUES }).map((n) => n.label),
466
+ ).toEqual([
467
+ "acm-sigconf",
468
+ "AgenticDev",
469
+ "AIDC",
470
+ "AISec",
471
+ "ieee-conference",
472
+ ]);
473
+ });
474
+ });