@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/help.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
// [LAW:one-type-per-behavior] candybar-settings-ui-aok.6 — the `(?)` affordance.
|
|
2
|
-
// It is NOT a fourth kind of disclosure. A `(?)` is the SAME binary toggle
|
|
3
|
-
// `kind: "group"`, `{{ menu }}`, edit mode's trigger and the global settings menu
|
|
4
|
-
// all are; it differs from a group in exactly two VALUES — the text its trigger
|
|
5
|
-
// binds (`(?)`/`✕` rather than `label ▸`/`label ▾`) and what its body contains
|
|
6
|
-
// (help lines rather than arbitrary layout). So every artifact below comes out
|
|
7
|
-
// of `disclosure.ts`, and this module contributes no state, no gate, and no
|
|
8
|
-
// glyph rule of its own — only the two values and the shape they fill.
|
|
9
|
-
//
|
|
10
|
-
// The ticket that asked for this predicted the alternative: "if it arrives as
|
|
11
|
-
// another hand-rolled set of four artifacts, the finding to report is that the
|
|
12
|
-
// disclosure itself wants to be one thing". It did. `disclosure.ts` had
|
|
13
|
-
// single-sourced only the two artifacts that are DATA (the state var, the cycle
|
|
14
|
-
// action) and left the two that are STRINGS — the trigger template and the body
|
|
15
|
-
// gate — hand-written at five sites, which is where the `+▸` double-glyph bug of
|
|
16
|
-
// .4 lived. `disclosureTrigger`/`disclosureGate` finish that extraction, and
|
|
17
|
-
// this file is the first caller that never had a copy to begin with.
|
|
18
|
-
//
|
|
19
|
-
// [LAW:one-way-deps] Content is a PARAMETER, never an import: this module knows
|
|
20
|
-
// how to mint a help disclosure and nothing about what any particular one says.
|
|
21
|
-
// The bundled sentences live in `src/help-text.ts` — the corpus `--help` prints
|
|
22
|
-
// — and the two synthesis passes that place a `(?)` pass them in. That is what
|
|
23
|
-
// keeps the bar's help and the CLI's help one set of strings rather than two
|
|
24
|
-
// hand-maintained copies [LAW:one-source-of-truth].
|
|
25
|
-
|
|
26
|
-
import type { ActionDecl } from "./action.js";
|
|
27
|
-
import type { LayoutNode, SegmentDecl, VariableDecl } from "./dsl-types.js";
|
|
28
|
-
import {
|
|
29
|
-
DISCLOSURE_CLOSED,
|
|
30
|
-
DISCLOSURE_GLYPH_CLOSE,
|
|
31
|
-
disclosureCycleAction,
|
|
32
|
-
disclosureGate,
|
|
33
|
-
disclosureStateVar,
|
|
34
|
-
disclosureTrigger,
|
|
35
|
-
type DisclosureRef,
|
|
36
|
-
} from "./disclosure.js";
|
|
37
|
-
|
|
38
|
-
// [LAW:one-source-of-truth] The trigger's closed text, one spelling for the
|
|
39
|
-
// whole bar so a reader learns the affordance once. Its OPEN text is the shared
|
|
40
|
-
// `DISCLOSURE_GLYPH_CLOSE` — the same ✕ a picker's close cell and edit mode's
|
|
41
|
-
// opened `+` wear, because it is the same meaning: click to close what this
|
|
42
|
-
// opened.
|
|
43
|
-
//
|
|
44
|
-
// [LAW:no-silent-failure] Deliberately per-state rather than one static `(?)`.
|
|
45
|
-
// Several `(?)` triggers can be visible at once (edit mode's and the config
|
|
46
|
-
// menu's, whenever both surfaces are open), and the tint that distinguishes
|
|
47
|
-
// other open disclosures — node-registry's `drops.length > 0` — recolours a
|
|
48
|
-
// segment's RESOLVED BACKGROUND, so it is a no-op on any segment declaring no
|
|
49
|
-
// bg. .5 gave edit mode a look at the GLOBALS level only, which left its insert
|
|
50
|
-
// chrome without one; the trigger's own text is therefore the only place the
|
|
51
|
-
// open state can be read, exactly as it is for the `+` beside it.
|
|
52
|
-
export const HELP_GLYPH_CLOSED = "(?)";
|
|
53
|
-
|
|
54
|
-
// The open member of a help disclosure's binary key: it holds this or the shared
|
|
55
|
-
// CLOSED sentinel, like every other binary disclosure in the bar.
|
|
56
|
-
const HELP_OPEN = "open";
|
|
57
|
-
|
|
58
|
-
// [LAW:one-source-of-truth] The declarations a `(?)` contributes, keyed by final
|
|
59
|
-
// name — the same accumulator shape `ChromeArtifacts`/`MenuArtifacts` already
|
|
60
|
-
// thread through their synthesis passes, so a caller merges one more set of
|
|
61
|
-
// artifacts the way it already merges its own.
|
|
62
|
-
export interface HelpArtifacts {
|
|
63
|
-
readonly variables: Record<string, VariableDecl>;
|
|
64
|
-
readonly actions: Record<string, ActionDecl>;
|
|
65
|
-
readonly segments: Record<string, SegmentDecl>;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// [LAW:types-are-the-program] The two nodes a caller must place, returned
|
|
69
|
-
// separately because they belong in different rows and only the caller knows
|
|
70
|
-
// which: the trigger is a CELL that joins a row the caller already has (so
|
|
71
|
-
// opening help never widens the bar and closed help costs no row), and the body
|
|
72
|
-
// is a ROW of its own that exists only while the disclosure is open.
|
|
73
|
-
export interface HelpDisclosure {
|
|
74
|
-
readonly trigger: LayoutNode;
|
|
75
|
-
readonly body: LayoutNode;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Mint one `(?)` and its body.
|
|
79
|
-
//
|
|
80
|
-
// `name` is the reserved-namespace base every artifact derives from — the
|
|
81
|
-
// trigger segment, the state variable and the cycle action all take it
|
|
82
|
-
// verbatim, the same one-name-four-artifacts convention `groups.<name>` and
|
|
83
|
-
// `settings.menu` already use, so the toggle's click and the body's gate cannot
|
|
84
|
-
// address different keys [LAW:one-source-of-truth].
|
|
85
|
-
//
|
|
86
|
-
// `within` names the disclosures this help sits INSIDE (edit mode, the settings
|
|
87
|
-
// menu). Both nodes inherit those gates, so a `(?)` opened inside a surface that
|
|
88
|
-
// is then closed cannot leave its body stranded on the bar — the body's gate is
|
|
89
|
-
// its own ref AND every enclosing one, which is what "open" actually means for a
|
|
90
|
-
// nested disclosure [LAW:dataflow-not-control-flow].
|
|
91
|
-
export function declareHelp(
|
|
92
|
-
name: string,
|
|
93
|
-
lines: readonly string[],
|
|
94
|
-
within: readonly DisclosureRef[],
|
|
95
|
-
out: HelpArtifacts,
|
|
96
|
-
surface?: { readonly bg: string; readonly fg: string },
|
|
97
|
-
): HelpDisclosure {
|
|
98
|
-
const self: DisclosureRef = { variable: name, member: HELP_OPEN };
|
|
99
|
-
out.variables[name] = disclosureStateVar(name, DISCLOSURE_CLOSED);
|
|
100
|
-
out.actions[name] = disclosureCycleAction(name, HELP_OPEN);
|
|
101
|
-
out.segments[name] = {
|
|
102
|
-
template: disclosureTrigger(
|
|
103
|
-
name,
|
|
104
|
-
HELP_GLYPH_CLOSED,
|
|
105
|
-
DISCLOSURE_GLYPH_CLOSE,
|
|
106
|
-
),
|
|
107
|
-
...surface,
|
|
108
|
-
// The trigger is visible wherever its host surface is. An unnested `(?)`
|
|
109
|
-
// (`within` empty) is always visible, and declares no `when` at all.
|
|
110
|
-
...gateOf(within),
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// [LAW:one-source-of-truth] One line is one SEGMENT whose template is that
|
|
114
|
-
// line VERBATIM — no wrapper text, no joining, no reformatting, and no
|
|
115
|
-
// escaping. A `template` IS template source, so its static runs reach the bar
|
|
116
|
-
// unchanged; escaping belongs to text spliced INSIDE a quoted `{{ }}` argument
|
|
117
|
-
// (what `disclosureTrigger` above does, and this is not), and applying it here
|
|
118
|
-
// would put backslashes on the bar the moment a sentence used a quote. That is
|
|
119
|
-
// what makes "the bar's help IS the corpus" checkable as identity rather than
|
|
120
|
-
// as string similarity: the declaration a test reads and the sentence
|
|
121
|
-
// `src/help-text.ts` exports are one value, byte for byte.
|
|
122
|
-
const bodyGate = disclosureGate(self, ...within);
|
|
123
|
-
const children = lines.map((line, i): LayoutNode => {
|
|
124
|
-
const lineName = `${name}.${i}`;
|
|
125
|
-
// [LAW:single-enforcer] The gate lives on the body container below and
|
|
126
|
-
// nowhere else — the render walk ANDs an ancestor's `when` into every
|
|
127
|
-
// descendant (src/dsl/render.ts:764), so a copy here would be a second
|
|
128
|
-
// enforcer of one predicate with nothing keeping the two equal.
|
|
129
|
-
out.segments[lineName] = { template: line, ...surface };
|
|
130
|
-
return { kind: "segment", name: lineName };
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
trigger: { kind: "segment", name },
|
|
135
|
-
body: {
|
|
136
|
-
kind: "container",
|
|
137
|
-
direction: "horizontal",
|
|
138
|
-
children,
|
|
139
|
-
when: bodyGate,
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// The enclosing gate as an optional `when` field — the codebase's standard
|
|
145
|
-
// spelling for "this field exists only when there is something to put in it",
|
|
146
|
-
// keeping `disclosureGate`'s first argument required so a gate over nothing
|
|
147
|
-
// stays unrepresentable.
|
|
148
|
-
function gateOf(within: readonly DisclosureRef[]): { when?: string } {
|
|
149
|
-
const [first, ...rest] = within;
|
|
150
|
-
return first === undefined ? {} : { when: disclosureGate(first, ...rest) };
|
|
151
|
-
}
|
package/src/config/ident.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// [LAW:one-source-of-truth] THE identifier-collapse rule — collapse an
|
|
2
|
-
// arbitrary name (a segment, action, preset, or menu-host name) to an
|
|
3
|
-
// identifier-shaped fragment so synthesized var/action/segment names carry
|
|
4
|
-
// no dots, brackets, or other characters that would break a template field
|
|
5
|
-
// path or a synthesis-time accumulator key. Every non-alphanumeric RUN
|
|
6
|
-
// collapses to a single `_`.
|
|
7
|
-
//
|
|
8
|
-
// [LAW:no-silent-failure] Multiple sites depend on this SAME collapse
|
|
9
|
-
// producing the SAME result for the SAME input: menu-keys.ts derives a
|
|
10
|
-
// menu's synthesized SessionState/action identity from it; edit-chrome.ts
|
|
11
|
-
// keys synthesized per-preset reset/±-chrome artifacts by it;
|
|
12
|
-
// loader/cross-ref.ts's presetIdentCollisions checks that no two preset
|
|
13
|
-
// names collide under it BEFORE edit-chrome.ts ever runs. Before this
|
|
14
|
-
// module existed, three call sites reimplemented the same regex
|
|
15
|
-
// independently (module-privacy taken too far) — a genuine drift risk: if
|
|
16
|
-
// one copy changed without the others, the collision GUARD would stop
|
|
17
|
-
// matching the collision RULE it exists to enforce, silently reopening the
|
|
18
|
-
// exact "second preset steals the first's synthesized action" bug the
|
|
19
|
-
// guard was written to prevent. One function now; every site imports it.
|
|
20
|
-
export function ident(name: string): string {
|
|
21
|
-
return name.replace(/[^A-Za-z0-9]+/g, "_");
|
|
22
|
-
}
|
package/src/config/layout-ops.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
// [LAW:one-type-per-behavior] The seam brandon-layout-edit-2gc.1 opens: a
|
|
2
|
-
// bounded, statically-enumerable vocabulary for mutating a LayoutNode tree —
|
|
3
|
-
// remove the segment named X, insert a named segment before/after an
|
|
4
|
-
// existing one. No third LayoutNode kind, no free-form tree editing: a
|
|
5
|
-
// segment's own NAME is the stable position (dsl-types.ts already makes it
|
|
6
|
-
// one — SegmentNode.name is a ref into `segments`, unaffected by a sibling
|
|
7
|
-
// being added or removed elsewhere in the tree), so there is no sibling-index
|
|
8
|
-
// to invalidate when a prior op lands.
|
|
9
|
-
//
|
|
10
|
-
// [LAW:no-ambient-temporal-coupling] A position is never a fact frozen at
|
|
11
|
-
// declaration or click time — `removeSegment`/`insertSegment` always walk
|
|
12
|
-
// the CURRENT tree handed to them. The daemon (src/daemon/cache/render.ts)
|
|
13
|
-
// replays the accumulated op log against the live merged config on every
|
|
14
|
-
// reload, so a later, unrelated config edit is what the next replay sees,
|
|
15
|
-
// never a stale snapshot.
|
|
16
|
-
//
|
|
17
|
-
// [LAW:no-silent-failure] exception: a target/anchor absent from the tree is
|
|
18
|
-
// a documented NO-OP, not a thrown error — mirroring
|
|
19
|
-
// src/config/loader/merge.ts's applySegmentPaletteOverrides, the identical
|
|
20
|
-
// failure shape one layer up. A validated action can only ever name a
|
|
21
|
-
// segment the config declares (loader/cross-ref.ts checks this at config
|
|
22
|
-
// load), so a miss here only happens after a LATER edit (to the bundled
|
|
23
|
-
// default, the user's config, or an earlier op in the same accumulated list)
|
|
24
|
-
// removed the segment — there is nothing left to apply the op to, and
|
|
25
|
-
// bricking the whole render over a stale op would be worse than dropping it.
|
|
26
|
-
|
|
27
|
-
import type { LayoutNode, SegmentNode } from "./dsl-types.js";
|
|
28
|
-
|
|
29
|
-
// [LAW:types-are-the-program] The two operations brandon-layout-edit-2gc.1
|
|
30
|
-
// ships. Both address position by NAME, never by index. A future op (e.g.
|
|
31
|
-
// "move") is a new arm here, not a new node kind or a new codec.
|
|
32
|
-
export type LayoutOp =
|
|
33
|
-
| { readonly op: "remove"; readonly target: string }
|
|
34
|
-
| {
|
|
35
|
-
readonly op: "insert";
|
|
36
|
-
readonly segment: string;
|
|
37
|
-
readonly anchor: string;
|
|
38
|
-
readonly relation: "before" | "after";
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// [LAW:single-enforcer] THE codec for a LayoutOp crossing the click wire and
|
|
42
|
-
// the config-overrides file as one opaque string. `:` is the delimiter
|
|
43
|
-
// (loader/actions.ts's segmentNameSpec rejects `:` and `/` in every name an
|
|
44
|
-
// op can carry, so decode is unambiguous — a plain split, no escaping).
|
|
45
|
-
// Encode and decode live together so the format cannot drift between the
|
|
46
|
-
// validator gate (config-validators.ts, which encodes the ONE token a
|
|
47
|
-
// declared action allows), the render side (which emits that same token),
|
|
48
|
-
// and the daemon (which decodes it back).
|
|
49
|
-
export function encodeLayoutOp(op: LayoutOp): string {
|
|
50
|
-
return op.op === "remove"
|
|
51
|
-
? `remove:${op.target}`
|
|
52
|
-
: `insert:${op.segment}:${op.anchor}:${op.relation}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// [LAW:parse-dont-validate] Returns the typed op, or null for anything that
|
|
56
|
-
// doesn't decode — the boundary a caller stamps before trusting the shape.
|
|
57
|
-
// Reached only from data this process did not itself just encode (a
|
|
58
|
-
// hand-edited or previous-version config-overrides.json), never from a
|
|
59
|
-
// value produced by encodeLayoutOp in the same click.
|
|
60
|
-
export function decodeLayoutOp(token: string): LayoutOp | null {
|
|
61
|
-
const parts = token.split(":");
|
|
62
|
-
if (parts[0] === "remove" && parts.length === 2 && parts[1]) {
|
|
63
|
-
return { op: "remove", target: parts[1] };
|
|
64
|
-
}
|
|
65
|
-
if (
|
|
66
|
-
parts[0] === "insert" &&
|
|
67
|
-
parts.length === 4 &&
|
|
68
|
-
parts[1] &&
|
|
69
|
-
parts[2] &&
|
|
70
|
-
(parts[3] === "before" || parts[3] === "after")
|
|
71
|
-
) {
|
|
72
|
-
return {
|
|
73
|
-
op: "insert",
|
|
74
|
-
segment: parts[1],
|
|
75
|
-
anchor: parts[2],
|
|
76
|
-
relation: parts[3],
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// [LAW:single-enforcer] THE one collector of "which segment names does this
|
|
83
|
-
// tree contain" — brandon-layout-edit-2gc.3's edit-chrome synthesis
|
|
84
|
-
// (src/config/edit-chrome.ts) uses it to compute both halves of the +/-
|
|
85
|
-
// affordances: which segments are PRESENT (get a `-`) and, by set difference
|
|
86
|
-
// against every declared segment, which are ADDABLE (populate the `+`
|
|
87
|
-
// picker's domain). A name appearing more than once collapses to one entry —
|
|
88
|
-
// callers that care about occurrence COUNT (none currently do) need a
|
|
89
|
-
// different walk.
|
|
90
|
-
export function collectSegmentNames(root: LayoutNode): ReadonlySet<string> {
|
|
91
|
-
const out = new Set<string>();
|
|
92
|
-
const walk = (node: LayoutNode): void => {
|
|
93
|
-
if (node.kind === "segment") {
|
|
94
|
-
out.add(node.name);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
for (const child of node.children) walk(child);
|
|
98
|
-
};
|
|
99
|
-
walk(root);
|
|
100
|
-
return out;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// [LAW:dataflow-not-control-flow] Ops are DATA folded over the tree in
|
|
104
|
-
// order — replaying zero ops is the identity fold, replaying N is the same
|
|
105
|
-
// reduce for every N. No branch on "are there ops to apply."
|
|
106
|
-
export function applyLayoutOps(
|
|
107
|
-
root: LayoutNode,
|
|
108
|
-
ops: readonly LayoutOp[],
|
|
109
|
-
): LayoutNode {
|
|
110
|
-
return ops.reduce(applyOne, root);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function applyOne(root: LayoutNode, op: LayoutOp): LayoutNode {
|
|
114
|
-
return op.op === "remove"
|
|
115
|
-
? removeSegment(root, op.target)
|
|
116
|
-
: insertSegment(root, op.segment, op.anchor, op.relation);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// [LAW:types-are-the-program] Total and non-throwing: every LayoutNode maps
|
|
120
|
-
// to a LayoutNode, whether or not `target` is present. Acts on the FIRST
|
|
121
|
-
// segment found named `target`, in walkNodes' canonical pre-order — a
|
|
122
|
-
// deterministic, documented tie-break for the edge case of a duplicate name,
|
|
123
|
-
// never an ambiguous or thrown outcome. `done` is a closure-local flag (fresh
|
|
124
|
-
// per call, never shared across calls) that stops the search the instant a
|
|
125
|
-
// match is spliced out, so an EARLIER sibling's match always wins over a
|
|
126
|
-
// LATER one — exactly walkNodes' visit order, not merely "this container's
|
|
127
|
-
// own children before its descendants" (a subtly different, wrong order a
|
|
128
|
-
// single-pass children-array scan would produce). An absent target returns
|
|
129
|
-
// the identical root reference.
|
|
130
|
-
function removeSegment(root: LayoutNode, target: string): LayoutNode {
|
|
131
|
-
let done = false;
|
|
132
|
-
function walk(node: LayoutNode): LayoutNode {
|
|
133
|
-
if (done || node.kind === "segment") return node;
|
|
134
|
-
const children: LayoutNode[] = [];
|
|
135
|
-
for (const child of node.children) {
|
|
136
|
-
if (!done && child.kind === "segment" && child.name === target) {
|
|
137
|
-
done = true;
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
children.push(walk(child));
|
|
141
|
-
}
|
|
142
|
-
return { ...node, children };
|
|
143
|
-
}
|
|
144
|
-
const result = walk(root);
|
|
145
|
-
return done ? result : root;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// [LAW:types-are-the-program] Splices a new `{ kind: "segment", name:
|
|
149
|
-
// segment }` node immediately before/after the FIRST node found named
|
|
150
|
-
// `anchor`, in walkNodes' canonical pre-order (see removeSegment's `done`
|
|
151
|
-
// note — the same closure-local stop-at-first-match shape). Total and
|
|
152
|
-
// non-throwing: an absent anchor returns the identical root reference.
|
|
153
|
-
function insertSegment(
|
|
154
|
-
root: LayoutNode,
|
|
155
|
-
segment: string,
|
|
156
|
-
anchor: string,
|
|
157
|
-
relation: "before" | "after",
|
|
158
|
-
): LayoutNode {
|
|
159
|
-
let done = false;
|
|
160
|
-
function walk(node: LayoutNode): LayoutNode {
|
|
161
|
-
if (done || node.kind === "segment") return node;
|
|
162
|
-
const children: LayoutNode[] = [];
|
|
163
|
-
for (const child of node.children) {
|
|
164
|
-
if (!done && child.kind === "segment" && child.name === anchor) {
|
|
165
|
-
done = true;
|
|
166
|
-
const inserted: SegmentNode = { kind: "segment", name: segment };
|
|
167
|
-
if (relation === "before") children.push(inserted, child);
|
|
168
|
-
else children.push(child, inserted);
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
children.push(walk(child));
|
|
172
|
-
}
|
|
173
|
-
return { ...node, children };
|
|
174
|
-
}
|
|
175
|
-
const result = walk(root);
|
|
176
|
-
return done ? result : root;
|
|
177
|
-
}
|