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
package/src/presets.ts CHANGED
@@ -12,19 +12,27 @@
12
12
  * Anything else — an npm package name, a bare name — is refused by name. npm presets are not
13
13
  * supported yet; a bare name is almost always a shipped name missing its prefix, and the message says so.
14
14
  *
15
+ * ── WHAT EACH FILE IS ─────────────────────────────────────────────────────────────
16
+ * Every preset declares its `type` (`venue-profile.schema.json`): a `venue` names itself and links
17
+ * its call for papers, a template `family` names no venue, and the TeX `base` set is extended by
18
+ * nothing. A chain must read top-down as template → venue: a family extends only a family, and
19
+ * nothing extends the base set. A chain that does not is refused naming both files.
20
+ *
15
21
  * ── THE CHAIN ─────────────────────────────────────────────────────────────────────
16
22
  * At most `MAX_PRESET_DEPTH` files, root to leaf. A cycle (relative paths can form one) is refused
17
23
  * naming the chain. The chain is merged block by block:
18
24
  *
19
25
  * tex union — a child never removes a package its parent needs
20
26
  * format per key, the child wins; `kinds` by kind name, a child's kind replaces that kind
21
- * rules per rule id, the child wins
22
- * template the child wins; so does `name`, and `required_sections`, `register` and `talk` (each whole)
27
+ * rules per rule id, the child wins; `ruleOrigins` keeps which file set each
28
+ * template the child wins; so does `name`, and `required_sections`, `register`, `talk` and
29
+ * `portal` (each whole)
23
30
  * aliases union, with every `name` — what the venue is called along the chain
24
31
  *
25
- * ── THE LABEL ─────────────────────────────────────────────────────────────────────
26
- * Messages, the facts file and the build plan need a word for the venue. It is the most derived
27
- * preset's `name` when one sets it, else the spec's file name without its extension
32
+ * ── THE ID AND THE LABEL ──────────────────────────────────────────────────────────
33
+ * The facts file and the build plan carry the preset's `id`: the spec's file name without its
34
+ * extension, a stable key consumers match against preset ids. Messages say the `label`: the most
35
+ * derived venue's `name` when the chain has a venue, else the id
28
36
  * (`paperlint:agenticdev` → `agenticdev`, `./venues/usenix-sec.jsonc` → `usenix-sec`). Display
29
37
  * only: nothing is ever resolved by it.
30
38
  */
@@ -37,12 +45,13 @@ import {
37
45
  profileFileOf,
38
46
  venueNames,
39
47
  type PresetFile,
48
+ type PresetIdentity,
40
49
  type RequiredSection,
41
50
  type TexRequirements,
42
51
  type VenueFormat,
43
52
  } from "./tex-requirements.ts";
44
53
  import { callerPath } from "./caller-path.ts";
45
- import { err, ok, type Result } from "./domain/result.ts";
54
+ import { err, isOk, ok, type Result } from "./domain/result.ts";
46
55
  import type { Files } from "./ports/files.ts";
47
56
  import {
48
57
  readPaperSettings,
@@ -53,6 +62,7 @@ import { CONFIG_FILE } from "#lib/paper-config";
53
62
  import { messageOf } from "./domain/text.ts";
54
63
  import type { RegisterAnchor } from "./domain/register.ts";
55
64
  import type { VenueTalk } from "./domain/talk.ts";
65
+ import type { VenuePortal } from "./domain/submission.ts";
56
66
 
57
67
  /** The prefix of a shipped preset's spec. */
58
68
  export const SHIPPED_PREFIX = "paperlint:";
@@ -73,10 +83,17 @@ export interface PresetTemplate {
73
83
 
74
84
  /** A resolved chain, merged. */
75
85
  export interface Preset {
76
- /** The word for this venue in messages. */
86
+ /**
87
+ * The preset's id: the spec's file name without its extension (`agenticdev`, `usenix-sec`). The
88
+ * stable key a machine reads — the facts file and the build plan carry it — whatever `name` says.
89
+ */
90
+ readonly id: string;
91
+ /** The word for this venue in messages: the venue's `name`, else the id. */
77
92
  readonly label: string;
78
93
  /** Every file of the chain, root first. */
79
94
  readonly chain: readonly string[];
95
+ /** What the spec names — the leaf file of the chain: a venue (with its call), or a family. */
96
+ readonly identity: PresetIdentity;
80
97
  /**
81
98
  * The `\\documentclass` the venue's template uses, as the preset spells it, and the file that
82
99
  * spells it; null when no file of the chain names one. Read by the rules and `paperlint new`,
@@ -91,10 +108,14 @@ export interface Preset {
91
108
  readonly tex: TexRequirements;
92
109
  readonly format: VenueFormat;
93
110
  readonly rules: Readonly<Record<string, unknown>>;
111
+ /** Rule id → the file of the chain that set its entry in `rules` (the last one to name it). */
112
+ readonly ruleOrigins: Readonly<Record<string, string>>;
94
113
  /** The accepted papers the register rules measure a body against; empty when no file names any. */
95
114
  readonly registerAnchors: readonly RegisterAnchor[];
96
115
  /** What the venue asks a presenter to send (`talk/*`); null when no file of the chain says. */
97
116
  readonly talk: VenueTalk | null;
117
+ /** Where the venue takes submissions (`paperlint submission`); null when no file of the chain says. */
118
+ readonly portal: VenuePortal | null;
98
119
  }
99
120
 
100
121
  /** Why a spec does not resolve. */
@@ -108,6 +129,14 @@ export type PresetProblem =
108
129
  }
109
130
  | { readonly kind: "cycle"; readonly chain: readonly string[] }
110
131
  | { readonly kind: "too-deep"; readonly chain: readonly string[] }
132
+ /** `file` extends `parent`, which a preset of its type may not extend. */
133
+ | {
134
+ readonly kind: "wrong-parent";
135
+ readonly file: string;
136
+ readonly parent: string;
137
+ /** A family extending a venue, or anything extending the base set. */
138
+ readonly parentType: "venue" | "base";
139
+ }
111
140
  | { readonly kind: "broken"; readonly file: string; readonly why: string };
112
141
 
113
142
  /** The display label of a spec: the file name without its extension. Pure. */
@@ -162,27 +191,47 @@ function readPreset(
162
191
  }
163
192
  }
164
193
 
165
- /** The chain's files, leaf first, each parsed. */
166
- function chainOf(
167
- spec: string,
168
- from: string,
194
+ /** One file of a chain: where it is, and what it says. */
195
+ interface Link {
196
+ readonly file: string;
197
+ readonly preset: PresetFile;
198
+ }
199
+
200
+ /**
201
+ * The chain from `child` up, leaf first: `below` is every file read so far, `child` the last of them.
202
+ * Each parent is refused when it closes a cycle, makes the chain too long, or is of a type `child`
203
+ * may not extend.
204
+ */
205
+ function chainUp(
206
+ below: readonly Link[],
207
+ child: Link,
169
208
  deps: PresetDeps,
170
- ): Result<{ file: string; preset: PresetFile }[], PresetProblem> {
171
- const out: { file: string; preset: PresetFile }[] = [];
172
- let next: { spec: string; from: string } | null = { spec, from };
173
- while (next !== null) {
174
- const r = readPreset(next.spec, next.from, deps);
175
- if (!r.ok) return r;
176
- const files = out.map((x) => x.file);
177
- if (files.includes(r.value.file))
178
- return err({ kind: "cycle", chain: [...files, r.value.file] });
179
- if (out.length === MAX_PRESET_DEPTH)
180
- return err({ kind: "too-deep", chain: [...files, r.value.file] });
181
- out.push(r.value);
182
- const up: string | null = r.value.preset.extends;
183
- next = up === null ? null : { spec: up, from: r.value.file };
184
- }
185
- return ok(out);
209
+ ): Result<readonly Link[], PresetProblem> {
210
+ const up = child.preset.extends;
211
+ if (up === null) return ok(below);
212
+ const r = readPreset(up, child.file, deps);
213
+ if (!r.ok) return r;
214
+ const files = below.map((x) => x.file);
215
+ if (files.includes(r.value.file))
216
+ return err({ kind: "cycle", chain: [...files, r.value.file] });
217
+ if (below.length === MAX_PRESET_DEPTH)
218
+ return err({ kind: "too-deep", chain: [...files, r.value.file] });
219
+ const parentType = r.value.preset.identity.type;
220
+ if (parentType === "venue" && child.preset.identity.type === "family")
221
+ return err({
222
+ kind: "wrong-parent",
223
+ file: child.file,
224
+ parent: r.value.file,
225
+ parentType,
226
+ });
227
+ if (parentType === "base")
228
+ return err({
229
+ kind: "wrong-parent",
230
+ file: child.file,
231
+ parent: r.value.file,
232
+ parentType,
233
+ });
234
+ return chainUp([...below, r.value], r.value, deps);
186
235
  }
187
236
 
188
237
  /** A child's format over its parent's: per key the child wins, kinds by name. Pure. */
@@ -209,50 +258,68 @@ export function mergeFormat(
209
258
  };
210
259
  }
211
260
 
261
+ /** A venue's name; a family and the base set have none. Pure. */
262
+ const nameOf = (i: PresetIdentity): string | null =>
263
+ i.type === "venue" ? i.name : null;
264
+
265
+ /** A venue's name as a list, empty for a family and the base set. Pure. */
266
+ const namesOf = (i: PresetIdentity): readonly string[] =>
267
+ i.type === "venue" ? [i.name] : [];
268
+
269
+ /** What the merge carries from file to file: the preset without what only the whole chain knows. */
270
+ type Merging = Omit<Preset, "id" | "label" | "chain" | "identity"> & {
271
+ readonly name: string | null;
272
+ };
273
+
274
+ const NOTHING_MERGED: Merging = {
275
+ name: null,
276
+ template: null,
277
+ aliases: [],
278
+ blind: false,
279
+ requiredSections: [],
280
+ tex: NO_REQUIREMENTS,
281
+ format: NO_FORMAT,
282
+ rules: {},
283
+ ruleOrigins: {},
284
+ registerAnchors: [],
285
+ talk: null,
286
+ portal: null,
287
+ };
288
+
289
+ /** One file of the chain, `file`, merged over what its parents gave. Pure. */
290
+ const mergeOne = (
291
+ acc: Merging,
292
+ { preset: p, file }: { readonly preset: PresetFile; readonly file: string },
293
+ ): Merging => ({
294
+ name: nameOf(p.identity) ?? acc.name,
295
+ template: p.template === null ? acc.template : { text: p.template, file },
296
+ aliases: [...new Set([...acc.aliases, ...namesOf(p.identity), ...p.aliases])],
297
+ blind: p.blind ?? acc.blind,
298
+ requiredSections: p.requiredSections ?? acc.requiredSections,
299
+ tex: p.tex ? mergeRequirements(acc.tex, p.tex) : acc.tex,
300
+ format: mergeFormat(acc.format, p.format),
301
+ rules: { ...acc.rules, ...p.rules },
302
+ ruleOrigins: {
303
+ ...acc.ruleOrigins,
304
+ ...Object.fromEntries(Object.keys(p.rules).map((id) => [id, file])),
305
+ },
306
+ registerAnchors: p.registerAnchors ?? acc.registerAnchors,
307
+ talk: p.talk ?? acc.talk,
308
+ portal: p.portal ?? acc.portal,
309
+ });
310
+
212
311
  /** The chain, root first, merged into one preset. Pure. */
213
312
  function merged(
214
313
  spec: string,
215
314
  rootFirst: readonly { readonly preset: PresetFile; readonly file: string }[],
315
+ leaf: { readonly preset: PresetFile },
216
316
  ): Preset {
217
- const base: Omit<Preset, "label" | "chain"> & {
218
- readonly name: string | null;
219
- } = {
220
- name: null,
221
- template: null,
222
- aliases: [],
223
- blind: false,
224
- requiredSections: [],
225
- tex: NO_REQUIREMENTS,
226
- format: NO_FORMAT,
227
- rules: {},
228
- registerAnchors: [],
229
- talk: null,
230
- };
231
- const m = rootFirst.reduce(
232
- (acc, { preset: p, file }) => ({
233
- name: p.name ?? acc.name,
234
- template: p.template === null ? acc.template : { text: p.template, file },
235
- aliases: [
236
- ...new Set([
237
- ...acc.aliases,
238
- ...(p.name === null ? [] : [p.name]),
239
- ...p.aliases,
240
- ]),
241
- ],
242
- blind: p.blind ?? acc.blind,
243
- requiredSections: p.requiredSections ?? acc.requiredSections,
244
- tex: p.tex ? mergeRequirements(acc.tex, p.tex) : acc.tex,
245
- format: mergeFormat(acc.format, p.format),
246
- rules: { ...acc.rules, ...p.rules },
247
- registerAnchors: p.registerAnchors ?? acc.registerAnchors,
248
- talk: p.talk ?? acc.talk,
249
- }),
250
- base,
251
- );
252
- const { name, ...rest } = m;
317
+ const { name, ...rest } = rootFirst.reduce(mergeOne, NOTHING_MERGED);
253
318
  return {
319
+ id: labelOf(spec),
254
320
  label: name ?? labelOf(spec),
255
321
  chain: rootFirst.map((x) => x.file),
322
+ identity: leaf.preset.identity,
256
323
  ...rest,
257
324
  };
258
325
  }
@@ -266,13 +333,17 @@ export function resolvePreset(
266
333
  fromFile: string,
267
334
  deps: PresetDeps,
268
335
  ): Result<Preset, PresetProblem> {
269
- const chain = chainOf(spec, fromFile, deps);
270
- if (!chain.ok)
271
- return chain.error.kind === "not-found"
272
- ? err({ ...chain.error, shipped: shippedPresets(deps.venuesDir) })
273
- : chain;
274
- const rootFirst = [...chain.value].reverse();
275
- return ok(merged(spec, rootFirst));
336
+ const refused = (p: PresetProblem): Result<Preset, PresetProblem> =>
337
+ err(
338
+ p.kind === "not-found"
339
+ ? { ...p, shipped: shippedPresets(deps.venuesDir) }
340
+ : p,
341
+ );
342
+ const leaf = readPreset(spec, fromFile, deps);
343
+ if (!leaf.ok) return refused(leaf.error);
344
+ const chain = chainUp([leaf.value], leaf.value, deps);
345
+ if (!chain.ok) return refused(chain.error);
346
+ return ok(merged(spec, [...chain.value].reverse(), leaf.value));
276
347
  }
277
348
 
278
349
  /** The shipped presets' names (each is spelled `paperlint:<name>`). */
@@ -280,11 +351,48 @@ export function shippedPresets(venuesDir: string): string[] {
280
351
  return venueNames(venuesDir);
281
352
  }
282
353
 
354
+ /** The spec a paper writes in `extends` for the shipped preset `name`: `paperlint:<name>`. */
355
+ export const shippedSpec = (name: string): string => `${SHIPPED_PREFIX}${name}`;
356
+
357
+ /**
358
+ * Every shipped preset that resolves, each read the way a paper extending it would read it. The one
359
+ * piece that reads the presets directory. One that does not resolve is left out: `pdf/profile`
360
+ * owns that.
361
+ */
362
+ export function resolveShipped(deps: PresetDeps): readonly Preset[] {
363
+ const from = join(deps.venuesDir, CONFIG_FILE);
364
+ return shippedPresets(deps.venuesDir)
365
+ .map((name) => resolvePreset(shippedSpec(name), from, deps))
366
+ .filter(isOk)
367
+ .map((r) => r.value);
368
+ }
369
+
370
+ /** A shipped venue as a name knows it, and the files of its chain: what tells it from a paper's own. */
371
+ export type ShippedVenue = Pick<Preset, "label" | "aliases" | "chain">;
372
+
373
+ /**
374
+ * A resolved preset as a name knows it: its label, its aliases, its chain. Pure. (Not Remeda's
375
+ * `pick`: the app layer may not import it — `boundaries/dependencies`, `APP_EXTERNALS`.)
376
+ */
377
+ export const venueOf = ({ label, aliases, chain }: Preset): ShippedVenue => ({
378
+ label,
379
+ aliases,
380
+ chain,
381
+ });
382
+
383
+ /**
384
+ * THE ONE SOURCE OF VENUE NAMES: every shipped venue's label and `aliases` (with each `name` along
385
+ * its chain). Read by `paperlint new`, which refuses a folder name naming one, and through
386
+ * `otherVenues` by `tex/venue-leftover` and `paper/folder-venue-leftover`.
387
+ */
388
+ export const shippedVenueNames = (deps: PresetDeps): readonly ShippedVenue[] =>
389
+ resolveShipped(deps).map(venueOf);
390
+
283
391
  /** One line for a problem, naming what to change. Pure. */
284
392
  export function presetProblemText(p: PresetProblem): string {
285
393
  switch (p.kind) {
286
394
  case "unsupported":
287
- return `"extends": "${p.spec}" — a preset is \`paperlint:<name>\` (shipped) or a path starting with ./ or ../ (your own); npm presets: not yet supported. Did you mean "${SHIPPED_PREFIX}${p.spec}"?`;
395
+ return `"extends": "${p.spec}" — a preset is \`paperlint:<name>\` (shipped) or a path starting with ./ or ../ (your own); npm presets: not yet supported. Did you mean "${shippedSpec(p.spec)}"?`;
288
396
  case "not-found":
289
397
  return `"extends": "${p.spec}" names no preset (${p.file}); shipped presets (${SHIPPED_PREFIX}<name>): ${p.shipped.join(", ")}`;
290
398
  case "cycle":
@@ -293,6 +401,10 @@ export function presetProblemText(p: PresetProblem): string {
293
401
  return `the preset chain is longer than ${String(MAX_PRESET_DEPTH)}: ${p.chain.join(" → ")}`;
294
402
  case "broken":
295
403
  return `the preset ${p.file} does not parse: ${p.why}`;
404
+ case "wrong-parent":
405
+ return p.parentType === "base"
406
+ ? `the preset ${p.file} extends ${p.parent}, the TeX base set — every paper gets that set already, and no preset extends it`
407
+ : `the preset ${p.file} is a template family and extends ${p.parent}, a venue — a family builds only on a family; a venue extends a family or another venue`;
296
408
  }
297
409
  }
298
410