@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.
- package/dist/index.mjs +72 -71
- package/package.json +5 -6
- package/src/check.ts +0 -478
- package/src/cli-flags.ts +0 -8
- package/src/click/wire.ts +0 -158
- package/src/config/action.ts +0 -329
- package/src/config/cli.ts +0 -71
- package/src/config/default-dsl-config.ts +0 -1645
- package/src/config/disclosure.ts +0 -170
- package/src/config/dsl-loader.ts +0 -339
- package/src/config/dsl-types.ts +0 -581
- package/src/config/edit-chrome.ts +0 -559
- package/src/config/help.ts +0 -151
- package/src/config/ident.ts +0 -22
- package/src/config/layout-ops.ts +0 -177
- package/src/config/loader/actions.ts +0 -972
- package/src/config/loader/cache.ts +0 -206
- package/src/config/loader/cross-ref.ts +0 -714
- package/src/config/loader/cycles.ts +0 -148
- package/src/config/loader/diagnostics.ts +0 -99
- package/src/config/loader/discovery.ts +0 -182
- package/src/config/loader/edit-mode.ts +0 -137
- package/src/config/loader/emit-schema.ts +0 -68
- package/src/config/loader/globals.ts +0 -269
- package/src/config/loader/helpers.ts +0 -48
- package/src/config/loader/layout.ts +0 -693
- package/src/config/loader/looks.ts +0 -96
- package/src/config/loader/menu-synth.ts +0 -435
- package/src/config/loader/merge.ts +0 -115
- package/src/config/loader/persist-target.ts +0 -67
- package/src/config/loader/presets.ts +0 -119
- package/src/config/loader/refs.ts +0 -100
- package/src/config/loader/reserved-namespace.ts +0 -38
- package/src/config/loader/segments.ts +0 -120
- package/src/config/loader/validate-core.ts +0 -737
- package/src/config/loader/variables.ts +0 -260
- package/src/config/menu-keys.ts +0 -139
- package/src/config/option-domain.ts +0 -164
- package/src/config/presets.ts +0 -326
- package/src/config/settings-menu.ts +0 -775
- package/src/daemon/acquire.ts +0 -684
- package/src/daemon/cache/git.ts +0 -649
- package/src/daemon/cache/render.ts +0 -623
- package/src/daemon/cache/session-usage-store.ts +0 -720
- package/src/daemon/cache/watchers.ts +0 -249
- package/src/daemon/client-debug.ts +0 -120
- package/src/daemon/client-stats.ts +0 -130
- package/src/daemon/client-transport.ts +0 -273
- package/src/daemon/client.ts +0 -78
- package/src/daemon/config-overrides-store.ts +0 -663
- package/src/daemon/debug-types.ts +0 -91
- package/src/daemon/debug.ts +0 -264
- package/src/daemon/fork-bomb-breaker.ts +0 -351
- package/src/daemon/limits.ts +0 -211
- package/src/daemon/log.ts +0 -81
- package/src/daemon/parent-watchdog.ts +0 -87
- package/src/daemon/paths.ts +0 -211
- package/src/daemon/process-fingerprint.ts +0 -146
- package/src/daemon/protocol.ts +0 -292
- package/src/daemon/render-payload.ts +0 -1256
- package/src/daemon/server.ts +0 -1330
- package/src/daemon/session-state-file.ts +0 -108
- package/src/daemon/session-state.ts +0 -237
- package/src/daemon/socket-lease.ts +0 -209
- package/src/daemon/socket-ownership.ts +0 -209
- package/src/daemon/stats.ts +0 -235
- package/src/daemon/verbs/config-validators.ts +0 -250
- package/src/daemon/verbs/index.ts +0 -706
- package/src/daemon/verbs/state-validators.ts +0 -249
- package/src/daemon/verbs/validator-registry.ts +0 -457
- package/src/demo/dsl.ts +0 -143
- package/src/demo/mock-data.ts +0 -67
- package/src/demo/statusline.json5 +0 -94
- package/src/dsl/node-registry.ts +0 -374
- package/src/dsl/render.ts +0 -803
- package/src/help-text.ts +0 -90
- package/src/index.ts +0 -210
- package/src/install/currency.ts +0 -197
- package/src/install/index.ts +0 -557
- package/src/proc/launch.ts +0 -459
- package/src/proc/stats-handle.ts +0 -13
- package/src/render/action.ts +0 -883
- package/src/render/active-segment.ts +0 -78
- package/src/render/diagnostic-style.ts +0 -23
- package/src/render/diagnostic-text.ts +0 -77
- package/src/render/error-glyph.ts +0 -53
- package/src/render/menu.ts +0 -257
- package/src/render/outcome-plan.ts +0 -45
- package/src/render/picker.ts +0 -372
- package/src/render/segment-color.ts +0 -74
- package/src/render/split-lines.ts +0 -51
- package/src/render/strip.ts +0 -228
- package/src/segments/cache.ts +0 -131
- package/src/segments/context.ts +0 -190
- package/src/segments/git.ts +0 -1084
- package/src/segments/metrics.ts +0 -187
- package/src/segments/pricing.ts +0 -452
- package/src/segments/session.ts +0 -23
- package/src/segments/tmux.ts +0 -74
- package/src/template-engine/cells.ts +0 -90
- package/src/template-engine/colors.ts +0 -124
- package/src/template-engine/engine.ts +0 -108
- package/src/template-engine/funcs.ts +0 -232
- package/src/template-engine/index.ts +0 -11
- package/src/template-engine/layout.ts +0 -133
- package/src/template-engine/scope.ts +0 -62
- package/src/template-engine/sparkline.ts +0 -79
- package/src/themes/index.ts +0 -20
- package/src/themes/palette-resolvers.ts +0 -84
- package/src/themes/policy.ts +0 -393
- package/src/utils/cache.ts +0 -206
- package/src/utils/claude.ts +0 -683
- package/src/utils/color-support.ts +0 -118
- package/src/utils/formatters.ts +0 -99
- package/src/utils/logger.ts +0 -5
- package/src/utils/outcome.ts +0 -33
- package/src/utils/schema-validator.ts +0 -126
- package/src/utils/single-flight.ts +0 -57
- package/src/utils/terminal-width.ts +0 -51
- package/src/utils/terminal.ts +0 -11
- package/src/utils/transcript-fs.ts +0 -279
- package/src/var-system/index.ts +0 -24
- package/src/var-system/sources.ts +0 -1047
- package/src/var-system/store.ts +0 -223
- package/src/var-system/types.ts +0 -57
- 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
|
-
}
|
package/src/config/menu-keys.ts
DELETED
|
@@ -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
|
-
}
|