@promptctl/cc-candybar 1.25.0 → 1.27.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 (45) hide show
  1. package/dist/index.mjs +83 -76
  2. package/package.json +6 -6
  3. package/schema/cc-candybar.schema.json +193 -4
  4. package/src/check.ts +49 -27
  5. package/src/click/wire.ts +16 -0
  6. package/src/config/action.ts +57 -22
  7. package/src/config/default-dsl-config.ts +424 -55
  8. package/src/config/dsl-loader.ts +14 -2
  9. package/src/config/dsl-types.ts +59 -0
  10. package/src/config/loader/actions.ts +283 -109
  11. package/src/config/loader/cross-ref.ts +148 -28
  12. package/src/config/loader/emit-schema.ts +2 -0
  13. package/src/config/loader/globals.ts +118 -31
  14. package/src/config/loader/merge.ts +58 -1
  15. package/src/config/loader/persist-target.ts +32 -0
  16. package/src/config/loader/presets.ts +107 -0
  17. package/src/config/option-domain.ts +164 -0
  18. package/src/config/presets.ts +156 -0
  19. package/src/daemon/cache/git.ts +1 -1
  20. package/src/daemon/cache/render.ts +61 -7
  21. package/src/daemon/config-overrides-store.ts +322 -0
  22. package/src/daemon/paths.ts +10 -0
  23. package/src/daemon/render-payload.ts +84 -19
  24. package/src/daemon/server.ts +68 -55
  25. package/src/daemon/verbs/config-validators.ts +127 -0
  26. package/src/daemon/verbs/index.ts +129 -2
  27. package/src/daemon/verbs/state-validators.ts +98 -586
  28. package/src/daemon/verbs/validator-registry.ts +457 -0
  29. package/src/demo/dsl.ts +17 -10
  30. package/src/dsl/node-registry.ts +54 -39
  31. package/src/dsl/render.ts +158 -46
  32. package/src/help-text.ts +59 -0
  33. package/src/index.ts +2 -47
  34. package/src/install/index.ts +18 -4
  35. package/src/render/action.ts +155 -33
  36. package/src/render/active-segment.ts +78 -0
  37. package/src/render/menu.ts +16 -11
  38. package/src/render/picker.ts +51 -13
  39. package/src/render/segment-color.ts +74 -0
  40. package/src/segments/git.ts +389 -48
  41. package/src/template-engine/colors.ts +67 -45
  42. package/src/template-engine/engine.ts +11 -12
  43. package/src/themes/index.ts +1 -4
  44. package/src/themes/palette-resolvers.ts +22 -30
  45. package/src/themes/policy.ts +37 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptctl/cc-candybar",
3
- "version": "1.25.0",
3
+ "version": "1.27.0",
4
4
  "description": "Statusline renderer for Claude Code — a JSON5-configurable DSL with daemon-cached data sources, byte-clean palette-aware composition, and OSC8 click verbs.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -86,14 +86,14 @@
86
86
  "tsx": "^4.21.0",
87
87
  "typescript": "^5.0.0",
88
88
  "@promptctl/go-template-js": "^0.7.0",
89
- "@promptctl/rich-js": "^0.6.0",
89
+ "@promptctl/rich-js": "^0.7.0",
90
90
  "json5": "^2.2.3",
91
91
  "mobx": "^6.15.0"
92
92
  },
93
93
  "optionalDependencies": {
94
- "@promptctl/cc-candybar-darwin-arm64": "1.25.0",
95
- "@promptctl/cc-candybar-darwin-x64": "1.25.0",
96
- "@promptctl/cc-candybar-linux-x64": "1.25.0",
97
- "@promptctl/cc-candybar-linux-arm64": "1.25.0"
94
+ "@promptctl/cc-candybar-darwin-arm64": "1.27.0",
95
+ "@promptctl/cc-candybar-darwin-x64": "1.27.0",
96
+ "@promptctl/cc-candybar-linux-x64": "1.27.0",
97
+ "@promptctl/cc-candybar-linux-arm64": "1.27.0"
98
98
  }
99
99
  }
@@ -29,6 +29,9 @@
29
29
  "look": {
30
30
  "type": "string"
31
31
  },
32
+ "preset": {
33
+ "type": "string"
34
+ },
32
35
  "style": {
33
36
  "enum": [
34
37
  "powerline",
@@ -1141,10 +1144,20 @@
1141
1144
  "type": "string"
1142
1145
  },
1143
1146
  "from": {
1144
- "enum": [
1145
- "themes",
1146
- "styles",
1147
- "looks"
1147
+ "anyOf": [
1148
+ {
1149
+ "type": "string",
1150
+ "minLength": 1
1151
+ },
1152
+ {
1153
+ "type": "array",
1154
+ "items": {
1155
+ "type": "string",
1156
+ "minLength": 1
1157
+ },
1158
+ "minItems": 1,
1159
+ "uniqueItems": true
1160
+ }
1148
1161
  ]
1149
1162
  }
1150
1163
  },
@@ -1216,6 +1229,98 @@
1216
1229
  ],
1217
1230
  "additionalProperties": false
1218
1231
  },
1232
+ {
1233
+ "type": "object",
1234
+ "properties": {
1235
+ "persist": {
1236
+ "type": "string"
1237
+ },
1238
+ "to": {
1239
+ "type": "string"
1240
+ }
1241
+ },
1242
+ "required": [
1243
+ "persist",
1244
+ "to"
1245
+ ],
1246
+ "additionalProperties": false
1247
+ },
1248
+ {
1249
+ "type": "object",
1250
+ "properties": {
1251
+ "persist": {
1252
+ "type": "string"
1253
+ },
1254
+ "from": {
1255
+ "anyOf": [
1256
+ {
1257
+ "type": "string",
1258
+ "minLength": 1
1259
+ },
1260
+ {
1261
+ "type": "array",
1262
+ "items": {
1263
+ "type": "string",
1264
+ "minLength": 1
1265
+ },
1266
+ "minItems": 1,
1267
+ "uniqueItems": true
1268
+ }
1269
+ ]
1270
+ }
1271
+ },
1272
+ "required": [
1273
+ "persist",
1274
+ "from"
1275
+ ],
1276
+ "additionalProperties": false
1277
+ },
1278
+ {
1279
+ "type": "object",
1280
+ "properties": {
1281
+ "persist": {
1282
+ "type": "string"
1283
+ },
1284
+ "min": {
1285
+ "type": "integer"
1286
+ },
1287
+ "max": {
1288
+ "type": "integer"
1289
+ },
1290
+ "by": {
1291
+ "type": "integer"
1292
+ }
1293
+ },
1294
+ "required": [
1295
+ "persist",
1296
+ "min",
1297
+ "max",
1298
+ "by"
1299
+ ],
1300
+ "additionalProperties": false
1301
+ },
1302
+ {
1303
+ "type": "object",
1304
+ "properties": {
1305
+ "persist": {
1306
+ "type": "string"
1307
+ },
1308
+ "cycle": {
1309
+ "type": "array",
1310
+ "items": {
1311
+ "type": "string",
1312
+ "minLength": 1
1313
+ },
1314
+ "minItems": 2,
1315
+ "uniqueItems": true
1316
+ }
1317
+ },
1318
+ "required": [
1319
+ "persist",
1320
+ "cycle"
1321
+ ],
1322
+ "additionalProperties": false
1323
+ },
1219
1324
  {
1220
1325
  "type": "object",
1221
1326
  "properties": {
@@ -1239,6 +1344,18 @@
1239
1344
  "open"
1240
1345
  ],
1241
1346
  "additionalProperties": false
1347
+ },
1348
+ {
1349
+ "type": "object",
1350
+ "properties": {
1351
+ "reset": {
1352
+ "type": "string"
1353
+ }
1354
+ },
1355
+ "required": [
1356
+ "reset"
1357
+ ],
1358
+ "additionalProperties": false
1242
1359
  }
1243
1360
  ]
1244
1361
  }
@@ -1265,6 +1382,78 @@
1265
1382
  "additionalProperties": false
1266
1383
  }
1267
1384
  },
1385
+ "presets": {
1386
+ "type": "object",
1387
+ "additionalProperties": {
1388
+ "type": "object",
1389
+ "properties": {
1390
+ "root": {
1391
+ "$ref": "#/definitions/LayoutNode"
1392
+ },
1393
+ "globals": {
1394
+ "type": "object",
1395
+ "properties": {
1396
+ "default_bg": {
1397
+ "type": "string"
1398
+ },
1399
+ "default_fg": {
1400
+ "type": "string"
1401
+ },
1402
+ "default_empty_value": {
1403
+ "type": "string"
1404
+ },
1405
+ "default_separator": {
1406
+ "type": "string"
1407
+ },
1408
+ "default_truncate_marker": {
1409
+ "type": "string"
1410
+ },
1411
+ "palette": {
1412
+ "type": "string"
1413
+ },
1414
+ "look": {
1415
+ "type": "string"
1416
+ },
1417
+ "preset": {
1418
+ "not": {},
1419
+ "description": "not allowed inside a preset — a preset cannot select a preset"
1420
+ },
1421
+ "style": {
1422
+ "enum": [
1423
+ "powerline",
1424
+ "capsule",
1425
+ "plain"
1426
+ ]
1427
+ },
1428
+ "autoWrap": {
1429
+ "type": "boolean"
1430
+ },
1431
+ "padding": {
1432
+ "type": "integer",
1433
+ "minimum": 0,
1434
+ "maximum": 16
1435
+ },
1436
+ "charset": {
1437
+ "enum": [
1438
+ "unicode",
1439
+ "ascii"
1440
+ ]
1441
+ },
1442
+ "colorCompatibility": {
1443
+ "enum": [
1444
+ "truecolor",
1445
+ "256",
1446
+ "ansi",
1447
+ "none"
1448
+ ]
1449
+ }
1450
+ },
1451
+ "additionalProperties": false
1452
+ }
1453
+ },
1454
+ "additionalProperties": false
1455
+ }
1456
+ },
1268
1457
  "helpers": {
1269
1458
  "type": "object",
1270
1459
  "additionalProperties": {
package/src/check.ts CHANGED
@@ -37,13 +37,15 @@ import {
37
37
  lookKeyByName,
38
38
  effectiveStripStyle,
39
39
  } from "./themes/policy.js";
40
- import { resolverForThemeName } from "./themes/palette-resolvers.js";
40
+ import { paletteForThemeName } from "./themes/palette-resolvers.js";
41
+ import { effectivePresetName, presetGlobals } from "./config/presets.js";
41
42
  import {
42
43
  DEFAULT_CHARSET,
43
44
  DEFAULT_COLOR_COMPATIBILITY,
44
45
  DEFAULT_PADDING,
45
46
  DEFAULT_WRAP,
46
47
  } from "./render/strip.js";
48
+ import type { EffectiveGlobals } from "./daemon/render-payload.js";
47
49
 
48
50
  // [LAW:no-ambient-temporal-coupling] A fixed width keeps the verdict a function
49
51
  // of the config alone, not of whichever terminal invoked the check. Templates
@@ -60,16 +62,16 @@ const CHECK_WIDTH = 200;
60
62
  // field-name typo in the git/directory/metrics/budget branches slip through —
61
63
  // those branches only run when their data is present.
62
64
  //
63
- // `effectiveTheme` is threaded in exactly as the daemon threads it (server.ts
64
- // resolves effectiveThemeName once and feeds BOTH the payload's
65
- // `theme.effective` field and the basePalette below) [LAW:one-source-of-truth].
65
+ // `effective` is threaded in exactly as the daemon threads it (server.ts
66
+ // resolves one EffectiveGlobals struct per render and feeds BOTH the
67
+ // payload's `*.effective` fields and BuildLineOptions/basePalette below)
68
+ // [LAW:one-source-of-truth].
66
69
  //
67
70
  // test/example-configs.test.ts asserts rendered content against these literal
68
71
  // values (780s → "◷ 13m", cost $0.39, version 1.15.0, …); changing one here
69
72
  // fails that suite loudly rather than drifting silently.
70
73
  export function checkPayload(
71
- effectiveTheme: string,
72
- effectiveLook: string,
74
+ effective: EffectiveGlobals,
73
75
  ): Record<string, unknown> {
74
76
  const home = "/home/tester";
75
77
  const nowSec = Math.floor(Date.now() / 1000);
@@ -87,6 +89,7 @@ export function checkPayload(
87
89
  },
88
90
  git: {
89
91
  repoName: "cc-candybar",
92
+ repoUrl: "https://github.com/promptctl/cc-candybar",
90
93
  branch: "main",
91
94
  sha: "abc1234",
92
95
  ahead: 2,
@@ -116,8 +119,13 @@ export function checkPayload(
116
119
  weekly: { percentage: 21, resetsAt: nowSec + 5 * 86400 },
117
120
  cache: { expiresAt: nowSec + 15 * 60 },
118
121
  tmux: { session: "work" },
119
- theme: { effective: effectiveTheme },
120
- look: { effective: effectiveLook },
122
+ theme: { effective: effective.theme },
123
+ look: { effective: effective.look },
124
+ style: { effective: effective.style },
125
+ charset: { effective: effective.charset },
126
+ colorCompatibility: { effective: effective.colorCompatibility },
127
+ autoWrap: { effective: effective.autoWrap },
128
+ padding: { effective: effective.padding },
121
129
  };
122
130
  }
123
131
 
@@ -271,18 +279,30 @@ function loadRegisterRender(
271
279
  // validator registry, which a one-shot check has no wire to serve.
272
280
  deriveActionValidators(config);
273
281
 
274
- // Fresh session (no clicked theme/style), so the session half of each
275
- // resolution is null — the config default over the floor, exactly what the
276
- // daemon renders for a session that has never clicked.
277
- const effectiveTheme = effectiveThemeName(null, config.globals.palette);
278
- // Same fresh-session shape one dimension over: no clicked look, so the
279
- // config default over the "none" floor exactly what the daemon renders
280
- // for a session that has never clicked.
281
- const effectiveLook = effectiveLookName(
282
+ // Fresh session (no clicked theme/style/look), so the session half of
283
+ // each resolution is null — the config default over the floor, exactly
284
+ // what the daemon renders for a session that has never clicked.
285
+ // [LAW:one-source-of-truth] The preset resolves first and its fragment's
286
+ // globals feed every field below, the SAME order the daemon resolves in
287
+ // (server.ts) so `check` renders the arrangement a fresh session actually
288
+ // opens in, not the config's un-presetted root.
289
+ const preset = effectivePresetName(
282
290
  null,
283
- config.globals.look,
284
- config.looks,
291
+ config.globals.preset,
292
+ config.presets,
285
293
  );
294
+ const globals = presetGlobals(config, preset);
295
+ const effective: EffectiveGlobals = {
296
+ preset,
297
+ theme: effectiveThemeName(null, globals.palette),
298
+ look: effectiveLookName(null, globals.look, config.looks),
299
+ style: effectiveStripStyle(null, globals.style),
300
+ autoWrap: globals.autoWrap ?? DEFAULT_WRAP,
301
+ padding: globals.padding ?? DEFAULT_PADDING,
302
+ charset: globals.charset ?? DEFAULT_CHARSET,
303
+ colorCompatibility:
304
+ globals.colorCompatibility ?? DEFAULT_COLOR_COMPATIBILITY,
305
+ };
286
306
  // [LAW:no-silent-failure] A segment whose template THROWS while evaluating
287
307
  // (an `{{ action }}` display-arity mismatch, a MissingFieldError from a
288
308
  // partially-declared variable) renders as a visible ⚠ error cell — partial
@@ -296,22 +316,24 @@ function loadRegisterRender(
296
316
  compiled,
297
317
  store,
298
318
  registry,
299
- checkPayload(effectiveTheme, effectiveLook),
300
- resolverForThemeName(effectiveTheme),
319
+ checkPayload(effective),
320
+ paletteForThemeName(effective.theme),
301
321
  {
302
- style: effectiveStripStyle(null, config.globals.style),
322
+ style: effective.style,
303
323
  width: CHECK_WIDTH,
304
- colorCompatibility:
305
- config.globals.colorCompatibility ?? DEFAULT_COLOR_COMPATIBILITY,
306
- wrap: config.globals.autoWrap ?? DEFAULT_WRAP,
307
- padding: config.globals.padding ?? DEFAULT_PADDING,
308
- charset: config.globals.charset ?? DEFAULT_CHARSET,
324
+ colorCompatibility: effective.colorCompatibility,
325
+ wrap: effective.autoWrap,
326
+ padding: effective.padding,
327
+ charset: effective.charset,
309
328
  },
310
329
  {
311
330
  onSegmentError: (segName, message) =>
312
331
  segmentErrors.push(`segment "${segName}": ${message}`),
313
332
  },
314
- lookKeyByName(config.looks, effectiveLook),
333
+ {
334
+ look: lookKeyByName(config.looks, effective.look),
335
+ preset: effective.preset,
336
+ },
315
337
  );
316
338
  if (segmentErrors.length > 0) {
317
339
  throw new Error(
package/src/click/wire.ts CHANGED
@@ -50,6 +50,22 @@ export const VERB_SHOW_CONFIG_WARNING = "show-config-warning";
50
50
  // reads it at the cache-lookup boundary. Clicking a different config is a
51
51
  // side-effect isolated to the verb handler; the renderer only sees the result.
52
52
  export const VERB_LOAD_CONFIG = "load-config";
53
+ // [LAW:one-source-of-truth] `persist`'s twin of set-state/step-state: writes
54
+ // land in the daemon-owned config-overrides layer (never the hand-authored
55
+ // config file), which RenderCache merges on top of the user file every
56
+ // reload — the SAME file-watcher path a hand edit already takes
57
+ // (candybar-config-engine-71o.2). Args: `[sessionId, key, value]` — the
58
+ // sessionId is carried only for click.error surfacing, exactly like
59
+ // set-state; the write itself is daemon-global, not session-scoped.
60
+ export const VERB_SET_CONFIG = "set-config";
61
+ // [LAW:types-are-the-program] A RELATIVE nudge to a bounded config-overrides
62
+ // key (e.g. a padding stepper) — the config twin of step-state. Args:
63
+ // `[sessionId, key, by]`.
64
+ export const VERB_STEP_CONFIG = "step-config";
65
+ // [LAW:one-source-of-truth] The gated undo for `persist`: clears one
66
+ // config-overrides key, restoring the user-file/bundled-default value on the
67
+ // next reload. Args: `[sessionId, key]`.
68
+ export const VERB_RESET_CONFIG = "reset-config";
53
69
 
54
70
  // [LAW:types-are-the-program] An effect to EMIT: a verb plus its raw (unencoded)
55
71
  // positional args. The wire owns all encoding — callers never percent-encode.
@@ -24,24 +24,26 @@
24
24
  // These are the shapes a picker draws options from and the set/copy/open/int
25
25
  // discriminator the loader and the validator-derivation match on.
26
26
 
27
- // [LAW:one-source-of-truth] The domain lists a picker draws options from. Same
28
- // canonical sources the `themes()`/`styles()`/`looks()` bindings and the
29
- // set-state validators consult the rendered options and the derived gate
30
- // cannot diverge because there is no second enumeration. themes/styles are
31
- // static registry lists; "looks" is the one PER-CONFIG domain (the merged
32
- // `looks` block's names), so its resolution sites take the config's look names
33
- // as data rather than consulting a module constant.
34
- export type OptionSource = "themes" | "styles" | "looks";
35
- export const OPTION_SOURCES: readonly OptionSource[] = [
36
- "themes",
37
- "styles",
38
- "looks",
39
- ];
27
+ // [LAW:one-source-of-truth] The domain a picker draws options from. Resolved
28
+ // through option-domain.ts's registry themes/styles are registry-backed
29
+ // static lists, "looks" is the one PER-CONFIG domain (the merged `looks`
30
+ // block's names, threaded as data rather than consulted from a module
31
+ // constant), and an inline array is its own domain, needing no registration
32
+ // at all. Re-exported here so ActionDecl stays self-contained to read.
33
+ import type { OptionDomain } from "./option-domain.js";
34
+ export type { OptionDomain } from "./option-domain.js";
40
35
 
41
36
  // [LAW:types-are-the-program] The top-level discriminator of an ActionDecl — the
42
37
  // click effect is keyed by which of these is present. The loader proves
43
38
  // exactly-one-of; the renderer and validator-derivation match with no fallthrough.
44
- export const ACTION_KEYS = ["set", "copy", "open"] as const;
39
+ //
40
+ // [LAW:one-source-of-truth] `persist` is `set`'s PERSISTENT twin
41
+ // (candybar-config-engine-71o.2): `set` mutates per-session SessionState,
42
+ // `persist` mutates the config's `globals` DEFAULT through the daemon-owned
43
+ // overrides layer (never the hand-authored config file). `reset` clears one
44
+ // persisted override — the gated undo `persist` needs, since a machine-owned
45
+ // write with no way back would be a one-way ratchet.
46
+ export const ACTION_KEYS = ["set", "persist", "copy", "open", "reset"] as const;
45
47
  export type ActionKey = (typeof ACTION_KEYS)[number];
46
48
 
47
49
  // [LAW:types-are-the-program] An ActionDecl is the click effect a named action
@@ -53,7 +55,9 @@ export type ActionKey = (typeof ACTION_KEYS)[number];
53
55
  //
54
56
  // set + to — write a literal value -> allow-list {to}
55
57
  // set + from — write the option the template binds at render
56
- // (a picker ranges the domain) -> allow-list {options}
58
+ // (a picker ranges the domain a registered name like
59
+ // "themes", or an inline literal array) -> allow-list
60
+ // {options}
57
61
  // set + min/max/by — write wrap(current ± by) clamped to [min,max]
58
62
  // (a stepper affordance) -> range [min,max]
59
63
  // set + int — write any integer the render binds (a paged cursor:
@@ -72,12 +76,20 @@ export type ActionKey = (typeof ACTION_KEYS)[number];
72
76
  // copy — copy templated text to the clipboard -> no gate
73
77
  // open — open a templated target in the editor -> no gate
74
78
  //
75
- // [LAW:one-source-of-truth] Only `set` writes SessionState, so only `set`
76
- // derives a validator. copy/open write nothing they derive nothing. The
77
- // vocabulary grows by arms (a future `run`/`open-url`), not by validator plumbing.
79
+ // [LAW:one-source-of-truth] `set` writes SessionState and `persist` writes
80
+ // the config-overrides layer, so only those two derive a validator (through
81
+ // the SAME shared registry algebra see validator-registry.ts). copy/open/
82
+ // reset write nothing SPEC-shaped (reset's target is gated by key membership,
83
+ // not a value domain) — they derive nothing. The vocabulary grows by arms (a
84
+ // future `run`/`open-url`), not by validator plumbing.
85
+ //
86
+ // [LAW:one-type-per-behavior] `persist` mirrors `set`'s four value-source
87
+ // arms verbatim (to/from/min-max-by/cycle) MINUS `int`: an unbounded page
88
+ // cursor is a UI-only paging concept (a picker's own navigation state) with
89
+ // no meaning as a persisted config default.
78
90
  export type ActionDecl =
79
91
  | { readonly set: string; readonly to: string }
80
- | { readonly set: string; readonly from: OptionSource }
92
+ | { readonly set: string; readonly from: OptionDomain }
81
93
  | {
82
94
  readonly set: string;
83
95
  readonly min: number;
@@ -86,13 +98,36 @@ export type ActionDecl =
86
98
  }
87
99
  | { readonly set: string; readonly int: true }
88
100
  | { readonly set: string; readonly cycle: readonly string[] }
101
+ | { readonly persist: string; readonly to: string }
102
+ | { readonly persist: string; readonly from: OptionDomain }
103
+ | {
104
+ readonly persist: string;
105
+ readonly min: number;
106
+ readonly max: number;
107
+ readonly by: number;
108
+ }
109
+ | { readonly persist: string; readonly cycle: readonly string[] }
89
110
  | { readonly copy: string }
90
- | { readonly open: string };
111
+ | { readonly open: string }
112
+ | { readonly reset: string };
91
113
 
92
114
  // [LAW:dataflow-not-control-flow] Does this action write a SessionState key? A
93
115
  // `set` action composes a set-state click URL whose first segment is session.id;
94
- // copy/open embed none. One predicate the loader's session.id requirement folds
95
- // over — no per-arm branching at the callsite.
116
+ // copy/open/persist/reset embed none. One predicate the loader's session.id
117
+ // requirement folds over — no per-arm branching at the callsite.
96
118
  export function actionBindsSet(a: ActionDecl): boolean {
97
119
  return "set" in a;
98
120
  }
121
+
122
+ // [LAW:dataflow-not-control-flow] Does this action write the config-overrides
123
+ // layer? Mirrors actionBindsSet for the `persist` arm.
124
+ export function actionBindsPersist(a: ActionDecl): boolean {
125
+ return "persist" in a;
126
+ }
127
+
128
+ // [LAW:dataflow-not-control-flow] Does this action clear a config-overrides
129
+ // key? `reset` carries session.id on the wire too (for click-error surfacing,
130
+ // same as set/persist), so it joins the same requirement.
131
+ export function actionBindsReset(a: ActionDecl): boolean {
132
+ return "reset" in a;
133
+ }