@promptctl/cc-candybar 1.42.1 → 1.43.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 (126) hide show
  1. package/dist/index.mjs +72 -71
  2. package/package.json +5 -6
  3. package/src/check.ts +0 -478
  4. package/src/cli-flags.ts +0 -8
  5. package/src/click/wire.ts +0 -158
  6. package/src/config/action.ts +0 -329
  7. package/src/config/cli.ts +0 -71
  8. package/src/config/default-dsl-config.ts +0 -1645
  9. package/src/config/disclosure.ts +0 -170
  10. package/src/config/dsl-loader.ts +0 -339
  11. package/src/config/dsl-types.ts +0 -581
  12. package/src/config/edit-chrome.ts +0 -559
  13. package/src/config/help.ts +0 -151
  14. package/src/config/ident.ts +0 -22
  15. package/src/config/layout-ops.ts +0 -177
  16. package/src/config/loader/actions.ts +0 -972
  17. package/src/config/loader/cache.ts +0 -206
  18. package/src/config/loader/cross-ref.ts +0 -714
  19. package/src/config/loader/cycles.ts +0 -148
  20. package/src/config/loader/diagnostics.ts +0 -99
  21. package/src/config/loader/discovery.ts +0 -182
  22. package/src/config/loader/edit-mode.ts +0 -137
  23. package/src/config/loader/emit-schema.ts +0 -68
  24. package/src/config/loader/globals.ts +0 -269
  25. package/src/config/loader/helpers.ts +0 -48
  26. package/src/config/loader/layout.ts +0 -693
  27. package/src/config/loader/looks.ts +0 -96
  28. package/src/config/loader/menu-synth.ts +0 -435
  29. package/src/config/loader/merge.ts +0 -115
  30. package/src/config/loader/persist-target.ts +0 -67
  31. package/src/config/loader/presets.ts +0 -119
  32. package/src/config/loader/refs.ts +0 -100
  33. package/src/config/loader/reserved-namespace.ts +0 -38
  34. package/src/config/loader/segments.ts +0 -120
  35. package/src/config/loader/validate-core.ts +0 -737
  36. package/src/config/loader/variables.ts +0 -260
  37. package/src/config/menu-keys.ts +0 -139
  38. package/src/config/option-domain.ts +0 -164
  39. package/src/config/presets.ts +0 -326
  40. package/src/config/settings-menu.ts +0 -775
  41. package/src/daemon/acquire.ts +0 -684
  42. package/src/daemon/cache/git.ts +0 -649
  43. package/src/daemon/cache/render.ts +0 -623
  44. package/src/daemon/cache/session-usage-store.ts +0 -720
  45. package/src/daemon/cache/watchers.ts +0 -249
  46. package/src/daemon/client-debug.ts +0 -120
  47. package/src/daemon/client-stats.ts +0 -130
  48. package/src/daemon/client-transport.ts +0 -273
  49. package/src/daemon/client.ts +0 -78
  50. package/src/daemon/config-overrides-store.ts +0 -663
  51. package/src/daemon/debug-types.ts +0 -91
  52. package/src/daemon/debug.ts +0 -264
  53. package/src/daemon/fork-bomb-breaker.ts +0 -351
  54. package/src/daemon/limits.ts +0 -211
  55. package/src/daemon/log.ts +0 -81
  56. package/src/daemon/parent-watchdog.ts +0 -87
  57. package/src/daemon/paths.ts +0 -211
  58. package/src/daemon/process-fingerprint.ts +0 -146
  59. package/src/daemon/protocol.ts +0 -292
  60. package/src/daemon/render-payload.ts +0 -1256
  61. package/src/daemon/server.ts +0 -1330
  62. package/src/daemon/session-state-file.ts +0 -108
  63. package/src/daemon/session-state.ts +0 -237
  64. package/src/daemon/socket-lease.ts +0 -209
  65. package/src/daemon/socket-ownership.ts +0 -209
  66. package/src/daemon/stats.ts +0 -235
  67. package/src/daemon/verbs/config-validators.ts +0 -250
  68. package/src/daemon/verbs/index.ts +0 -706
  69. package/src/daemon/verbs/state-validators.ts +0 -249
  70. package/src/daemon/verbs/validator-registry.ts +0 -457
  71. package/src/demo/dsl.ts +0 -143
  72. package/src/demo/mock-data.ts +0 -67
  73. package/src/demo/statusline.json5 +0 -94
  74. package/src/dsl/node-registry.ts +0 -374
  75. package/src/dsl/render.ts +0 -803
  76. package/src/help-text.ts +0 -90
  77. package/src/index.ts +0 -210
  78. package/src/install/currency.ts +0 -197
  79. package/src/install/index.ts +0 -557
  80. package/src/proc/launch.ts +0 -459
  81. package/src/proc/stats-handle.ts +0 -13
  82. package/src/render/action.ts +0 -883
  83. package/src/render/active-segment.ts +0 -78
  84. package/src/render/diagnostic-style.ts +0 -23
  85. package/src/render/diagnostic-text.ts +0 -77
  86. package/src/render/error-glyph.ts +0 -53
  87. package/src/render/menu.ts +0 -257
  88. package/src/render/outcome-plan.ts +0 -45
  89. package/src/render/picker.ts +0 -372
  90. package/src/render/segment-color.ts +0 -74
  91. package/src/render/split-lines.ts +0 -51
  92. package/src/render/strip.ts +0 -228
  93. package/src/segments/cache.ts +0 -131
  94. package/src/segments/context.ts +0 -190
  95. package/src/segments/git.ts +0 -1084
  96. package/src/segments/metrics.ts +0 -187
  97. package/src/segments/pricing.ts +0 -452
  98. package/src/segments/session.ts +0 -23
  99. package/src/segments/tmux.ts +0 -74
  100. package/src/template-engine/cells.ts +0 -90
  101. package/src/template-engine/colors.ts +0 -124
  102. package/src/template-engine/engine.ts +0 -108
  103. package/src/template-engine/funcs.ts +0 -232
  104. package/src/template-engine/index.ts +0 -11
  105. package/src/template-engine/layout.ts +0 -133
  106. package/src/template-engine/scope.ts +0 -62
  107. package/src/template-engine/sparkline.ts +0 -79
  108. package/src/themes/index.ts +0 -20
  109. package/src/themes/palette-resolvers.ts +0 -84
  110. package/src/themes/policy.ts +0 -393
  111. package/src/utils/cache.ts +0 -206
  112. package/src/utils/claude.ts +0 -683
  113. package/src/utils/color-support.ts +0 -118
  114. package/src/utils/formatters.ts +0 -99
  115. package/src/utils/logger.ts +0 -5
  116. package/src/utils/outcome.ts +0 -33
  117. package/src/utils/schema-validator.ts +0 -126
  118. package/src/utils/single-flight.ts +0 -57
  119. package/src/utils/terminal-width.ts +0 -51
  120. package/src/utils/terminal.ts +0 -11
  121. package/src/utils/transcript-fs.ts +0 -279
  122. package/src/var-system/index.ts +0 -24
  123. package/src/var-system/sources.ts +0 -1047
  124. package/src/var-system/store.ts +0 -223
  125. package/src/var-system/types.ts +0 -57
  126. package/src/version.ts +0 -17
@@ -1,972 +0,0 @@
1
- // [LAW:types-are-the-program] The action-table schema. An ActionDecl is
2
- // discriminated by exactly-one-of set/persist/copy/open/reset; a `set` or
3
- // `persist` adds exactly-one value SOURCE (to/from/min-max-by/int — `persist`
4
- // excludes `int`). The proof here is what lets every downstream consumer
5
- // (renderAction, deriveActionValidators, deriveConfigActionValidators) match
6
- // on the present key with no fallthrough. Whether a `{{ action "name" }}`
7
- // reference resolves is a cross-ref concern. This file changes when the
8
- // action vocabulary changes.
9
- //
10
- // [LAW:no-mode-explosion] Unlike cache (single-key value-arms → oneOfPresent) and
11
- // variables (tag-by-field-value → taggedUnion), an action's arms are multi-key
12
- // RECORDS: a `set`/`persist` carries the discriminator plus a value-source group
13
- // (`to` | `from` | `min`/`max`/`by` | `int`). A single key never selects an arm, so
14
- // the shared present-key engine doesn't fit — bending it to would mean per-arm
15
- // sibling allow-lists and bespoke unknown-key messages bolted on as modes. Instead
16
- // the leaf machinery is shared (`fields` + `refine` + field specs carry every arm's
17
- // shape and cross-field invariant as DATA, parameterized by discriminator name so
18
- // `set` and `persist` share one field-map definition) and this file owns only the
19
- // thin total present-key dispatch — the irreducible union eliminator.
20
-
21
- import {
22
- ACTION_KEYS,
23
- PERSIST_WHEN,
24
- type ActionDecl,
25
- type ActionKey,
26
- type OptionDomain,
27
- } from "../action.js";
28
- import { findKeyLine } from "./diagnostics.js";
29
- import {
30
- describeType,
31
- describeValue,
32
- fields,
33
- isPlainObject,
34
- objectJson,
35
- refine,
36
- requireString,
37
- type ArmParse,
38
- type FieldSpec,
39
- type FieldSpecMap,
40
- type JsonNode,
41
- type Refinement,
42
- type ValidateCtx,
43
- } from "./validate-core.js";
44
-
45
- // [LAW:locality-or-seam] Structural validation of the `actions` block: each
46
- // action is discriminated by which of set/copy/open is present, a `set` further
47
- // by its value SOURCE (to | from | min/max/by | int). Whether a `{{ action
48
- // "name" }}` reference resolves is a cross-ref concern (validateCrossReferences),
49
- // which runs on the MERGED config so a segment can reference a default-provided
50
- // action.
51
- export function validateActions(
52
- ctx: ValidateCtx,
53
- raw: unknown,
54
- ): Record<string, ActionDecl> {
55
- if (raw === undefined) return {};
56
- if (!isPlainObject(raw)) {
57
- issue(
58
- ctx,
59
- "actions",
60
- `actions must be an object, got ${describeType(raw)}`,
61
- );
62
- return {};
63
- }
64
- // [LAW:types-are-the-program] Null-prototype record for user-keyed data, so an
65
- // action named "__proto__"/"constructor" is an ordinary own property, never a
66
- // prototype-chain mutation — matching the widgets block and the compiled maps.
67
- const out: Record<string, ActionDecl> = Object.create(null) as Record<
68
- string,
69
- ActionDecl
70
- >;
71
- for (const [name, decl] of Object.entries(raw)) {
72
- const parsed = validateActionDecl(ctx, `actions.${name}`, decl);
73
- if (parsed !== null) out[name] = parsed;
74
- }
75
- return out;
76
- }
77
-
78
- // [LAW:single-enforcer] One place pushes an issue with the resolved source line —
79
- // the line derivation is mechanical from the path, so no callsite restates it.
80
- function issue(ctx: ValidateCtx, path: string, message: string): void {
81
- ctx.issues.push({
82
- path,
83
- message,
84
- line: findKeyLine(ctx.source, path.split(".")),
85
- });
86
- }
87
-
88
- // [LAW:dataflow-not-control-flow] The top-level union eliminator: exactly one of
89
- // set/copy/open is present, then dispatch the whole record to that arm via the
90
- // arm table. The dispatch is a total projection over the present key (no
91
- // fallthrough) — the only branch is the presence-count, which every union must
92
- // discriminate somewhere. The set arm owns its own siblings (the value source),
93
- // so this level rejects no keys generically.
94
- function validateActionDecl(
95
- ctx: ValidateCtx,
96
- path: string,
97
- raw: unknown,
98
- ): ActionDecl | null {
99
- if (!isPlainObject(raw)) {
100
- issue(
101
- ctx,
102
- path,
103
- `${path} must be an action object, got ${describeType(raw)}`,
104
- );
105
- return null;
106
- }
107
- // [LAW:dataflow-not-control-flow] A dual-destination action
108
- // (candybar-settings-ui-aok.3) carries BOTH `set` and `persist`, so it
109
- // cannot be reached through the exactly-one-of eliminator below —
110
- // `persistWhen` is its own discriminator, and its presence selects the arm
111
- // exactly as the presence of `set` selects that one. The dual arm owns its
112
- // own siblings (the two destination keys plus its value source), like every
113
- // other arm here.
114
- if (PERSIST_WHEN in raw) {
115
- return valueSourceAction(ctx, path, raw, "dual", DUAL_ARMS);
116
- }
117
- const present = (ACTION_KEYS as readonly string[]).filter((k) => k in raw);
118
- if (present.length !== 1) {
119
- issue(
120
- ctx,
121
- path,
122
- `action must declare exactly one of: ${ACTION_KEYS.join(", ")}${
123
- present.length > 1 ? ` (found: ${present.join(", ")})` : ""
124
- }`,
125
- );
126
- return null;
127
- }
128
- return ACTION_ARMS[present[0] as ActionKey](ctx, path, raw);
129
- }
130
-
131
- // [LAW:dataflow-not-control-flow] The top-level arm table as DATA: copy/open share
132
- // one template-arm shape (their key names the only difference); set/persist
133
- // delegate to their value-source sub-union (the SAME field shapes, a different
134
- // discriminator key — see valueSourceAction); reset is copy/open's plain-string
135
- // sibling. The present key indexes this map — the eliminator never branches on
136
- // the key name.
137
- const ACTION_ARMS: Record<ActionKey, ArmParse<ActionDecl>> = {
138
- set: (ctx, path, raw) => valueSourceAction(ctx, path, raw, "set", SET_ARMS),
139
- persist: (ctx, path, raw) =>
140
- valueSourceAction(ctx, path, raw, "persist", PERSIST_ARMS),
141
- copy: templateArm("copy"),
142
- open: templateArm("open"),
143
- reset: resetArm,
144
- undo: markerArm("undo"),
145
- redo: markerArm("redo"),
146
- };
147
-
148
- // [LAW:one-source-of-truth] A copy/open action emits the closed single-key
149
- // object its arm validates — symmetric to `templateArm(key)`'s parse.
150
- function templateArmJson(key: "copy" | "open" | "reset"): JsonNode {
151
- return {
152
- type: "object",
153
- properties: { [key]: { type: "string" } },
154
- required: [key],
155
- additionalProperties: false,
156
- };
157
- }
158
-
159
- // [LAW:one-source-of-truth] One ActionDecl's schema: the set/persist sub-unions
160
- // (each arm's `json`, derived from SET_ARMS/PERSIST_ARMS) joined with
161
- // copy/open/reset — the SAME members `validateActionDecl` dispatches over. The
162
- // `actions` block is a name → ActionDecl map, symmetric to `validateActions`.
163
- function actionDeclJson(): JsonNode {
164
- return {
165
- anyOf: [
166
- ...SET_ARMS.map((arm) => arm.json),
167
- ...PERSIST_ARMS.map((arm) => arm.json),
168
- ...DUAL_ARMS.map((arm) => arm.json),
169
- templateArmJson("copy"),
170
- templateArmJson("open"),
171
- templateArmJson("reset"),
172
- markerArmJson("undo"),
173
- markerArmJson("redo"),
174
- ],
175
- };
176
- }
177
-
178
- export function actionsJson(): JsonNode {
179
- return { type: "object", additionalProperties: actionDeclJson() };
180
- }
181
-
182
- // [LAW:one-type-per-behavior] copy and open are one behavior — a single required
183
- // template string, no other keys — parameterized by the key. Both reject every
184
- // sibling key (the arm's only legal key is its own) with the bespoke per-key
185
- // message, then read the template.
186
- function templateArm(key: "copy" | "open"): ArmParse<ActionDecl> {
187
- return (ctx, path, raw) => {
188
- for (const k of Object.keys(raw)) {
189
- if (k !== key)
190
- issue(
191
- ctx,
192
- `${path}.${k}`,
193
- `Unknown key "${k}" on a ${key} action. Expected only: ${key}`,
194
- );
195
- }
196
- const tmpl = requireString(ctx, path, raw, key);
197
- return tmpl === null ? null : ({ [key]: tmpl } as unknown as ActionDecl);
198
- };
199
- }
200
-
201
- // [LAW:one-type-per-behavior] `reset` is copy/open's shape (a single required
202
- // string, no other keys) but the string is a KEY (a config globals field name),
203
- // not a template — no Go-template parsing happens for it, so it reuses the
204
- // slash-free/non-empty shape `set`/`persist` keys share rather than
205
- // requireString's bare-presence check. A `function` declaration (not a const
206
- // arrow) so it is hoisted — ACTION_ARMS above references it directly, not
207
- // through a deferred closure.
208
- function resetArm(
209
- ctx: ValidateCtx,
210
- path: string,
211
- raw: Record<string, unknown>,
212
- ): ActionDecl | null {
213
- for (const k of Object.keys(raw)) {
214
- if (k !== "reset")
215
- issue(
216
- ctx,
217
- `${path}.${k}`,
218
- `Unknown key "${k}" on a reset action. Expected only: reset`,
219
- );
220
- }
221
- const key = slashFreeString(
222
- ctx,
223
- path,
224
- "reset",
225
- raw,
226
- `reset key must be non-empty (the config globals field to clear)`,
227
- (v) => `reset key "${v}" contains "/" — keys must be slash-free`,
228
- );
229
- return key === null ? null : { reset: key };
230
- }
231
-
232
- // [LAW:one-type-per-behavior] `undo`/`redo` are copy/open/reset's shape one
233
- // step further reduced: a single required key whose only legal VALUE is the
234
- // literal `true` (mirrors intMarkerSpec — a marker, not data), because there
235
- // is no key to name: the history they step is one global stack over the
236
- // whole overrides layer, not a per-target write. `function`, not a const
237
- // arrow, so ACTION_ARMS above (built before this declaration in source
238
- // order) can reference it directly via hoisting.
239
- function markerArm(key: "undo" | "redo"): ArmParse<ActionDecl> {
240
- return (ctx, path, raw) => {
241
- for (const k of Object.keys(raw)) {
242
- if (k !== key)
243
- issue(
244
- ctx,
245
- `${path}.${k}`,
246
- `Unknown key "${k}" on a ${key} action. Expected only: ${key}`,
247
- );
248
- }
249
- if (raw[key] !== true) {
250
- issue(
251
- ctx,
252
- `${path}.${key}`,
253
- `${key} must be the literal true (it takes no key — it steps the ONE global history over the whole overrides layer), got ${describeValue(raw[key])}`,
254
- );
255
- return null;
256
- }
257
- return { [key]: true } as unknown as ActionDecl;
258
- };
259
- }
260
-
261
- // [LAW:one-source-of-truth] Mirrors templateArmJson's shape one level
262
- // narrower: the value schema is `const: true`, not `type: string` — a
263
- // marker action carries no data, on the wire or in the schema.
264
- function markerArmJson(key: "undo" | "redo"): JsonNode {
265
- return {
266
- type: "object",
267
- properties: { [key]: { const: true } },
268
- required: [key],
269
- additionalProperties: false,
270
- };
271
- }
272
-
273
- // ─── The `set` value-source sub-union ────────────────────────────────────────
274
-
275
- // [LAW:single-enforcer] A set-state URL path segment must be a non-empty,
276
- // slash-free string — the set-state value is a slash-delimited
277
- // <session>/<key>/<value> run, so an empty or slash-bearing segment is
278
- // undeliverable. One validator, two callers (the `set` key and a literal `to`
279
- // value), each supplying its bespoke message — the shape is enforced once, the
280
- // wording stays per-use DATA. The codec itself is slash-safe; this is a
281
- // deliberate upstream restriction so a slash-bearing key/value never reaches the
282
- // wire, surfaced at load rather than thrown when validators register.
283
- function slashFreeString(
284
- ctx: ValidateCtx,
285
- path: string,
286
- field: string,
287
- raw: Record<string, unknown>,
288
- emptyMessage: string,
289
- slashMessage: (value: string) => string,
290
- ): string | null {
291
- const v = requireString(ctx, path, raw, field);
292
- if (v === null) return null;
293
- const at = `${path}.${field}`;
294
- if (v === "") {
295
- issue(ctx, at, emptyMessage);
296
- return null;
297
- }
298
- if (v.includes("/")) {
299
- issue(ctx, at, slashMessage(v));
300
- return null;
301
- }
302
- return v;
303
- }
304
-
305
- // [LAW:types-are-the-program] Which KEYS a value-source action carries beside
306
- // its value source, as data — one for a single-destination `set`/`persist`,
307
- // three for a `dual` (both destination keys plus the selector naming which is
308
- // written). Every consumer below (the key validation, the unknown-key
309
- // allow-list, the emitted JSON schema, the reconstructed member) reads this
310
- // one table, so adding the dual arm never meant a second dispatcher: the
311
- // discriminator stopped being ONE key and became a LIST of them, and the
312
- // existing machinery folds over the list [LAW:dataflow-not-control-flow].
313
- type Discriminator = "set" | "persist" | "dual";
314
-
315
- const DISCRIMINATOR_KEYS: Readonly<
316
- Record<Discriminator, ReadonlyArray<readonly [string, string]>>
317
- > = {
318
- set: [["set", "the SessionState key to write"]],
319
- persist: [["persist", "the config globals field to write"]],
320
- dual: [
321
- ["set", "the SessionState key written while persistWhen is off"],
322
- ["persist", "the config globals field written while persistWhen is on"],
323
- [
324
- PERSIST_WHEN,
325
- "the SessionState key whose boolean value chooses the destination",
326
- ],
327
- ],
328
- };
329
-
330
- // [LAW:dataflow-not-control-flow] The discriminator keys are validated once
331
- // for every value source (they are shared across all arms of that
332
- // discriminator), before the source is detected — so a bad key and an
333
- // ambiguous source both surface in one pass. They are therefore NOT fields of
334
- // any arm's `fields` map; the arm parses only the value-source payload, and
335
- // the dispatcher re-attaches them.
336
- //
337
- // [LAW:no-silent-failure] Returns null when ANY key fails, after reporting
338
- // every one of them — the caller threads that null exactly as it threads a
339
- // failed payload, so a partly-valid dual never reconstructs into a member
340
- // missing a destination.
341
- function validateDiscriminatorKeys(
342
- ctx: ValidateCtx,
343
- path: string,
344
- raw: Record<string, unknown>,
345
- discriminator: Discriminator,
346
- ): Record<string, string> | null {
347
- const out: Record<string, string> = {};
348
- const keys = DISCRIMINATOR_KEYS[discriminator];
349
- let ok = true;
350
- for (const [key, noun] of keys) {
351
- // [LAW:no-silent-failure] An ABSENT key gets the shape, not a type
352
- // mismatch. A single-destination arm cannot reach this (its key is the
353
- // discriminator that selected the arm), so this only ever fires on a dual
354
- // that named one destination and not the other — where "persist must be a
355
- // string, got undefined" describes the symptom and teaches nothing, and
356
- // the author needs to be told the three keys travel together.
357
- if (!(key in raw)) {
358
- issue(
359
- ctx,
360
- path,
361
- `${key} is required here (${noun}) — a dual-destination action declares ${keys
362
- .map(([k]) => k)
363
- .join(", ")} together, plus one value source`,
364
- );
365
- ok = false;
366
- continue;
367
- }
368
- const value = slashFreeString(
369
- ctx,
370
- path,
371
- key,
372
- raw,
373
- `${key} key must be non-empty (${noun})`,
374
- (v) => `${key} key "${v}" contains "/" — keys must be slash-free`,
375
- );
376
- if (value === null) {
377
- ok = false;
378
- continue;
379
- }
380
- out[key] = value;
381
- }
382
- // [LAW:no-silent-failure] Every failing key is reported before returning, so
383
- // an author who omits two of a dual's three keys sees both in one pass —
384
- // matching every other multi-issue check in this file, and matching what the
385
- // comment above promises.
386
- return ok ? out : null;
387
- }
388
-
389
- // [LAW:one-source-of-truth] The wire verb name a discriminator's writes
390
- // travel over — `set-state` for `set` (SessionState), `set-config` for
391
- // `persist` (the config-overrides layer), and BOTH for a dual, whose one
392
- // value crosses whichever wire the selector names. Threaded into the shared
393
- // field specs below so their "cannot be delivered on the X wire" messages
394
- // name the wire the value actually crosses, and the field/value noun ("set
395
- // value" / "persist value") names the actual action kind, not always `set`.
396
- function wireName(discriminator: Discriminator): string {
397
- if (discriminator === "set") return "set-state";
398
- return discriminator === "persist" ? "set-config" : "set-state/set-config";
399
- }
400
-
401
- // [LAW:types-are-the-program] Each value source's payload as a field map — the
402
- // non-discriminator keys that source carries. `fields` runs every spec
403
- // (reporting all issues) and fails the arm when a required field is absent or
404
- // invalid; `refine` adds the cross-field invariants `fields` cannot express.
405
- // The reconstructed payload IS the member minus the discriminator, which the
406
- // dispatcher re-attaches. Built once per discriminator (`set`/`persist` share
407
- // field SHAPE but not error WORDING — see setLiteralSpec/fromSpec/cycleSpec).
408
- const TO_FIELDS_SET: FieldSpecMap<{ to: string }> = {
409
- to: setLiteralSpec("set"),
410
- };
411
- const TO_FIELDS_PERSIST: FieldSpecMap<{ to: string }> = {
412
- to: setLiteralSpec("persist"),
413
- };
414
- const FROM_FIELDS_SET: FieldSpecMap<{ from: OptionDomain }> = {
415
- from: fromSpec("set"),
416
- };
417
- const FROM_FIELDS_PERSIST: FieldSpecMap<{ from: OptionDomain }> = {
418
- from: fromSpec("persist"),
419
- };
420
- const BOUNDED_FIELDS: FieldSpecMap<{ min: number; max: number; by: number }> = {
421
- min: requireIntSpec(),
422
- max: requireIntSpec(),
423
- by: requireIntSpec(),
424
- };
425
- const INT_FIELDS: FieldSpecMap<{ int: true }> = { int: intMarkerSpec() };
426
- const CYCLE_FIELDS_SET: FieldSpecMap<{ cycle: readonly string[] }> = {
427
- cycle: cycleSpec("set"),
428
- };
429
- const TO_FIELDS_DUAL: FieldSpecMap<{ to: string }> = {
430
- to: setLiteralSpec("dual"),
431
- };
432
- const FROM_FIELDS_DUAL: FieldSpecMap<{ from: OptionDomain }> = {
433
- from: fromSpec("dual"),
434
- };
435
- const CYCLE_FIELDS_DUAL: FieldSpecMap<{ cycle: readonly string[] }> = {
436
- cycle: cycleSpec("dual"),
437
- };
438
- const CYCLE_FIELDS_PERSIST: FieldSpecMap<{ cycle: readonly string[] }> = {
439
- cycle: cycleSpec("persist"),
440
- };
441
- // [LAW:one-type-per-behavior] brandon-layout-edit-2gc.1's two structural-edit
442
- // arms — PERSIST-only (see action.ts's ActionDecl doc comment for why there
443
- // is no `set` twin). Each field reuses layoutNameSpec: a segment/anchor name
444
- // must be non-empty and free of both `/` (the click wire's own segment
445
- // delimiter) and `:` (layout-ops.ts's op-token delimiter) — the SAME
446
- // wire-safety diligence slashFreeString already applies to `to`/`cycle`
447
- // members, one forbidden character wider.
448
- const REMOVE_SEGMENT_FIELDS: FieldSpecMap<{ removeSegment: string }> = {
449
- removeSegment: layoutNameSpec("removeSegment"),
450
- };
451
- const INSERT_SEGMENT_FIELDS: FieldSpecMap<{
452
- insertSegment: string;
453
- anchor: string;
454
- relation: "before" | "after";
455
- }> = {
456
- insertSegment: layoutNameSpec("insertSegment"),
457
- anchor: layoutNameSpec("anchor"),
458
- relation: relationSpec(),
459
- };
460
- // [LAW:one-type-per-behavior] `insertSegmentFrom`'s payload mirrors
461
- // `insertSegment`'s verbatim except the segment name is a `from`-shaped
462
- // OptionDomain (fromSpec, the SAME field `set`/`persist … from` already
463
- // validate) instead of a literal layout name — the "to" vs "from" split every
464
- // other value source already draws, one arm over.
465
- const INSERT_SEGMENT_FROM_FIELDS: FieldSpecMap<{
466
- insertSegmentFrom: OptionDomain;
467
- anchor: string;
468
- relation: "before" | "after";
469
- }> = {
470
- insertSegmentFrom: fromSpec("persist"),
471
- anchor: layoutNameSpec("anchor"),
472
- relation: relationSpec(),
473
- };
474
-
475
- // [LAW:types-are-the-program] A bounded step is fully described by an integer
476
- // domain (min < max) and a non-zero integer increment (`by`; negative for a
477
- // down-step). The validator derives the range [min,max] (the wire gate); the
478
- // renderer wraps current ± by inside it. These two cross-field invariants are the
479
- // refinements `fields` cannot express — relating two fields, not one — carried as
480
- // DATA whose messages interpolate the assembled value. Unlike a stepper widget's
481
- // positive `step`, `by` may be negative (the down affordance), so the check is
482
- // non-zero, not positive.
483
- interface BoundedPayload {
484
- min: number;
485
- max: number;
486
- by: number;
487
- }
488
- const minLessThanMax: Refinement<BoundedPayload> = {
489
- ok: (v) => v.min < v.max,
490
- issue: (v) => ({
491
- field: "min",
492
- message: `min (${v.min}) must be less than max (${v.max})`,
493
- }),
494
- };
495
- const byNonZero: Refinement<BoundedPayload> = {
496
- ok: (v) => v.by !== 0,
497
- issue: () => ({
498
- field: "by",
499
- message: `by must be a non-zero integer (the per-click increment; negative steps down)`,
500
- }),
501
- };
502
-
503
- // [LAW:types-are-the-program] A value-source arm is its payload field map plus
504
- // its refinements; `detect` (the non-discriminator keys whose presence
505
- // selects it), `allowed` (those keys plus the discriminator, the unknown-key
506
- // allow-list), and `label` (the source name in the exactly-one message — the
507
- // detect keys joined by "/") all DERIVE from the field map, so the field set
508
- // is the single source for what the arm parses, permits, and is named by.
509
- // Parameterized by `discriminator` ("set" | "persist") so `set` and `persist`
510
- // share the identical to/from/min-max-by/cycle shapes without duplicating
511
- // their field maps — only the discriminator's NAME differs in the emitted
512
- // object shape and the reconstructed member.
513
- interface ValueSourceArm {
514
- readonly detect: readonly string[];
515
- readonly allowed: readonly string[];
516
- readonly label: string;
517
- // [LAW:one-source-of-truth] The arm's emit facet: the closed object schema
518
- // for this discriminator's value source — the discriminator key plus the
519
- // source's own fields, derived from the SAME field map `fields` validates.
520
- // Cross-field refinements (min<max, by≠0) are unexpressible in JSON Schema,
521
- // so only the structural shape is emitted.
522
- readonly json: JsonNode;
523
- readonly parse: ArmParse<Partial<ActionDecl>>;
524
- }
525
-
526
- // [LAW:no-mode-explosion] `detectKeys` narrows WHICH of an arm's fields the
527
- // present-count dispatch keys off, independent of `allowed`/`label` (still
528
- // the full field set — what the arm PERMITS and is NAMED by never changes).
529
- // Every arm before insertSegmentFrom had a field set disjoint from every
530
- // other arm's, so `Object.keys(fieldMap)` was a safe default for both jobs
531
- // at once. insertSegmentFrom breaks that: it shares `anchor`/`relation` with
532
- // insertSegment (same POSITION shape, different segment-name SOURCE), so
533
- // dispatching on the full set would make an ordinary `insertSegment` action
534
- // spuriously match both arms via those shared keys. Pass the true
535
- // discriminator (the field no sibling arm carries) here; omit it when the
536
- // field set already is disjoint from every sibling, as it is everywhere else.
537
- function valueSourceArm<P extends object>(
538
- discriminator: Discriminator,
539
- fieldMap: FieldSpecMap<P>,
540
- checks: ReadonlyArray<Refinement<P>> = [],
541
- detectKeys?: readonly string[],
542
- ): ValueSourceArm {
543
- const fullKeys = Object.keys(fieldMap);
544
- const detect = detectKeys ?? fullKeys;
545
- const keys = DISCRIMINATOR_KEYS[discriminator].map(([k]) => k);
546
- const inner: ArmParse<P> = (ctx, path, raw) =>
547
- fields(ctx, fieldMap, path, raw);
548
- const source = objectJson(fieldMap) as {
549
- properties: Record<string, JsonNode>;
550
- required?: readonly string[];
551
- };
552
- return {
553
- detect,
554
- allowed: [...keys, ...fullKeys],
555
- label: fullKeys.join("/"),
556
- json: {
557
- type: "object",
558
- properties: {
559
- ...Object.fromEntries(keys.map((k) => [k, { type: "string" }])),
560
- ...source.properties,
561
- },
562
- required: [...keys, ...(source.required ?? [])],
563
- additionalProperties: false,
564
- },
565
- parse: (checks.length
566
- ? refine(inner, ...checks)
567
- : inner) as unknown as ArmParse<Partial<ActionDecl>>,
568
- };
569
- }
570
-
571
- // [LAW:dataflow-not-control-flow] The value-source arms in the order their
572
- // labels appear in the exactly-one message. A `set` declares exactly one of
573
- // these; the dispatcher counts presence over `detect` and reconstructs
574
- // `{ set, ...payload }`.
575
- const SET_ARMS: readonly ValueSourceArm[] = [
576
- valueSourceArm("set", TO_FIELDS_SET),
577
- valueSourceArm("set", FROM_FIELDS_SET),
578
- valueSourceArm("set", BOUNDED_FIELDS, [minLessThanMax, byNonZero]),
579
- valueSourceArm("set", INT_FIELDS),
580
- valueSourceArm("set", CYCLE_FIELDS_SET),
581
- ];
582
-
583
- // [LAW:one-type-per-behavior] `persist` mirrors `set` minus the `int` arm — a
584
- // page cursor is a UI-only paging concept with no meaning as a persisted
585
- // config default (see action.ts's ActionDecl comment). `removeSegment`/
586
- // `insertSegment` are ADDITIONAL persist-only arms with no `set` counterpart.
587
- const PERSIST_ARMS: readonly ValueSourceArm[] = [
588
- valueSourceArm("persist", TO_FIELDS_PERSIST),
589
- valueSourceArm("persist", FROM_FIELDS_PERSIST),
590
- valueSourceArm("persist", BOUNDED_FIELDS, [minLessThanMax, byNonZero]),
591
- valueSourceArm("persist", CYCLE_FIELDS_PERSIST),
592
- valueSourceArm("persist", REMOVE_SEGMENT_FIELDS),
593
- // [LAW:no-mode-explosion] Both insertSegment arms narrow detectKeys to
594
- // their own discriminating field — see valueSourceArm's own comment. They
595
- // share "anchor"/"relation" (same position shape, different segment-name
596
- // source), so dispatching on the full field set would make EITHER arm
597
- // spuriously match an action declaring the other.
598
- valueSourceArm("persist", INSERT_SEGMENT_FIELDS, [], ["insertSegment"]),
599
- valueSourceArm(
600
- "persist",
601
- INSERT_SEGMENT_FROM_FIELDS,
602
- [],
603
- ["insertSegmentFrom"],
604
- ),
605
- ];
606
-
607
- // [LAW:one-type-per-behavior] A dual declares any value source BOTH
608
- // destinations share — `set` minus `int` (a page cursor has no durable
609
- // meaning), which is also `persist` minus its structural-edit arms (those are
610
- // persist-only by design, so they have no destination to choose between).
611
- // The field maps are the SET ones with dual wording, so a dual's value obeys
612
- // exactly the shape a `set` and a `persist` of that source each obey.
613
- const DUAL_ARMS: readonly ValueSourceArm[] = [
614
- valueSourceArm("dual", TO_FIELDS_DUAL),
615
- valueSourceArm("dual", FROM_FIELDS_DUAL),
616
- valueSourceArm("dual", BOUNDED_FIELDS, [minLessThanMax, byNonZero]),
617
- valueSourceArm("dual", CYCLE_FIELDS_DUAL),
618
- ];
619
-
620
- // [LAW:one-source-of-truth] The clause list, not the joined string, is the
621
- // data that varies per discriminator — the "or" belongs on the LAST clause
622
- // only, and which clause is last differs between `set` (ends at cycle) and
623
- // `persist` (ends at insertSegment), so building a list and joining it is
624
- // what keeps that placement correct without a second copy of the sentence.
625
- function valueSourceClauses(discriminator: Discriminator): string[] {
626
- const clauses = [
627
- `"to" (a literal value)`,
628
- `"from" (an option domain — a registered domain name like "themes"/"styles"/"looks", or an inline array of literal values)`,
629
- `"min"/"max"/"by" (a bounded step)`,
630
- ];
631
- if (discriminator === "set")
632
- clauses.push(`"int" (an unbounded integer cursor)`);
633
- clauses.push(`"cycle" (an enumerated domain stepped in order)`);
634
- if (discriminator === "persist") {
635
- clauses.push(
636
- `"removeSegment" (remove a named segment from the layout)`,
637
- `"insertSegment"/"anchor"/"relation" (insert a named segment before/after an existing one)`,
638
- `"insertSegmentFrom"/"anchor"/"relation" (insert a segment PICKED from an option domain before/after an existing one)`,
639
- );
640
- }
641
- return clauses;
642
- }
643
-
644
- function VALUE_SOURCE_MESSAGE(discriminator: Discriminator): string {
645
- const clauses = valueSourceClauses(discriminator);
646
- const last = clauses[clauses.length - 1]!;
647
- const list =
648
- clauses.length === 1
649
- ? last
650
- : `${clauses.slice(0, -1).join(", ")}, or ${last}`;
651
- return `a ${discriminator} action declares exactly one value source: ${list}`;
652
- }
653
-
654
- // [LAW:dataflow-not-control-flow] The set/persist sub-union eliminator:
655
- // validate the shared discriminator key, count which value sources are
656
- // present, require exactly one, reject keys outside that arm's allow-list,
657
- // parse the payload, reconstruct the member. The variability (which arms,
658
- // each arm's fields/refinements/allow-list) is the `arms` data; the only
659
- // branches are the presence-count and the null-threading both the key and
660
- // the payload share.
661
- function valueSourceAction(
662
- ctx: ValidateCtx,
663
- path: string,
664
- raw: Record<string, unknown>,
665
- discriminator: Discriminator,
666
- arms: readonly ValueSourceArm[],
667
- ): ActionDecl | null {
668
- const keys = validateDiscriminatorKeys(ctx, path, raw, discriminator);
669
-
670
- const present = arms.filter((arm) => arm.detect.some((k) => k in raw));
671
- if (present.length !== 1) {
672
- issue(
673
- ctx,
674
- path,
675
- `${VALUE_SOURCE_MESSAGE(discriminator)}${
676
- present.length > 1
677
- ? ` — found: ${present.map((a) => a.label).join(", ")}`
678
- : ""
679
- }`,
680
- );
681
- return null;
682
- }
683
- const arm = present[0]!;
684
-
685
- for (const k of Object.keys(raw)) {
686
- if (!arm.allowed.includes(k))
687
- issue(
688
- ctx,
689
- `${path}.${k}`,
690
- `Unknown key "${k}" on this ${discriminator} action. Expected one of: ${arm.allowed.join(", ")}`,
691
- );
692
- }
693
-
694
- const payload = arm.parse(ctx, path, raw);
695
- return keys === null || payload === null
696
- ? null
697
- : ({ ...keys, ...payload } as unknown as ActionDecl);
698
- }
699
-
700
- // [LAW:no-silent-fallbacks] A literal `to` and the discriminator key share
701
- // the non-empty/slash-free shape — the wire rejects empty values and splits
702
- // on "/", so either is undeliverable. The empty/slash messages are this
703
- // arm's, the shape is the shared enforcer's. Built once per discriminator
704
- // (see TO_FIELDS_SET/TO_FIELDS_PERSIST) so a `persist` action's message names
705
- // "persist value" and the set-config wire, never `set`'s wording.
706
- function setLiteralSpec(discriminator: Discriminator): FieldSpec<string> {
707
- const wire = wireName(discriminator);
708
- return {
709
- required: true,
710
- json: { type: "string" },
711
- parse: (ctx, path, field, raw) =>
712
- slashFreeString(
713
- ctx,
714
- path,
715
- field,
716
- raw,
717
- `${discriminator} value must be non-empty — an empty value cannot be delivered on the ${wire} wire`,
718
- (v) =>
719
- `${discriminator} value "${v}" contains "/" — ${discriminator} values must be slash-free`,
720
- ) ?? undefined,
721
- };
722
- }
723
-
724
- // [LAW:types-are-the-program] `from` is either a NAME (a non-empty string,
725
- // resolved against the option-domain registry) or an INLINE literal domain (a
726
- // non-empty array of deliverable wire values — the same non-empty/
727
- // slash-free wire shape `to` and `cycle` members enforce, plus the same
728
- // uniqueness `cycleSpec` requires: a duplicate has no successor-ambiguity
729
- // concern here, but it would render the same picker cell twice for no
730
- // benefit). This arm proves only the SHAPE; whether a named domain actually
731
- // resolves needs the merged config's per-config domains (e.g. "looks"), so
732
- // that check is a cross-reference concern (validateCrossReferences) —
733
- // symmetric to how a layout node's segment ref or a `{{ action }}` ref
734
- // resolves post-merge. Built once per discriminator, same reason as
735
- // setLiteralSpec.
736
- function fromSpec(discriminator: Discriminator): FieldSpec<OptionDomain> {
737
- const wire = wireName(discriminator);
738
- return {
739
- required: true,
740
- json: {
741
- anyOf: [
742
- { type: "string", minLength: 1 },
743
- {
744
- type: "array",
745
- items: { type: "string", minLength: 1 },
746
- minItems: 1,
747
- uniqueItems: true,
748
- },
749
- ],
750
- },
751
- parse: (ctx, path, field, raw) => {
752
- const from = raw[field];
753
- const at = `${path}.${field}`;
754
- if (typeof from === "string") {
755
- if (from === "") {
756
- issue(ctx, at, `${field} must be a non-empty domain name`);
757
- return undefined;
758
- }
759
- return from;
760
- }
761
- if (Array.isArray(from) && from.every((m) => typeof m === "string")) {
762
- const members = from as string[];
763
- if (members.length === 0) {
764
- issue(
765
- ctx,
766
- at,
767
- `${field} must name a domain (a non-empty string) or declare an inline domain (a non-empty array of values)`,
768
- );
769
- return undefined;
770
- }
771
- if (members.some((m) => m === "")) {
772
- issue(
773
- ctx,
774
- at,
775
- `${field} array members must be non-empty — an empty value cannot be delivered on the ${wire} wire`,
776
- );
777
- return undefined;
778
- }
779
- const slashed = members.filter((m) => m.includes("/"));
780
- if (slashed.length > 0) {
781
- issue(
782
- ctx,
783
- at,
784
- `${field} array member(s) ${slashed.map((m) => `"${m}"`).join(", ")} contain "/" — ${discriminator} values must be slash-free`,
785
- );
786
- return undefined;
787
- }
788
- if (new Set(members).size !== members.length) {
789
- issue(
790
- ctx,
791
- at,
792
- `${field} array members must be unique — a duplicated value would render the same picker option twice`,
793
- );
794
- return undefined;
795
- }
796
- return members;
797
- }
798
- issue(
799
- ctx,
800
- at,
801
- `${field} must be a domain name (a string) or an inline domain (an array of strings), got ${describeValue(from)}`,
802
- );
803
- return undefined;
804
- },
805
- };
806
- }
807
-
808
- // [LAW:types-are-the-program] `cycle` is the enumerated domain a click steps
809
- // through: at least two members (one member has no successor to step to — that
810
- // is a literal `to`), each a deliverable wire value (non-empty, slash-free
811
- // — the same wire shape `to` enforces), no duplicates (the successor of a
812
- // duplicated member is ambiguous). Members double as the derived allow-list
813
- // gate, so a member this spec admits is a value the wire delivers, by
814
- // construction. Built once per discriminator, same reason as setLiteralSpec.
815
- function cycleSpec(discriminator: Discriminator): FieldSpec<readonly string[]> {
816
- const wire = wireName(discriminator);
817
- return {
818
- required: true,
819
- json: {
820
- type: "array",
821
- items: { type: "string", minLength: 1 },
822
- minItems: 2,
823
- uniqueItems: true,
824
- },
825
- parse: (ctx, path, field, raw) => {
826
- const v = raw[field];
827
- const at = `${path}.${field}`;
828
- if (!Array.isArray(v) || v.some((m) => typeof m !== "string")) {
829
- issue(
830
- ctx,
831
- at,
832
- `cycle must be an array of strings (the enumerated values a click steps through), got ${describeType(v)}`,
833
- );
834
- return undefined;
835
- }
836
- const members = v as string[];
837
- if (members.length < 2) {
838
- issue(
839
- ctx,
840
- at,
841
- `cycle needs at least two members (one member has no successor — use a literal "to")`,
842
- );
843
- return undefined;
844
- }
845
- const empty = members.some((m) => m === "");
846
- const slashed = members.filter((m) => m.includes("/"));
847
- if (empty) {
848
- issue(
849
- ctx,
850
- at,
851
- `cycle members must be non-empty — an empty value cannot be delivered on the ${wire} wire`,
852
- );
853
- return undefined;
854
- }
855
- if (slashed.length > 0) {
856
- issue(
857
- ctx,
858
- at,
859
- `cycle member(s) ${slashed.map((m) => `"${m}"`).join(", ")} contain "/" — ${discriminator} values must be slash-free`,
860
- );
861
- return undefined;
862
- }
863
- if (new Set(members).size !== members.length) {
864
- issue(
865
- ctx,
866
- at,
867
- `cycle members must be unique — the successor of a duplicated member is ambiguous`,
868
- );
869
- return undefined;
870
- }
871
- return members;
872
- },
873
- };
874
- }
875
-
876
- // [LAW:no-silent-fallbacks] `int` is a marker, not a value — it declares the key
877
- // an unbounded-integer cursor. Only the literal `true` is meaningful; anything
878
- // else is a typo to surface, not silently coerce.
879
- function intMarkerSpec(): FieldSpec<true> {
880
- return {
881
- required: true,
882
- json: { const: true },
883
- parse: (ctx, path, field, raw) => {
884
- if (raw[field] !== true) {
885
- issue(
886
- ctx,
887
- `${path}.${field}`,
888
- `int must be the literal true (declares the key an unbounded integer cursor — a paged picker's page key), got ${describeValue(raw[field])}`,
889
- );
890
- return undefined;
891
- }
892
- return true;
893
- },
894
- };
895
- }
896
-
897
- // [LAW:one-source-of-truth] A layout op's segment-name field (removeSegment /
898
- // insertSegment / anchor) is non-empty and free of BOTH wire-structural
899
- // characters: `/` (the click wire's own multi-arg segment delimiter, the
900
- // same restriction slashFreeString already enforces for `to`/`cycle`) and
901
- // `:` (layout-ops.ts's op-token delimiter — a name containing it would make
902
- // encodeLayoutOp's output ambiguous to decode). One spec, three callsites,
903
- // so the two-character restriction can't drift between them.
904
- function layoutNameSpec(field: string): FieldSpec<string> {
905
- return {
906
- required: true,
907
- json: { type: "string" },
908
- parse: (ctx, path, f, raw) => {
909
- const v = requireString(ctx, path, raw, f);
910
- if (v === null) return undefined;
911
- const at = `${path}.${f}`;
912
- if (v === "") {
913
- issue(ctx, at, `${field} must be non-empty (a segment name)`);
914
- return undefined;
915
- }
916
- if (v.includes("/") || v.includes(":")) {
917
- issue(
918
- ctx,
919
- at,
920
- `${field} "${v}" contains "/" or ":" — segment names in a layout op must be free of both (the click wire's own delimiter and layout-ops.ts's op-token delimiter)`,
921
- );
922
- return undefined;
923
- }
924
- return v;
925
- },
926
- };
927
- }
928
-
929
- // [LAW:types-are-the-program] `relation` is a closed two-value enum, not a
930
- // free string — a typo (`"befor"`) is a load error, never a click-time
931
- // surprise. Mirrors intMarkerSpec's "one legal literal" shape, widened to
932
- // two.
933
- function relationSpec(): FieldSpec<"before" | "after"> {
934
- return {
935
- required: true,
936
- json: { enum: ["before", "after"] },
937
- parse: (ctx, path, field, raw) => {
938
- const v = raw[field];
939
- if (v !== "before" && v !== "after") {
940
- issue(
941
- ctx,
942
- `${path}.${field}`,
943
- `relation must be "before" or "after", got ${describeValue(v)}`,
944
- );
945
- return undefined;
946
- }
947
- return v;
948
- },
949
- };
950
- }
951
-
952
- // [LAW:types-are-the-program] A required integer field — the field key (min / max
953
- // / by) comes from the map, the message names it. A non-integer or absent value
954
- // reports and fails the arm.
955
- function requireIntSpec(): FieldSpec<number> {
956
- return {
957
- required: true,
958
- json: { type: "integer" },
959
- parse: (ctx, path, field, raw) => {
960
- const v = raw[field];
961
- if (typeof v !== "number" || !Number.isInteger(v)) {
962
- issue(
963
- ctx,
964
- `${path}.${field}`,
965
- `${field} must be an integer, got ${describeValue(v)}`,
966
- );
967
- return undefined;
968
- }
969
- return v;
970
- },
971
- };
972
- }