@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,260 +0,0 @@
1
- // [LAW:types-are-the-program] The variable schema: a VariableDecl is discriminated
2
- // by `kind` (literal / input / env / file / shell / template / time / git / state),
3
- // declared as DATA (VARIABLE_SCHEMA) and interpreted by the tag-by-field-value
4
- // engine (taggedUnion). Each arm is a `fields` schema over its member's non-`kind`
5
- // fields, except `input`, whose `default` must match its `type` — a genuine
6
- // cross-field invariant carried as a closure. This file changes when a source
7
- // kind's shape changes; adding a kind is one new arm here plus its runtime impl.
8
-
9
- import {
10
- GIT_FIELDS,
11
- type GitField,
12
- type EnvVarDecl,
13
- type FileVarDecl,
14
- type InputVarDecl,
15
- type GitVarDecl,
16
- type LiteralVarDecl,
17
- type ShellVarDecl,
18
- type StateVarDecl,
19
- type TemplateVarDecl,
20
- type TimeVarDecl,
21
- type VariableDecl,
22
- } from "../dsl-types.js";
23
- import { findKeyLine } from "./diagnostics.js";
24
- import {
25
- describeType,
26
- fields,
27
- isPlainObject,
28
- objectJson,
29
- optionalStringSpec,
30
- optionalTypedDefault,
31
- requireStringSpec,
32
- optionalEnumSpec,
33
- taggedUnion,
34
- taggedUnionJson,
35
- withConst,
36
- type FieldSpec,
37
- type FieldSpecMap,
38
- type JsonNode,
39
- type TaggedArm,
40
- type TaggedUnionSchema,
41
- type ValidateCtx,
42
- } from "./validate-core.js";
43
- import {
44
- optionalCacheSpec,
45
- requireCacheSpec,
46
- ttlOnlyCacheSpec,
47
- } from "./cache.js";
48
-
49
- export function validateVariables(
50
- ctx: ValidateCtx,
51
- pathPrefix: string,
52
- raw: unknown,
53
- ): Record<string, VariableDecl> {
54
- if (raw === undefined) return {};
55
- if (!isPlainObject(raw)) {
56
- ctx.issues.push({
57
- path: pathPrefix,
58
- message: `${pathPrefix} must be an object, got ${describeType(raw)}`,
59
- line: findKeyLine(ctx.source, pathPrefix.split(".")),
60
- });
61
- return {};
62
- }
63
-
64
- const out: Record<string, VariableDecl> = {};
65
- for (const [name, decl] of Object.entries(raw)) {
66
- const parsed = taggedUnion(
67
- ctx,
68
- VARIABLE_SCHEMA,
69
- `${pathPrefix}.${name}`,
70
- decl,
71
- );
72
- if (parsed !== null) out[name] = parsed;
73
- }
74
- return out;
75
- }
76
-
77
- // [LAW:types-are-the-program] `value` is a required union literal with a bespoke
78
- // message whose line points at the variable (not `.value`) — a custom spec, since
79
- // the generic string/enum specs encode different line behavior.
80
- function literalValueSpec(): FieldSpec<string | number | boolean> {
81
- return {
82
- required: true,
83
- json: { type: ["string", "number", "boolean"] },
84
- parse: (ctx, path, _field, raw) => {
85
- const value = raw.value;
86
- if (
87
- typeof value !== "string" &&
88
- typeof value !== "number" &&
89
- typeof value !== "boolean"
90
- ) {
91
- ctx.issues.push({
92
- path: `${path}.value`,
93
- message: `literal value must be string|number|boolean, got ${describeType(value)}`,
94
- line: findKeyLine(ctx.source, path.split(".")),
95
- });
96
- return undefined;
97
- }
98
- return value;
99
- },
100
- };
101
- }
102
-
103
- // [LAW:types-are-the-program] `field` is a required member of the closed GitField
104
- // set with a bespoke one-of message — a custom spec for the same reason.
105
- function gitFieldSpec(): FieldSpec<GitField> {
106
- return {
107
- required: true,
108
- json: { enum: [...GIT_FIELDS] },
109
- parse: (ctx, path, _field, raw) => {
110
- const field = raw.field;
111
- if (
112
- typeof field !== "string" ||
113
- !GIT_FIELDS.includes(field as GitField)
114
- ) {
115
- ctx.issues.push({
116
- path: `${path}.field`,
117
- message: `git field must be one of: ${GIT_FIELDS.join(", ")}, got ${JSON.stringify(field)}`,
118
- line: findKeyLine(ctx.source, [...path.split("."), "field"]),
119
- });
120
- return undefined;
121
- }
122
- return field as GitField;
123
- },
124
- };
125
- }
126
-
127
- // [LAW:types-are-the-program] `input`'s `default` carries the cross-field
128
- // invariant — it must match the declared `type` (an absent or invalid `type`
129
- // defaults the check to "string"). A field spec receives the WHOLE record, so it
130
- // reads its sibling `raw.type` to pick the expected type WITHOUT re-reporting a
131
- // bad type (the `type` field spec owns that error — reading raw here avoids the
132
- // duplicate issue). This is what lets `input` derive BOTH `parse` and `json` from
133
- // one field map via `arm()`, like every other arm — closing the last spot where
134
- // the two interpreters were authored independently [LAW:one-source-of-truth].
135
- function inputDefaultSpec(): FieldSpec<string | number | boolean> {
136
- return {
137
- required: false,
138
- json: { type: ["string", "number", "boolean"] },
139
- parse: (ctx, path, _field, raw) => {
140
- const t = raw.type;
141
- const expected =
142
- t === "number" || t === "boolean" || t === "string" ? t : "string";
143
- return optionalTypedDefault(ctx, path, raw, expected);
144
- },
145
- };
146
- }
147
-
148
- // [LAW:dataflow-not-control-flow] Each arm's field set is DATA over the member's
149
- // non-`kind` fields; the engine supplies the discriminator. `fields` runs every
150
- // spec (reporting all issues) and fails the arm when a required field is absent
151
- // or invalid — the conditional-spread + result-threading the old switch hand-rolled.
152
- const LITERAL_FIELDS: FieldSpecMap<Omit<LiteralVarDecl, "kind">> = {
153
- value: literalValueSpec(),
154
- default: optionalStringSpec(),
155
- };
156
- const INPUT_FIELDS: FieldSpecMap<Omit<InputVarDecl, "kind">> = {
157
- path: requireStringSpec(),
158
- type: optionalEnumSpec(["string", "number", "boolean"] as const),
159
- default: inputDefaultSpec(),
160
- };
161
- const ENV_FIELDS: FieldSpecMap<Omit<EnvVarDecl, "kind">> = {
162
- name: requireStringSpec(),
163
- default: optionalStringSpec(),
164
- };
165
- const FILE_FIELDS: FieldSpecMap<Omit<FileVarDecl, "kind">> = {
166
- path: requireStringSpec(),
167
- readMode: optionalEnumSpec(["whole", "first-line"] as const),
168
- regex: optionalStringSpec(),
169
- cache: requireCacheSpec("file"),
170
- default: optionalStringSpec(),
171
- };
172
- const SHELL_FIELDS: FieldSpecMap<Omit<ShellVarDecl, "kind">> = {
173
- command: requireStringSpec(),
174
- regex: optionalStringSpec(),
175
- cache: requireCacheSpec("shell"),
176
- default: optionalStringSpec(),
177
- };
178
- const TEMPLATE_FIELDS: FieldSpecMap<Omit<TemplateVarDecl, "kind">> = {
179
- template: requireStringSpec(),
180
- cache: optionalCacheSpec(),
181
- default: optionalStringSpec(),
182
- };
183
- const TIME_FIELDS: FieldSpecMap<Omit<TimeVarDecl, "kind">> = {
184
- layout: requireStringSpec(),
185
- // [LAW:no-silent-failure] ttl-only: the runtime honors no other invalidation
186
- // on a clock-driven var, so the loader rejects what it would otherwise have
187
- // had to silently coerce.
188
- cache: ttlOnlyCacheSpec(),
189
- default: optionalStringSpec(),
190
- };
191
- const GIT_VAR_FIELDS: FieldSpecMap<Omit<GitVarDecl, "kind">> = {
192
- field: gitFieldSpec(),
193
- cache: requireCacheSpec("git"),
194
- default: optionalStringSpec(),
195
- };
196
- const STATE_FIELDS: FieldSpecMap<Omit<StateVarDecl, "kind">> = {
197
- key: requireStringSpec(),
198
- default: optionalStringSpec(),
199
- };
200
-
201
- // [LAW:decomposition] A regular arm parses its non-`kind` fields via `fields` and
202
- // re-attaches the tag the engine already validated; null threading is preserved.
203
- function arm<
204
- K extends VariableDecl["kind"],
205
- M extends Omit<Extract<VariableDecl, { kind: K }>, "kind">,
206
- >(
207
- kind: K,
208
- fieldMap: FieldSpecMap<M>,
209
- ): TaggedArm<Extract<VariableDecl, { kind: K }>> {
210
- return {
211
- // [LAW:one-source-of-truth] The arm's emit facet: the member object schema
212
- // with its `kind` discriminator baked in — `objectJson` over the SAME field
213
- // map `fields` validates, plus `{ kind: { const } }`. taggedUnionJson collects
214
- // these verbatim into the union's anyOf.
215
- json: withConst(objectJson(fieldMap), "kind", kind),
216
- parse: (ctx: ValidateCtx, path: string, raw: Record<string, unknown>) => {
217
- const body = fields(ctx, fieldMap, path, raw);
218
- // [LAW:types-are-the-program] `fieldMap: FieldSpecMap<M>` is checked against
219
- // the member's non-`kind` fields at each call site, so {kind, ...body} IS the
220
- // member; TS can't relate the reconstruction to the distributed Extract for a
221
- // generic K, hence the cast — the call-site check carries the real guarantee.
222
- return body === null
223
- ? null
224
- : ({ kind, ...body } as unknown as Extract<VariableDecl, { kind: K }>);
225
- },
226
- };
227
- }
228
-
229
- const VARIABLE_SCHEMA: TaggedUnionSchema<VariableDecl, "kind"> = {
230
- tag: "kind",
231
- noun: "source kind",
232
- arms: {
233
- literal: arm("literal", LITERAL_FIELDS),
234
- // [LAW:one-source-of-truth] `input`'s `default`/`type` cross-field invariant
235
- // lives in `inputDefaultSpec` (a field spec reading its sibling), so `input`
236
- // is one field map like every other arm — `arm()` derives both `parse` and
237
- // `json` from INPUT_FIELDS, no hand-authored schema to keep in sync.
238
- input: arm("input", INPUT_FIELDS),
239
- env: arm("env", ENV_FIELDS),
240
- file: arm("file", FILE_FIELDS),
241
- shell: arm("shell", SHELL_FIELDS),
242
- template: arm("template", TEMPLATE_FIELDS),
243
- time: arm("time", TIME_FIELDS),
244
- git: arm("git", GIT_VAR_FIELDS),
245
- state: arm("state", STATE_FIELDS),
246
- },
247
- };
248
-
249
- // [LAW:one-source-of-truth] One VariableDecl's schema, derived from the SAME
250
- // VARIABLE_SCHEMA the validator interprets — the tag-by-kind anyOf.
251
- export function variableDeclJson(): JsonNode {
252
- return taggedUnionJson(VARIABLE_SCHEMA);
253
- }
254
-
255
- // [LAW:one-source-of-truth] The `variables` block (and a segment's nested `vars`)
256
- // is a name → VariableDecl map; both surfaces emit this one shape, symmetric to
257
- // both calling `validateVariables`.
258
- export function variablesMapJson(): JsonNode {
259
- return { type: "object", additionalProperties: variableDeclJson() };
260
- }
@@ -1,139 +0,0 @@
1
- // [LAW:one-source-of-truth] THE derivation of a menu's disclosure identity — the
2
- // single place both the loader (which SYNTHESIZES the state var + cycle action)
3
- // and the renderer (which READS openness + emits the toggle) agree on "which key
4
- // holds which open menu". A `{{ menu }}` helper is context-free about its NAME
5
- // (it cannot see the segment it sits in), so the loader and the render walk both
6
- // derive identity from the same two facts — the host segment name and the menu's
7
- // own apply-action name — and MUST produce identical strings or a click would
8
- // write a key the render never reads. Keeping the rule in one module makes that
9
- // agreement structural rather than a coincidence of two copies.
10
- //
11
- // [LAW:decomposition] A menu's identity is `(stateKey, member)`:
12
- // • member = the menu's apply-action name — unique per menu within a segment,
13
- // so two menus in ONE segment are distinct.
14
- // • stateKey = the SessionState key whose value names the open member. By
15
- // DEFAULT each menu owns a UNIQUE key (`menus.<seg>.<apply>`), so
16
- // it toggles only itself — menus are INDEPENDENT. Passing an
17
- // explicit shared key makes sibling menus share one key
18
- // (`menus.<key>`); one key holds one open member, so they become
19
- // mutually exclusive (an accordion) — exactly group sugar's shared-
20
- // key mechanism, selected by a VALUE not a mode
21
- // [LAW:dataflow-not-control-flow]. There is no implicit "the row is
22
- // the key" position magic: identity depends only on names a reader
23
- // can see in the template, never on tree position.
24
-
25
- import { ident } from "./ident.js";
26
-
27
- // [LAW:one-source-of-truth] The reserved namespace every synthesized menu
28
- // artifact (state var + cycle action) lives under, mirroring group sugar's
29
- // `groups.`. A user-authored name under this prefix is a load error so synthesis
30
- // can never silently collide.
31
- export const MENU_NS = "menus.";
32
-
33
- // [LAW:one-source-of-truth] The closed sentinel and the ▸/▾ glyphs are the shared
34
- // disclosure primitive, not menu-specific — they live in src/config/disclosure.ts
35
- // (DISCLOSURE_CLOSED / DISCLOSURE_GLYPH_*) so group sugar and {{ menu }} cannot
36
- // drift. This module keeps only the menu's own IDENTITY derivation (member = apply
37
- // name; key = optional shared key), which group sugar derives differently.
38
-
39
- // [LAW:single-enforcer] A menu's member name IS its apply-action name. Both the
40
- // loader and the helper call this so neither restates the rule.
41
- export function menuMember(applyName: string): string {
42
- return applyName;
43
- }
44
-
45
- // [LAW:single-enforcer] THE state key for a menu. Independent (no shared key):
46
- // unique per (segment, apply) so the menu toggles only itself. Shared key: the
47
- // key all siblings passing the same string agree on, so one open member wins
48
- // (accordion). The shared form ignores the segment name on purpose — that is how
49
- // menus in DIFFERENT segments become mutually exclusive.
50
- export function menuStateKey(
51
- segName: string,
52
- applyName: string,
53
- sharedKey: string | undefined,
54
- ): string {
55
- return sharedKey !== undefined
56
- ? MENU_NS + ident(sharedKey)
57
- : MENU_NS + ident(segName) + "." + ident(applyName);
58
- }
59
-
60
- // The synthesized cycle action a menu's disclosure toggle realizes: writing its
61
- // state key between MENU_CLOSED and its member. Named per (stateKey, member) so
62
- // menus sharing a key contribute distinct cycles on it — the existing same-key
63
- // validator merge unions their members into one gate.
64
- export function menuActionName(stateKey: string, member: string): string {
65
- return stateKey + "." + member;
66
- }
67
-
68
- // [LAW:single-enforcer] THE page-cursor key a menu's picker body paginates by —
69
- // synthesized by the loader (state var + int action, both named by this key)
70
- // and derived again by the renderer, so neither side hand-declares or restates
71
- // it. One cursor PER DISCLOSURE STATE KEY, not per menu: a shared (accordion)
72
- // key holds at most one open member, so its one open body is the only body the
73
- // cursor can belong to — sharing is exact by construction, not an
74
- // approximation — and the disclosure toggle's page-0 reset re-seeds it on
75
- // every open. [LAW:one-source-of-truth] The synthesized state VARIABLE is
76
- // named by this same string (the disclosure-var convention), so the renderer
77
- // reads the live page via this one name.
78
- export function menuPageKey(stateKey: string): string {
79
- return stateKey + ".page";
80
- }
81
-
82
- // [LAW:types-are-the-program] The `{{ menu }}` rare-knob options, spelled as ONE
83
- // trailing `(dict …)` argument. Defaults are the canonical path: paged=true (a
84
- // drop menu wants bounded height; a short domain paginates to one page and shows
85
- // no arrows anyway), closeOnPick=false (stay-open so options can be tried in a
86
- // row), key omitted (an independent menu).
87
- export interface MenuOptions {
88
- readonly closeOnPick: boolean;
89
- readonly paged: boolean;
90
- readonly key: string | undefined;
91
- }
92
-
93
- // [LAW:one-source-of-truth] THE reader of a menu's options dict — the loader
94
- // folds it over `staticDictEntries` (gating identity at load) and the renderer
95
- // folds it over the evaluated dict object (realizing the body), so the option
96
- // vocabulary, value types, and defaults live exactly once. An unknown name or a
97
- // mistyped value throws with text naming the legal shape — the blind authoring
98
- // agent's teaching channel [LAW:no-silent-failure]; the loader attaches segment
99
- // context, the renderer surfaces it via composeWithDiagnostics.
100
- export function parseMenuOptions(
101
- entries: Readonly<Record<string, unknown>>,
102
- ): MenuOptions {
103
- for (const name of Object.keys(entries)) {
104
- if (name !== "closeOnPick" && name !== "paged" && name !== "key") {
105
- throw new Error(
106
- `unknown {{ menu }} option "${name}" — the options dict takes "closeOnPick" (bool, default false), "paged" (bool, default true), "key" (string, accordion grouping)`,
107
- );
108
- }
109
- }
110
- const bool = (name: "closeOnPick" | "paged", def: boolean): boolean => {
111
- const v = entries[name];
112
- if (v === undefined) return def;
113
- if (typeof v !== "boolean") {
114
- throw new Error(
115
- `{{ menu }} option "${name}" must be a boolean, got ${JSON.stringify(v)} (e.g. (dict "${name}" ${String(!def)}))`,
116
- );
117
- }
118
- return v;
119
- };
120
- const key = entries["key"];
121
- if (key !== undefined && typeof key !== "string") {
122
- throw new Error(
123
- `{{ menu }} option "key" must be a string naming the accordion group, got ${JSON.stringify(key)}`,
124
- );
125
- }
126
- // [LAW:types-are-the-program] An empty shared key would collapse the state key
127
- // to the bare reserved `menus.` namespace (and a `menus..member` action name) —
128
- // a key, when present, must name a group.
129
- if (key === "") {
130
- throw new Error(
131
- `{{ menu }} has an empty accordion key — a shared key must be a non-empty name (or omit "key" for an independent menu)`,
132
- );
133
- }
134
- return {
135
- closeOnPick: bool("closeOnPick", false),
136
- paged: bool("paged", true),
137
- key,
138
- };
139
- }
@@ -1,164 +0,0 @@
1
- // [LAW:one-type-per-behavior] An option domain is a NAME → members lookup,
2
- // regardless of where the members come from. Before this module, "themes" /
3
- // "styles" / "looks" were three special-cased branches wearing a closed
4
- // TypeScript union (OptionSource) — a hardcoded list of legal domain NAMES
5
- // with extra steps. There is exactly one domain concept: a `from` value is
6
- // either an INLINE literal domain (an authored array — zero registration,
7
- // zero engine edit) or the NAME of a domain resolved through this registry.
8
- // Adding a new registry-backed domain is one `registerOptionDomain` call
9
- // (data), never a new union member every consumer must re-match on.
10
- //
11
- // [LAW:no-shared-mutable-globals] `_GLOBAL_OPTION_DOMAINS` is shared mutable
12
- // state with exactly one owner (this module) and one explicit API
13
- // (registerOptionDomain / resolveOptionDomain / knownOptionDomainNames). It
14
- // holds only domains whose members are legitimately PROCESS-lifetime static
15
- // (themes, styles — module-init snapshots, same reasoning as
16
- // template-engine/funcs.ts's THEMES_LIST/STYLES_LIST caches). A domain whose
17
- // members vary PER CONFIG (the merged `looks:` block — two daemon render-cache
18
- // entries can hold different looks blocks for different configs
19
- // simultaneously) can never live here; it is threaded explicitly as
20
- // `perConfigDomains`, the same way `lookNames` already is.
21
- //
22
- // [LAW:one-source-of-truth] This is the ONE place a domain name resolves to
23
- // its members. render/action.ts (rendering options) and
24
- // daemon/verbs/state-validators.ts (deriving the click gate) both call
25
- // through here instead of each hand-rolling the themes/styles/looks branch —
26
- // the rendered options and the derived gate cannot diverge because there is
27
- // no second resolver.
28
-
29
- import {
30
- CHARSETS,
31
- COLOR_COMPATIBILITIES,
32
- listResolvablePaletteNames,
33
- STRIP_STYLES,
34
- } from "../themes/policy.js";
35
- import { presetNames } from "./presets.js";
36
-
37
- // [LAW:types-are-the-program] The authoring shape of a `set … from` value: a
38
- // bare string names a domain (resolved through the registry or a per-config
39
- // override); a non-empty array of strings IS the domain, inline, needing no
40
- // name and no registration. Mirrors the `cycle` field's array shape — an
41
- // author already knows this pattern.
42
- export type OptionDomain = string | readonly string[];
43
-
44
- export type OptionDomainResolver = () => readonly string[];
45
-
46
- interface DomainEntry {
47
- readonly permanent: boolean;
48
- readonly resolve: OptionDomainResolver;
49
- }
50
-
51
- const _GLOBAL_OPTION_DOMAINS = new Map<string, DomainEntry>();
52
-
53
- function registerBuiltinDomain(
54
- name: string,
55
- resolve: OptionDomainResolver,
56
- ): void {
57
- _GLOBAL_OPTION_DOMAINS.set(name, { permanent: true, resolve });
58
- }
59
-
60
- // [LAW:one-source-of-truth] "themes"/"styles" become ORDINARY registrations —
61
- // the same registerOptionDomain any future caller uses — reading the same
62
- // canonical lists the set-state validator and the `themes()`/`styles()`
63
- // template bindings already consult (listResolvablePaletteNames/
64
- // STRIP_STYLES). No special-cased branch remains anywhere downstream.
65
- registerBuiltinDomain("themes", () => listResolvablePaletteNames());
66
- registerBuiltinDomain("styles", () => STRIP_STYLES);
67
- // [LAW:one-source-of-truth] Same shape as themes/styles: the exact consts
68
- // the loader's own field validation and the render layer's glyph/color-depth
69
- // dispatch already derive from (themes/policy.ts CHARSETS/COLOR_COMPATIBILITIES)
70
- // — a menu drawing from these can never enumerate a value the render layer
71
- // would reject.
72
- registerBuiltinDomain("charsets", () => CHARSETS);
73
- registerBuiltinDomain("colorCompatibilities", () => COLOR_COMPATIBILITIES);
74
-
75
- // [LAW:no-silent-fallbacks] A built-in domain can never be re-claimed — a
76
- // config or feature registering a custom domain named "themes" gets a loud
77
- // load-time error, never a silent shadow of the real theme list. Registering
78
- // returns a disposer (same shape as registerStateValidator) so a caller with
79
- // a bounded lifetime (a test, a future per-feature domain) can clean up.
80
- export function registerOptionDomain(
81
- name: string,
82
- resolve: OptionDomainResolver,
83
- ): () => void {
84
- const existing = _GLOBAL_OPTION_DOMAINS.get(name);
85
- if (existing) {
86
- throw new Error(
87
- `registerOptionDomain: option domain "${name}" is already registered` +
88
- (existing.permanent
89
- ? " (a built-in domain — built-ins cannot be reclaimed)"
90
- : ""),
91
- );
92
- }
93
- _GLOBAL_OPTION_DOMAINS.set(name, { permanent: false, resolve });
94
- let active = true;
95
- return () => {
96
- if (!active) return;
97
- active = false;
98
- const entry = _GLOBAL_OPTION_DOMAINS.get(name);
99
- if (entry && !entry.permanent) _GLOBAL_OPTION_DOMAINS.delete(name);
100
- };
101
- }
102
-
103
- // [LAW:one-source-of-truth] THE single construction of a config's per-config
104
- // domain overrides — the domains whose members are declared IN the config
105
- // rather than in the registry above: "looks" (the merged `looks:` block) and
106
- // "presets" (the merged `presets:` block). cross-ref.ts (checking a `from` name
107
- // resolves), state-validators.ts (deriving the click gate), and dsl/render.ts
108
- // (compiling render-time options) each need this map; before this function they
109
- // each rebuilt it independently, three sites that could silently drift if a
110
- // future per-config domain were added to only some of them.
111
- //
112
- // [LAW:locality-or-seam] The parameter is the CONFIG, structurally typed to the
113
- // blocks read here — not one positional record per domain. Presets were the
114
- // second per-config domain, and adding them under the old `(looks)` signature
115
- // would have rippled a new argument through all three call sites; under this
116
- // one, a third domain is a single line HERE and nothing else moves. Structural
117
- // (rather than importing DslConfig) so this leaf module still never imports
118
- // dsl-types.ts — that would cycle through dsl-types.ts -> action.ts ->
119
- // option-domain.ts (type-only, but still a cycle this module stays clear of,
120
- // per [LAW:one-way-deps]).
121
- export function perConfigDomainsFor(config: {
122
- readonly looks: Readonly<Record<string, unknown>>;
123
- readonly presets: Readonly<Record<string, unknown>>;
124
- }): ReadonlyMap<string, readonly string[]> {
125
- return new Map([
126
- ["looks", Object.keys(config.looks)],
127
- // [LAW:one-source-of-truth] Not `Object.keys` — the floor is selectable
128
- // whether or not a config declares it, and presetNames is where that is
129
- // stated (once, for the gate and the render alike).
130
- ["presets", presetNames(config.presets)],
131
- ]);
132
- }
133
-
134
- // [LAW:one-source-of-truth] The full set of names `from` may legally name for
135
- // THIS config: every globally-registered domain plus this config's per-config
136
- // overrides (currently just "looks"). Used both to resolve a name and to spell
137
- // out the legal set in an unknown-domain error.
138
- export function knownOptionDomainNames(
139
- perConfigDomains: ReadonlyMap<string, readonly string[]>,
140
- ): readonly string[] {
141
- return [
142
- ...new Set([..._GLOBAL_OPTION_DOMAINS.keys(), ...perConfigDomains.keys()]),
143
- ];
144
- }
145
-
146
- // [LAW:dataflow-not-control-flow] One total resolution: an inline array IS
147
- // its own domain (no lookup); a string is a NAME resolved first against this
148
- // config's per-config overrides, then the global registry. A name matching
149
- // neither is a genuine error — the loader's cross-ref pass already proved
150
- // every `from` name resolves before this runs, so a miss here is a
151
- // caller/wiring bug, not a config-authoring mistake.
152
- export function resolveOptionDomain(
153
- from: OptionDomain,
154
- perConfigDomains: ReadonlyMap<string, readonly string[]>,
155
- ): readonly string[] {
156
- if (typeof from !== "string") return from;
157
- const local = perConfigDomains.get(from);
158
- if (local) return local;
159
- const entry = _GLOBAL_OPTION_DOMAINS.get(from);
160
- if (entry) return entry.resolve();
161
- throw new Error(
162
- `unknown option domain "${from}" (have: ${knownOptionDomainNames(perConfigDomains).join(", ")})`,
163
- );
164
- }