@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
package/src/config/disclosure.ts
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
// [LAW:one-source-of-truth] THE disclosure primitive: the one toggle machinery
|
|
2
|
-
// that both group sugar (`kind: "group"`, src/config/loader/layout.ts) and the
|
|
3
|
-
// `{{ menu }}` helper (src/config/loader/menu-synth.ts + src/render/menu.ts) are
|
|
4
|
-
// built on. A disclosure is a binary toggle over a SessionState key: the key
|
|
5
|
-
// holds either the CLOSED sentinel or a single MEMBER name; a `cycle` action
|
|
6
|
-
// flips between the two; a ▸/▾ glyph shows which state it is in; sibling
|
|
7
|
-
// disclosures sharing one key become mutually exclusive (an accordion) because
|
|
8
|
-
// one key holds one open member. Group and menu differ ONLY in their BODY (group
|
|
9
|
-
// reveals an arbitrary layout container gated by a `when`; menu drops a picker
|
|
10
|
-
// grid below the row) and in where the trigger lives (group synthesizes a toggle
|
|
11
|
-
// segment; the menu helper IS the trigger). The toggle itself — sentinel, glyphs,
|
|
12
|
-
// the `state` var, the `cycle` action — is single-sourced HERE so the two
|
|
13
|
-
// body-kinds cannot drift [LAW:one-type-per-behavior][LAW:decomposition].
|
|
14
|
-
//
|
|
15
|
-
// [LAW:one-way-deps] This module is intentionally PURE — it imports only decl
|
|
16
|
-
// TYPES (erased at build) and holds no loader (`ValidateCtx`, diagnostics) nor
|
|
17
|
-
// render (rich-js) dependency, so both the loader synthesis passes and the render
|
|
18
|
-
// helper can share it without dragging one layer into the other. The loader-side
|
|
19
|
-
// reserved-namespace collision check — the other half of the shared machinery,
|
|
20
|
-
// which needs the validation context — lives in
|
|
21
|
-
// `src/config/loader/reserved-namespace.ts`.
|
|
22
|
-
|
|
23
|
-
import type { ActionDecl } from "./action.js";
|
|
24
|
-
import type { VariableDecl } from "./dsl-types.js";
|
|
25
|
-
|
|
26
|
-
// The "nothing open" sentinel a disclosure's key starts from and returns to on
|
|
27
|
-
// close. A disclosure's MEMBER (a group name / a menu apply-action name) may
|
|
28
|
-
// never equal this — an equal member would make the cycle `[closed, "closed"]`
|
|
29
|
-
// (two identical members, never openable), which both synthesis passes reject.
|
|
30
|
-
export const DISCLOSURE_CLOSED = "closed";
|
|
31
|
-
|
|
32
|
-
// [LAW:representation] The disclosure glyph vocabulary — one pair for the whole
|
|
33
|
-
// bar so every disclosure reads the same (trailing the label/content it gates,
|
|
34
|
-
// per pdu.8): collapsed ▸, expanded ▾.
|
|
35
|
-
//
|
|
36
|
-
// [LAW:one-source-of-truth] These are the AUTHORED default, never an emission.
|
|
37
|
-
// Every disclosure splices them into the template it synthesizes — group sugar
|
|
38
|
-
// (loader/layout.ts), the settings menu (settings-menu.ts), the bundled drawer
|
|
39
|
-
// — and a hand-authored config writes whichever glyph it likes, because the
|
|
40
|
-
// trigger's text is a display bound at the call site like any other. Until
|
|
41
|
-
// candybar-settings-ui-aok.4 `{{ menu }}` was the exception, appending ▸/▾ from
|
|
42
|
-
// its own runtime where no author could see or decline it, which is how edit
|
|
43
|
-
// mode's `+` came to render `+▸`.
|
|
44
|
-
export const DISCLOSURE_GLYPH_CLOSED = "▸";
|
|
45
|
-
export const DISCLOSURE_GLYPH_OPEN = "▾";
|
|
46
|
-
|
|
47
|
-
// [LAW:one-source-of-truth] The glyph that CLOSES an open disclosure. The
|
|
48
|
-
// picker body's ✕ has always been this; it lives here now because a trigger can
|
|
49
|
-
// wear it too — edit mode's `+` does, since a `+` whose only open-state cue was
|
|
50
|
-
// the ▸ this change removed would otherwise be indistinguishable from its
|
|
51
|
-
// siblings (three insertion points render byte-identically when one is open,
|
|
52
|
-
// and their dropped bodies are identical too, so row 0 is the only place the
|
|
53
|
-
// answer can live). Two affordances, one meaning, one glyph.
|
|
54
|
-
export const DISCLOSURE_GLYPH_CLOSE = "✕";
|
|
55
|
-
|
|
56
|
-
// [LAW:single-enforcer] THE display rule every multi-state trigger obeys: bind
|
|
57
|
-
// one display per member, or ONE static display that shows in every state. It
|
|
58
|
-
// lives here, beside the toggle machinery, because both disclosure kinds need
|
|
59
|
-
// it at different times — the loader can count a call's arguments statically
|
|
60
|
-
// and wants an ISSUE to report, the renderer holds the evaluated displays and
|
|
61
|
-
// wants to THROW — and a rule spelled once in each place is a rule that drifts.
|
|
62
|
-
// A `{{ menu }}` folds through it with two members (its `[closed, member]`
|
|
63
|
-
// cycle) and a cycle `{{ action }}` with as many as it declares; nothing about
|
|
64
|
-
// the rule is disclosure-specific beyond who calls it.
|
|
65
|
-
export function cycleDisplayIssue(
|
|
66
|
-
subject: string,
|
|
67
|
-
count: number,
|
|
68
|
-
members: number,
|
|
69
|
-
): string | undefined {
|
|
70
|
-
if (count === 0) return `${subject} needs a display (the clickable text)`;
|
|
71
|
-
if (count !== 1 && count !== members) {
|
|
72
|
-
return `${subject} cycles ${members} members; bind one display per member (${members}) or one static display, got ${count}`;
|
|
73
|
-
}
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// [LAW:dataflow-not-control-flow] Which display shows is a pure function of
|
|
78
|
-
// (bound displays, current member index): a single static display shows in
|
|
79
|
-
// every state, per-member displays index by the state. Throws the one rule's
|
|
80
|
-
// text rather than silently dropping or repeating an argument.
|
|
81
|
-
export function pickCycleDisplay(
|
|
82
|
-
subject: string,
|
|
83
|
-
displays: readonly string[],
|
|
84
|
-
members: number,
|
|
85
|
-
index: number,
|
|
86
|
-
): string {
|
|
87
|
-
const issue = cycleDisplayIssue(subject, displays.length, members);
|
|
88
|
-
if (issue !== undefined) throw new Error(issue);
|
|
89
|
-
return displays.length === 1 ? displays[0]! : displays[index]!;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// [LAW:one-source-of-truth] Go-template string-literal escaping for any DISPLAY
|
|
93
|
-
// text a synthesis splices INSIDE a quoted `{{ }}` argument of a template it
|
|
94
|
-
// emits — a group's label, a preset name in the reset banner, a `(?)` trigger's
|
|
95
|
-
// closed/open glyphs. NOT for a template's own body text, which is source rather
|
|
96
|
-
// than a splice: a help line is assigned verbatim (help.ts) because escaping one
|
|
97
|
-
// would put a backslash on the bar. It lives here, beside the two splices
|
|
98
|
-
// that need it most, because it was already two verbatim copies (loader/layout.ts
|
|
99
|
-
// and edit-chrome.ts, whose comment deferred the merge until "one small rule"
|
|
100
|
-
// earned its own home). The `(?)` affordance was the third caller, so it did.
|
|
101
|
-
export function escapeTemplateLiteral(s: string): string {
|
|
102
|
-
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// [LAW:types-are-the-program] One open disclosure, named by the two strings that
|
|
106
|
-
// decide it: the VARIABLE a body reads and the MEMBER value that means "this one
|
|
107
|
-
// is open". They are distinct because a group's variable is per-group
|
|
108
|
-
// (`groups.<name>`) while its state KEY may be shared with accordion siblings —
|
|
109
|
-
// so the pair, never a lone key, is what identifies an open state.
|
|
110
|
-
export interface DisclosureRef {
|
|
111
|
-
readonly variable: string;
|
|
112
|
-
readonly member: string;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// [LAW:single-enforcer] THE body predicate a disclosure implies. Variadic
|
|
116
|
-
// because nesting is conjunction and nothing else: a row inside two disclosures
|
|
117
|
-
// is open when both are, which is one list, not a compound spelling. Callers
|
|
118
|
-
// that used to hand-write `{{ eq .x "open" }}` beside `{{ and (eq .x "open")
|
|
119
|
-
// (eq .y "open") }}` now pass one ref or two to one function — the shape stops
|
|
120
|
-
// varying with the depth [LAW:dataflow-not-control-flow].
|
|
121
|
-
//
|
|
122
|
-
// `and` is variadic in Go templates and returns its sole argument when given
|
|
123
|
-
// one, so the single-disclosure case needs no separate spelling.
|
|
124
|
-
//
|
|
125
|
-
// [LAW:types-are-the-program] The first ref is a separate parameter so a gate
|
|
126
|
-
// over ZERO disclosures — which would emit an argument-less `{{ and }}` and gate
|
|
127
|
-
// on nothing — is unrepresentable, with no runtime guard to state it.
|
|
128
|
-
export function disclosureGate(
|
|
129
|
-
first: DisclosureRef,
|
|
130
|
-
...rest: readonly DisclosureRef[]
|
|
131
|
-
): string {
|
|
132
|
-
const terms = [first, ...rest]
|
|
133
|
-
.map((o) => `(eq .${o.variable} "${escapeTemplateLiteral(o.member)}")`)
|
|
134
|
-
.join(" ");
|
|
135
|
-
return `{{ and ${terms} }}`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// [LAW:single-enforcer] THE trigger template a disclosure's toggle segment
|
|
139
|
-
// carries: one `{{ action }}` over the cycle action, binding the author's text
|
|
140
|
-
// per state — closed first, matching the cycle's own closed-first member order
|
|
141
|
-
// so the display index and the member index are the same number. Since .4 every
|
|
142
|
-
// trigger authors its own text (the runtime appends no glyph), which makes this
|
|
143
|
-
// the one place the binding is spelled; before it, five sites spelled it and the
|
|
144
|
-
// `+▸` double-glyph bug lived in the gap between two of them.
|
|
145
|
-
export function disclosureTrigger(
|
|
146
|
-
action: string,
|
|
147
|
-
closed: string,
|
|
148
|
-
open: string,
|
|
149
|
-
): string {
|
|
150
|
-
return `{{ action "${action}" "${escapeTemplateLiteral(closed)}" "${escapeTemplateLiteral(open)}" }}`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// [LAW:single-enforcer] THE backing `state` variable a disclosure key implies:
|
|
154
|
-
// it holds the open member's name and defaults to `def` (the CLOSED sentinel for
|
|
155
|
-
// an independent disclosure, or an initially-open member for a group's
|
|
156
|
-
// `open: true`). One shape, so a group var and a menu var declared on one key
|
|
157
|
-
// cannot disagree on kind or key.
|
|
158
|
-
export function disclosureStateVar(key: string, def: string): VariableDecl {
|
|
159
|
-
return { kind: "state", key, default: def };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// [LAW:single-enforcer] THE toggle action a disclosure realizes: a binary `cycle`
|
|
163
|
-
// between the CLOSED sentinel and the member (ordered closed-first, so an unset or
|
|
164
|
-
// sibling-held key counts as the first member — the toggle renders ▸ and a click
|
|
165
|
-
// writes the member, opening it and auto-closing any accordion sibling). The
|
|
166
|
-
// derived click gate (`deriveActionValidators`) reads this like every other
|
|
167
|
-
// `set`, so a disclosure toggle needs no parallel verb [LAW:single-enforcer].
|
|
168
|
-
export function disclosureCycleAction(key: string, member: string): ActionDecl {
|
|
169
|
-
return { set: key, cycle: [DISCLOSURE_CLOSED, member] };
|
|
170
|
-
}
|
package/src/config/dsl-loader.ts
DELETED
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
// [LAW:one-type-per-behavior] Three primitives, three concerns:
|
|
2
|
-
//
|
|
3
|
-
// parseDslConfig (text → RawDslConfig)
|
|
4
|
-
// JSON5 syntax + per-record structural validation. Rejects the removed
|
|
5
|
-
// `layout:` key and `kind:"cells"` node with migration-pointing errors.
|
|
6
|
-
// Throws ConfigError on syntax / structural problems.
|
|
7
|
-
//
|
|
8
|
-
// mergeWithDefault (RawDslConfig + DslConfig → DslConfig)
|
|
9
|
-
// Cascade: shallow merge globals fields, by-name merge variables and
|
|
10
|
-
// segments, wholesale root replacement when present. Pure function.
|
|
11
|
-
//
|
|
12
|
-
// validateConfig (DslConfig → ValidatedConfig)
|
|
13
|
-
// Cross-references + cycle detection on the merged shape. Sole producer
|
|
14
|
-
// of ValidatedConfig. Throws ConfigError on cross-ref / cycle problems.
|
|
15
|
-
//
|
|
16
|
-
// loadConfig (path|null → DslConfig) wires parse+merge for the daemon's
|
|
17
|
-
// production path. validateConfig finishes the chain.
|
|
18
|
-
//
|
|
19
|
-
// [LAW:dataflow-not-control-flow] Validation passes accumulate issues into
|
|
20
|
-
// a list; consumers see every problem at once (compiler-style).
|
|
21
|
-
//
|
|
22
|
-
// This file is the pipeline orchestrator + the public barrel. Each validation
|
|
23
|
-
// concern lives in its own `loader/` module (split by change-reason); the
|
|
24
|
-
// re-exports below keep the import surface stable for every consumer.
|
|
25
|
-
|
|
26
|
-
import fs from "node:fs";
|
|
27
|
-
import JSON5 from "json5";
|
|
28
|
-
import {
|
|
29
|
-
type DslConfig,
|
|
30
|
-
type RawDslConfig,
|
|
31
|
-
type ValidatedConfig,
|
|
32
|
-
} from "./dsl-types.js";
|
|
33
|
-
import { listResolvablePaletteNames } from "../themes/policy.js";
|
|
34
|
-
import {
|
|
35
|
-
ConfigError,
|
|
36
|
-
findKeyLine,
|
|
37
|
-
type ConfigIssue,
|
|
38
|
-
} from "./loader/diagnostics.js";
|
|
39
|
-
import {
|
|
40
|
-
describeType,
|
|
41
|
-
isPlainObject,
|
|
42
|
-
type Mutable,
|
|
43
|
-
type ValidateCtx,
|
|
44
|
-
} from "./loader/validate-core.js";
|
|
45
|
-
import { mergeWithDefault } from "./loader/merge.js";
|
|
46
|
-
import { validateEditGlobals, validateGlobals } from "./loader/globals.js";
|
|
47
|
-
import { validateVariables } from "./loader/variables.js";
|
|
48
|
-
import { validateSegments } from "./loader/segments.js";
|
|
49
|
-
import { synthesizeGroupDecls, validateRoot } from "./loader/layout.js";
|
|
50
|
-
import { synthesizeMenuDecls } from "./loader/menu-synth.js";
|
|
51
|
-
import { synthesizeEditModeToggle } from "./loader/edit-mode.js";
|
|
52
|
-
import { synthesizeEditChrome } from "./edit-chrome.js";
|
|
53
|
-
import { SETTINGS_NS, synthesizeSettingsMenu } from "./settings-menu.js";
|
|
54
|
-
import { reservedNamespaceCollisions } from "./loader/reserved-namespace.js";
|
|
55
|
-
import { validateActions } from "./loader/actions.js";
|
|
56
|
-
import { validateLooks } from "./loader/looks.js";
|
|
57
|
-
import { validatePresets } from "./loader/presets.js";
|
|
58
|
-
import { validateHelpers } from "./loader/helpers.js";
|
|
59
|
-
import { validateCrossReferences } from "./loader/cross-ref.js";
|
|
60
|
-
import { validateNoCycles } from "./loader/cycles.js";
|
|
61
|
-
|
|
62
|
-
// ─── Public barrel ───────────────────────────────────────────────────────────
|
|
63
|
-
// [LAW:locality-or-seam] Consumers import from `dsl-loader`; the internal split
|
|
64
|
-
// is invisible to them. Moving a symbol between loader/ modules never touches a
|
|
65
|
-
// callsite as long as it stays re-exported here.
|
|
66
|
-
|
|
67
|
-
export { ConfigError, findKeyLine } from "./loader/diagnostics.js";
|
|
68
|
-
export type { ConfigIssue } from "./loader/diagnostics.js";
|
|
69
|
-
export {
|
|
70
|
-
expandHome,
|
|
71
|
-
dslConfigCandidatePaths,
|
|
72
|
-
resolveDslConfigPath,
|
|
73
|
-
detectConfigCollisions,
|
|
74
|
-
} from "./loader/discovery.js";
|
|
75
|
-
export {
|
|
76
|
-
mergeWithDefault,
|
|
77
|
-
applySegmentPaletteOverrides,
|
|
78
|
-
} from "./loader/merge.js";
|
|
79
|
-
export {
|
|
80
|
-
extractTemplateRefs,
|
|
81
|
-
extractActionRefs,
|
|
82
|
-
extractPickerMenuRefs,
|
|
83
|
-
} from "./loader/refs.js";
|
|
84
|
-
|
|
85
|
-
// ─── Three-stage pipeline ────────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Load a JSON5 DSL config file from disk and merge it with the given
|
|
89
|
-
* default. Returns the effective DslConfig AND the raw source text.
|
|
90
|
-
*
|
|
91
|
-
* `path = null` means "no user file exists" — returns `dflt` unchanged
|
|
92
|
-
* (uniform merge against an empty raw, which is deep-equal to `dflt`) and
|
|
93
|
-
* an empty source. No consumer branches on file presence; that branch lives
|
|
94
|
-
* inside loadConfig exactly once.
|
|
95
|
-
*
|
|
96
|
-
* [LAW:one-way-deps] `dflt` is a required parameter, not a default pointing at
|
|
97
|
-
* DEFAULT_DSL_CONFIG: this module is generic merge/parse machinery, and
|
|
98
|
-
* DEFAULT_DSL_CONFIG is a specific, higher-level instance built ON TOP of it
|
|
99
|
-
* (default-dsl-config.ts imports parseDslConfig/mergeWithDefault to
|
|
100
|
-
* synthesize itself — see that file). A default param here pointing back at
|
|
101
|
-
* DEFAULT_DSL_CONFIG would make this generic module depend on its own
|
|
102
|
-
* specific consumer — a cycle every caller who wants "the bundled default"
|
|
103
|
-
* resolves explicitly by importing DEFAULT_DSL_CONFIG themselves.
|
|
104
|
-
*
|
|
105
|
-
* [LAW:one-source-of-truth] The source is returned alongside the config so the
|
|
106
|
-
* caller can hand it to validateConfig — cross-ref diagnostics (line numbers,
|
|
107
|
-
* the authored-surface discriminator) are derived from it, and the file is read
|
|
108
|
-
* exactly once here rather than re-read downstream.
|
|
109
|
-
*
|
|
110
|
-
* Throws ConfigError on JSON5 syntax / structural / per-record validation
|
|
111
|
-
* failures. Cross-references and cycles are validateConfig()'s job.
|
|
112
|
-
*
|
|
113
|
-
* [LAW:dataflow-not-control-flow] One function, one branch, same operations
|
|
114
|
-
* each call.
|
|
115
|
-
*/
|
|
116
|
-
export function loadConfig(
|
|
117
|
-
path: string | null,
|
|
118
|
-
dflt: DslConfig,
|
|
119
|
-
allowedPalettes?: ReadonlySet<string>,
|
|
120
|
-
): { config: DslConfig; source: string } {
|
|
121
|
-
const source = path === null ? "" : fs.readFileSync(path, "utf-8");
|
|
122
|
-
const raw: RawDslConfig =
|
|
123
|
-
path === null ? {} : parseDslConfig(path, source, allowedPalettes);
|
|
124
|
-
return { config: mergeWithDefault(raw, dflt), source };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Promote a merged DslConfig to a ValidatedConfig by running cross-references
|
|
129
|
-
* and cycle detection. Sole producer of ValidatedConfig in the codebase — the
|
|
130
|
-
* phantom brand makes "the renderer never receives an unvalidated config" a
|
|
131
|
-
* compile-time invariant, not a runtime convention.
|
|
132
|
-
*
|
|
133
|
-
* Throws ConfigError aggregating every issue.
|
|
134
|
-
*
|
|
135
|
-
* [LAW:single-enforcer] One cast site, here, exclusive.
|
|
136
|
-
*/
|
|
137
|
-
export function validateConfig(
|
|
138
|
-
config: DslConfig,
|
|
139
|
-
filePath = "<config>",
|
|
140
|
-
source = "",
|
|
141
|
-
allowedPalettes: ReadonlySet<string> = new Set(listResolvablePaletteNames()),
|
|
142
|
-
): ValidatedConfig {
|
|
143
|
-
const issues: ConfigIssue[] = [];
|
|
144
|
-
const ctx: ValidateCtx = { source, issues, allowedPalettes, groups: [] };
|
|
145
|
-
validateCrossReferences(ctx, config);
|
|
146
|
-
validateNoCycles(ctx, config);
|
|
147
|
-
if (issues.length > 0) {
|
|
148
|
-
throw new ConfigError(filePath, issues);
|
|
149
|
-
}
|
|
150
|
-
// [LAW:one-source-of-truth] Edit-mode's CHROME half (brandon-layout-edit-
|
|
151
|
-
// 2gc.3), synthesized HERE — not in parseDslConfig alongside the toggle —
|
|
152
|
-
// because it needs the fully merged, preset-resolved, rootOps-replayed
|
|
153
|
-
// tree cross-ref/cycles just proved sound. Its own output (segment refs
|
|
154
|
-
// into freshly-synthesized segments, actions into freshly-synthesized
|
|
155
|
-
// actions) is correct by construction and does not re-enter cross-ref/
|
|
156
|
-
// cycle checking, exactly as group/menu synthesis's output doesn't either.
|
|
157
|
-
// [LAW:dataflow-not-control-flow] candybar-settings-ui-aok.1's global settings
|
|
158
|
-
// menu, spliced BEFORE edit chrome so edit chrome walks the final content tree
|
|
159
|
-
// and treats the menu's reserved `settings.` names as chrome-exempt — the
|
|
160
|
-
// full ordering argument lives in settings-menu.ts's header, beside the pass
|
|
161
|
-
// it governs.
|
|
162
|
-
const withChrome = synthesizeEditChrome(synthesizeSettingsMenu(config));
|
|
163
|
-
return withChrome as ValidatedConfig;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Parse a JSON5 DSL config source into a RawDslConfig. JSON5 syntax + per-
|
|
168
|
-
* record structural validation. Cross-references and cycles are NOT checked
|
|
169
|
-
* here — they belong to validateConfig, which runs on the merged shape.
|
|
170
|
-
*
|
|
171
|
-
* Returned shape preserves absence: top-level keys are optional in RawDslConfig.
|
|
172
|
-
*
|
|
173
|
-
* `allowedPalettes` is the set of palette names a `palette:` field may name.
|
|
174
|
-
* It defaults to every name that resolves to a concrete Palette, so production
|
|
175
|
-
* always validates loudly against the real registry. Tests inject a custom set
|
|
176
|
-
* to exercise validation without depending on registry contents.
|
|
177
|
-
*/
|
|
178
|
-
export function parseDslConfig(
|
|
179
|
-
filePath: string,
|
|
180
|
-
source: string,
|
|
181
|
-
allowedPalettes: ReadonlySet<string> = new Set(listResolvablePaletteNames()),
|
|
182
|
-
): RawDslConfig {
|
|
183
|
-
// ── Stage 1: JSON5 syntax. A parse error here is single, immediate, and
|
|
184
|
-
// carries line/col from the json5 package — no point continuing to other
|
|
185
|
-
// passes that need a parsed structure to inspect.
|
|
186
|
-
const raw = parseJson5OrThrow(filePath, source);
|
|
187
|
-
|
|
188
|
-
const issues: ConfigIssue[] = [];
|
|
189
|
-
const ctx: ValidateCtx = { source, issues, allowedPalettes, groups: [] };
|
|
190
|
-
|
|
191
|
-
// ── Stage 2: top-level shape + per-record shape. Absence survives as
|
|
192
|
-
// `undefined` in the returned RawDslConfig.
|
|
193
|
-
if (!isPlainObject(raw)) {
|
|
194
|
-
throw new ConfigError(filePath, [
|
|
195
|
-
{
|
|
196
|
-
path: "",
|
|
197
|
-
message: `Config root must be an object, got ${describeType(raw)}`,
|
|
198
|
-
},
|
|
199
|
-
]);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const topLevel = validateTopLevel(ctx, raw);
|
|
203
|
-
|
|
204
|
-
if (issues.length > 0) {
|
|
205
|
-
throw new ConfigError(filePath, issues);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return topLevel;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// ─── Internals ───────────────────────────────────────────────────────────────
|
|
212
|
-
|
|
213
|
-
interface Json5Error extends Error {
|
|
214
|
-
lineNumber?: number;
|
|
215
|
-
columnNumber?: number;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function parseJson5OrThrow(filePath: string, source: string): unknown {
|
|
219
|
-
try {
|
|
220
|
-
return JSON5.parse(source);
|
|
221
|
-
} catch (err) {
|
|
222
|
-
const e = err as Json5Error;
|
|
223
|
-
throw new ConfigError(filePath, [
|
|
224
|
-
{
|
|
225
|
-
path: "",
|
|
226
|
-
message: `JSON5 syntax error: ${e.message}`,
|
|
227
|
-
line: e.lineNumber,
|
|
228
|
-
col: e.columnNumber,
|
|
229
|
-
},
|
|
230
|
-
]);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// [LAW:types-are-the-program] Returns RawDslConfig — absence of a top-level
|
|
235
|
-
// key survives the parse as `undefined`, distinct from explicit empty. The
|
|
236
|
-
// merge step downstream decides what "absent" means policy-wise (currently:
|
|
237
|
-
// inherit from default).
|
|
238
|
-
function validateTopLevel(
|
|
239
|
-
ctx: ValidateCtx,
|
|
240
|
-
raw: Record<string, unknown>,
|
|
241
|
-
): RawDslConfig {
|
|
242
|
-
for (const key of Object.keys(raw)) {
|
|
243
|
-
if (!TOP_LEVEL_KEYS.has(key)) {
|
|
244
|
-
ctx.issues.push({
|
|
245
|
-
path: key,
|
|
246
|
-
message: `Unknown top-level key "${key}". Expected one of: ${[...TOP_LEVEL_KEYS].join(", ")}`,
|
|
247
|
-
line: findKeyLine(ctx.source, [key]),
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const out: Mutable<RawDslConfig> = {};
|
|
253
|
-
if (raw.globals !== undefined)
|
|
254
|
-
out.globals = validateGlobals(ctx, "globals", raw.globals);
|
|
255
|
-
if (raw.variables !== undefined)
|
|
256
|
-
out.variables = validateVariables(ctx, "variables", raw.variables);
|
|
257
|
-
if (raw.segments !== undefined)
|
|
258
|
-
out.segments = validateSegments(ctx, raw.segments);
|
|
259
|
-
// [LAW:no-silent-failure] `layout:` was removed in 2de.19. Reject loudly with
|
|
260
|
-
// a migration hint so the author knows exactly how to rewrite their config.
|
|
261
|
-
if (raw.layout !== undefined) {
|
|
262
|
-
ctx.issues.push({
|
|
263
|
-
path: "layout",
|
|
264
|
-
message:
|
|
265
|
-
`"layout" is no longer supported — use "root" with the A-grammar instead.\n` +
|
|
266
|
-
` Replace: layout: [["seg1", "seg2"], ["seg3"]]\n` +
|
|
267
|
-
` With: root: { v: [{ h: ["seg1", "seg2"] }, "seg3"] }\n` +
|
|
268
|
-
` Single-row example: root: { h: ["seg1", "seg2"] }`,
|
|
269
|
-
line: findKeyLine(ctx.source, ["layout"]),
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
if (raw.root !== undefined) out.root = validateRoot(ctx, "root", raw.root);
|
|
273
|
-
if (raw.actions !== undefined)
|
|
274
|
-
out.actions = validateActions(ctx, raw.actions);
|
|
275
|
-
if (raw.looks !== undefined) out.looks = validateLooks(ctx, raw.looks);
|
|
276
|
-
// [LAW:one-source-of-truth] Parsed BEFORE the synthesis passes below, because
|
|
277
|
-
// a preset's `root` runs through the same validateRoot and therefore collects
|
|
278
|
-
// its group sugar into the same `ctx.groups` the top-level root does — the
|
|
279
|
-
// synthesized artifacts must see every group the config declares, wherever it
|
|
280
|
-
// was staged from.
|
|
281
|
-
if (raw.presets !== undefined)
|
|
282
|
-
out.presets = validatePresets(ctx, raw.presets);
|
|
283
|
-
// [LAW:one-type-per-behavior] Edit mode's staged display globals — the same
|
|
284
|
-
// fragment shape a preset carries, one rung later in the precedence chain, so
|
|
285
|
-
// it runs through the same field table (validateEditGlobals) rather than a
|
|
286
|
-
// parallel schema listing which globals edit mode may set.
|
|
287
|
-
if (raw.editGlobals !== undefined)
|
|
288
|
-
out.editGlobals = validateEditGlobals(ctx, "editGlobals", raw.editGlobals);
|
|
289
|
-
if (raw.helpers !== undefined)
|
|
290
|
-
out.helpers = validateHelpers(ctx, raw.helpers);
|
|
291
|
-
// [LAW:one-source-of-truth] Group sugar synthesis runs AFTER every section
|
|
292
|
-
// parsed: each group collected during the root walk emits its state var +
|
|
293
|
-
// cycle action + toggle segment into the raw sections (so they merge over the
|
|
294
|
-
// default and cross-ref like any user declaration), and user names under the
|
|
295
|
-
// reserved namespace are rejected against the fully-parsed sections.
|
|
296
|
-
synthesizeGroupDecls(ctx, out);
|
|
297
|
-
// [LAW:one-source-of-truth] Menu synthesis runs AFTER group synthesis (a group
|
|
298
|
-
// body may host menu-bearing segments) and after every section parsed: each
|
|
299
|
-
// menu placement detected in the root walk emits its state var + cycle action
|
|
300
|
-
// into the raw sections, so they merge over the default, derive the click gate
|
|
301
|
-
// through deriveActionValidators, and collide loudly with any user name under
|
|
302
|
-
// the reserved namespace.
|
|
303
|
-
synthesizeMenuDecls(ctx, out);
|
|
304
|
-
// [LAW:one-source-of-truth] Edit-mode's TOGGLE half (brandon-layout-edit-
|
|
305
|
-
// 2gc.3) — unconditional, like the reservation above, so `edit.mode`/
|
|
306
|
-
// `edit.toggle` exist in EVERY parsed file and a hand-authored trigger
|
|
307
|
-
// segment cross-ref-checks normally. The CHROME half (the per-position +/-
|
|
308
|
-
// affordances) runs later, in validateConfig, once the merged/preset-
|
|
309
|
-
// resolved/rootOps-replayed tree exists to derive it from.
|
|
310
|
-
synthesizeEditModeToggle(ctx, out);
|
|
311
|
-
// [LAW:one-source-of-truth] The global settings menu reserves its namespace
|
|
312
|
-
// here and synthesizes NOTHING here: the tree it must be present in only
|
|
313
|
-
// exists after merge (a user `root` replaces the default's), so the artifacts
|
|
314
|
-
// are minted in validateConfig. The reservation is unconditional all the same,
|
|
315
|
-
// mirroring every other namespace above — "you never author settings.*" is a
|
|
316
|
-
// stable contract, not a rule that switches on when the pass happens to fire.
|
|
317
|
-
reservedNamespaceCollisions(
|
|
318
|
-
ctx,
|
|
319
|
-
out,
|
|
320
|
-
SETTINGS_NS,
|
|
321
|
-
"the global settings menu",
|
|
322
|
-
);
|
|
323
|
-
return out;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// [LAW:no-silent-failure] `layout` is intentionally absent — a config that
|
|
327
|
-
// writes it gets an explicit migration error, not an "unknown key" message.
|
|
328
|
-
const TOP_LEVEL_KEYS = new Set([
|
|
329
|
-
"globals",
|
|
330
|
-
"variables",
|
|
331
|
-
"segments",
|
|
332
|
-
"layout",
|
|
333
|
-
"root",
|
|
334
|
-
"actions",
|
|
335
|
-
"looks",
|
|
336
|
-
"presets",
|
|
337
|
-
"editGlobals",
|
|
338
|
-
"helpers",
|
|
339
|
-
]);
|