@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,714 +0,0 @@
|
|
|
1
|
-
// [LAW:single-enforcer] All cross-reference resolution on the MERGED config:
|
|
2
|
-
// layout nodes name declared segments, every template-bearing field references
|
|
3
|
-
// only existing variables/actions, depends_on points at declared variables, and
|
|
4
|
-
// state/set-action configs declare the session.id anchor. Runs after merge so a
|
|
5
|
-
// user surface can reference default-provided segments/actions. This file changes
|
|
6
|
-
// when the visibility/scoping rules between config parts change.
|
|
7
|
-
|
|
8
|
-
import JSON5 from "json5";
|
|
9
|
-
import {
|
|
10
|
-
hasCacheField,
|
|
11
|
-
walkNodes,
|
|
12
|
-
type DslConfig,
|
|
13
|
-
type LayoutNode,
|
|
14
|
-
type PresetDecl,
|
|
15
|
-
type VariableDecl,
|
|
16
|
-
} from "../dsl-types.js";
|
|
17
|
-
import {
|
|
18
|
-
actionBindsPersist,
|
|
19
|
-
actionBindsRedo,
|
|
20
|
-
actionBindsReset,
|
|
21
|
-
actionBindsSet,
|
|
22
|
-
actionIsDual,
|
|
23
|
-
PERSIST_WHEN,
|
|
24
|
-
actionBindsUndo,
|
|
25
|
-
type ActionDecl,
|
|
26
|
-
} from "../action.js";
|
|
27
|
-
import {
|
|
28
|
-
knownOptionDomainNames,
|
|
29
|
-
perConfigDomainsFor,
|
|
30
|
-
} from "../option-domain.js";
|
|
31
|
-
import { listGlobalsFieldNames } from "./globals.js";
|
|
32
|
-
import { parsePersistTarget } from "./persist-target.js";
|
|
33
|
-
import { presetNames } from "../presets.js";
|
|
34
|
-
import {
|
|
35
|
-
canHostSessionState,
|
|
36
|
-
countAnchors,
|
|
37
|
-
isSettingsAnchor,
|
|
38
|
-
SESSION_ID_VAR,
|
|
39
|
-
SETTINGS_ANCHOR,
|
|
40
|
-
} from "../settings-menu.js";
|
|
41
|
-
import { ident } from "../ident.js";
|
|
42
|
-
import { findKeyLine } from "./diagnostics.js";
|
|
43
|
-
import { isPlainObject, type ValidateCtx } from "./validate-core.js";
|
|
44
|
-
import {
|
|
45
|
-
extractActionRefs,
|
|
46
|
-
extractPickerMenuRefs,
|
|
47
|
-
extractTemplateRefs,
|
|
48
|
-
refResolves,
|
|
49
|
-
} from "./refs.js";
|
|
50
|
-
|
|
51
|
-
// [LAW:one-source-of-truth] The renamed built-in segments: old name → current
|
|
52
|
-
// name. A user config (which merges on top of the bundled default) that names a
|
|
53
|
-
// renamed segment in `root` finds no matching declaration and would otherwise
|
|
54
|
-
// get the generic "does not match any declared segment" error. This map turns
|
|
55
|
-
// that into a migration pointer [LAW:no-silent-failure] — data, not a per-name
|
|
56
|
-
// branch, so a future rename is one row here, not new control flow.
|
|
57
|
-
export const RENAMED_SEGMENTS: Readonly<Record<string, string>> = {
|
|
58
|
-
gitTaculous: "gitaculous",
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// [LAW:single-enforcer] Runs HERE — on `cfg.presets`, the MERGED map — not
|
|
62
|
-
// in loader/presets.ts's per-file structural pass (where a round-1 version
|
|
63
|
-
// of this check lived): that pass validates one config source at a time
|
|
64
|
-
// (the bundled default's own RAW_DEFAULT_DSL_CONFIG, or a user's file,
|
|
65
|
-
// independently), so it could only ever catch a collision between two
|
|
66
|
-
// preset names declared in ONE source. synthesizeEditChrome — the thing
|
|
67
|
-
// this guard protects, which keys its per-preset reset action/segment (and
|
|
68
|
-
// the pre-existing per-gap +/- actions) by `ident(presetName)` in a plain
|
|
69
|
-
// object accumulator with no re-entrant cross-ref check — runs on the
|
|
70
|
-
// MERGED config (dsl-loader.ts), so the collision it can actually produce
|
|
71
|
-
// is a merged one: a user preset whose ident collides with a name the
|
|
72
|
-
// BUNDLED library or a different file contributed. This is the one place
|
|
73
|
-
// that sees that merged set, so it's the one place that can prove no
|
|
74
|
-
// collision exists in it.
|
|
75
|
-
//
|
|
76
|
-
// [LAW:one-source-of-truth] `ident` is imported from ../ident.ts — the ONE
|
|
77
|
-
// collapse rule menu-keys.ts, edit-chrome.ts, and this guard all now share,
|
|
78
|
-
// so a future tweak to the rule can't silently desync the guard from the
|
|
79
|
-
// thing it checks.
|
|
80
|
-
//
|
|
81
|
-
// [LAW:no-silent-failure] Two preset names that collapse to the SAME
|
|
82
|
-
// synthesis identifier (e.g. "quick-look" and "quick_look" both → "quick_
|
|
83
|
-
// look") would silently steal each other's synthesized artifacts: the
|
|
84
|
-
// SECOND preset processed overwrites the first's entries, leaving the
|
|
85
|
-
// first preset's already-built tree holding a segment ref to a name that
|
|
86
|
-
// now points at the second preset's reset action. A user clicking "reset"
|
|
87
|
-
// on preset A would silently reset preset B instead.
|
|
88
|
-
function presetIdentCollisions(
|
|
89
|
-
ctx: ValidateCtx,
|
|
90
|
-
presets: Readonly<Record<string, PresetDecl>>,
|
|
91
|
-
): void {
|
|
92
|
-
const byIdent = new Map<string, string[]>();
|
|
93
|
-
for (const name of Object.keys(presets)) {
|
|
94
|
-
const id = ident(name);
|
|
95
|
-
const names = byIdent.get(id);
|
|
96
|
-
if (names) names.push(name);
|
|
97
|
-
else byIdent.set(id, [name]);
|
|
98
|
-
}
|
|
99
|
-
for (const [id, names] of byIdent) {
|
|
100
|
-
if (names.length < 2) continue;
|
|
101
|
-
ctx.issues.push({
|
|
102
|
-
path: "presets",
|
|
103
|
-
message: `preset names ${names.map((n) => JSON.stringify(n)).join(" and ")} both collapse to the same synthesis identifier "${id}" — edit mode's synthesized reset affordance would silently steal one preset's action for the other. Rename one.`,
|
|
104
|
-
line: findKeyLine(ctx.source, ["presets"]),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function validateCrossReferences(
|
|
110
|
-
ctx: ValidateCtx,
|
|
111
|
-
cfg: DslConfig,
|
|
112
|
-
): void {
|
|
113
|
-
// [LAW:locality-or-seam] globals.look names a member of the MERGED looks
|
|
114
|
-
// block (a user's default may be a bundled look — same reason every cross-ref
|
|
115
|
-
// runs post-merge). Same existence-check shape as layout→segments; an unknown
|
|
116
|
-
// name is a load error, never a silent identity fallback.
|
|
117
|
-
if (
|
|
118
|
-
cfg.globals.look !== undefined &&
|
|
119
|
-
!Object.prototype.hasOwnProperty.call(cfg.looks, cfg.globals.look)
|
|
120
|
-
) {
|
|
121
|
-
ctx.issues.push({
|
|
122
|
-
path: "globals.look",
|
|
123
|
-
message: `globals.look "${cfg.globals.look}" does not match any declared look (have: ${Object.keys(cfg.looks).join(", ")})`,
|
|
124
|
-
line: findKeyLine(ctx.source, ["globals", "look"]),
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
// [LAW:one-type-per-behavior] globals.preset is globals.look one dimension
|
|
128
|
-
// over — the same post-merge membership check against the same kind of
|
|
129
|
-
// per-config block, for the same reason (a user's default may name a
|
|
130
|
-
// bundled preset). A typo'd DEFAULT is a load error even though a stale
|
|
131
|
-
// SESSION pick collapses silently to the floor: the config file is authored
|
|
132
|
-
// and re-readable, so naming a preset that does not exist is a mistake we can
|
|
133
|
-
// point at; a session pick is a click made against a config that has since
|
|
134
|
-
// changed, which is not [LAW:no-silent-failure].
|
|
135
|
-
if (
|
|
136
|
-
cfg.globals.preset !== undefined &&
|
|
137
|
-
!Object.prototype.hasOwnProperty.call(cfg.presets, cfg.globals.preset)
|
|
138
|
-
) {
|
|
139
|
-
ctx.issues.push({
|
|
140
|
-
path: "globals.preset",
|
|
141
|
-
message: `globals.preset "${cfg.globals.preset}" does not match any declared preset (have: ${Object.keys(cfg.presets).join(", ")})`,
|
|
142
|
-
line: findKeyLine(ctx.source, ["globals", "preset"]),
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
presetIdentCollisions(ctx, cfg.presets);
|
|
146
|
-
// [LAW:one-source-of-truth] A `set … from` NAME must resolve — checked
|
|
147
|
-
// against this config's per-config domains ("looks", the merged looks:
|
|
148
|
-
// block) plus the global registry (themes/styles, and any future
|
|
149
|
-
// registration), the SAME set resolveOptionDomain consults at render and
|
|
150
|
-
// gate-derivation time. An inline array `from` is its own domain — nothing
|
|
151
|
-
// to resolve. Runs post-merge for the same reason globals.look does above:
|
|
152
|
-
// "looks" isn't fully known until the user's looks: block has merged onto
|
|
153
|
-
// the bundled stdlib.
|
|
154
|
-
const optionDomains = perConfigDomainsFor(cfg);
|
|
155
|
-
for (const [name, a] of Object.entries(cfg.actions)) {
|
|
156
|
-
if (!("set" in a) || !("from" in a) || typeof a.from !== "string") continue;
|
|
157
|
-
if (!knownOptionDomainNames(optionDomains).includes(a.from)) {
|
|
158
|
-
ctx.issues.push({
|
|
159
|
-
path: `actions.${name}.from`,
|
|
160
|
-
message: `actions.${name} from: references unknown option domain "${a.from}" (have: ${knownOptionDomainNames(optionDomains).join(", ")})`,
|
|
161
|
-
line: findKeyLine(ctx.source, ["actions", name, "from"]),
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
// [LAW:no-silent-failure] A dual action's `persistWhen` must name a key some
|
|
166
|
-
// `state` variable declares. The structural pass proves only that it is a
|
|
167
|
-
// deliverable wire key; whether it RESOLVES is a cross-ref concern, exactly
|
|
168
|
-
// as `from`'s domain is above.
|
|
169
|
-
//
|
|
170
|
-
// Without this, a typo loads perfectly cleanly and then does nothing
|
|
171
|
-
// forever: compileDual falls back to the raw key name, activeDestination
|
|
172
|
-
// reads it, finds nothing, and `parseSessionBoolean` answers null — which
|
|
173
|
-
// means "session". So the checkbox the author wired to the real key flips a
|
|
174
|
-
// value no control reads, the destination never changes, and there is no
|
|
175
|
-
// error anywhere to explain why. A silently-permanent session write is the
|
|
176
|
-
// worst possible shape for this failure, since it looks exactly like
|
|
177
|
-
// working software.
|
|
178
|
-
for (const [name, a] of Object.entries(cfg.actions)) {
|
|
179
|
-
if (!actionIsDual(a)) continue;
|
|
180
|
-
const selector = a[PERSIST_WHEN];
|
|
181
|
-
if (!declaresStateKey(cfg, selector)) {
|
|
182
|
-
ctx.issues.push({
|
|
183
|
-
path: `actions.${name}.${PERSIST_WHEN}`,
|
|
184
|
-
message: `actions.${name} ${PERSIST_WHEN}: "${selector}" is not a declared state key — a dual action's selector must name a { kind: "state", key: "${selector}" } variable, or the destination can never change`,
|
|
185
|
-
line: findKeyLine(ctx.source, ["actions", name, PERSIST_WHEN]),
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
// [LAW:no-silent-failure] A `persist`/`reset` target must name a REAL
|
|
190
|
-
// Globals field OR a declared segment's `palette` (candybar-config-engine-
|
|
191
|
-
// 71o.6 — `segments.<name>.palette`, parsed by the one shared authority in
|
|
192
|
-
// persist-target.ts) — the loader's structural pass (loader/actions.ts)
|
|
193
|
-
// only proves the key is non-empty/slash-free, the same shape a `set` key
|
|
194
|
-
// needs for the wire, but a persist/reset key additionally has to land
|
|
195
|
-
// somewhere real. Catching a typo (`persist: "pallete"`) or a dangling
|
|
196
|
-
// segment name here turns a confusing click-time "registration invariant
|
|
197
|
-
// broken" error into a clear load-time one naming the actual allowed
|
|
198
|
-
// targets — the same species of fix as the `from` domain check just above.
|
|
199
|
-
for (const [name, a] of Object.entries(cfg.actions)) {
|
|
200
|
-
const key = "persist" in a ? a.persist : "reset" in a ? a.reset : null;
|
|
201
|
-
if (key === null) continue;
|
|
202
|
-
const discriminator = "persist" in a ? "persist" : "reset";
|
|
203
|
-
const target = parsePersistTarget(key);
|
|
204
|
-
if (target === null) {
|
|
205
|
-
ctx.issues.push({
|
|
206
|
-
path: `actions.${name}.${discriminator}`,
|
|
207
|
-
message: `actions.${name}: "${key}" is not a config globals field (have: ${listGlobalsFieldNames().join(", ")}), a "segments.<name>.palette" target, or a "presets.<name>.rootOps" target`,
|
|
208
|
-
line: findKeyLine(ctx.source, ["actions", name, discriminator]),
|
|
209
|
-
});
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
if (target.scope === "preset-root-ops") {
|
|
213
|
-
checkPresetRootOpsTarget(
|
|
214
|
-
ctx,
|
|
215
|
-
cfg,
|
|
216
|
-
name,
|
|
217
|
-
discriminator,
|
|
218
|
-
key,
|
|
219
|
-
target.preset,
|
|
220
|
-
a,
|
|
221
|
-
);
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (target.scope !== "segment-palette") continue;
|
|
225
|
-
if (!Object.prototype.hasOwnProperty.call(cfg.segments, target.segment)) {
|
|
226
|
-
ctx.issues.push({
|
|
227
|
-
path: `actions.${name}.${discriminator}`,
|
|
228
|
-
message: `actions.${name}: "${key}" names segment "${target.segment}" which is not declared (have segments: ${Object.keys(cfg.segments).join(", ")})`,
|
|
229
|
-
line: findKeyLine(ctx.source, ["actions", name, discriminator]),
|
|
230
|
-
});
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
// [LAW:types-are-the-program] A palette is a NAME, not a number — a
|
|
234
|
-
// bounded stepper (`min`/`max`/`by`) has no meaning over it, unlike a
|
|
235
|
-
// Globals field where nothing today enforces value/field-kind agreement
|
|
236
|
-
// either way. Rejecting it here (rather than tolerating a numeric-string
|
|
237
|
-
// palette name that only fails later at `paletteForThemeName`) keeps
|
|
238
|
-
// the failure at load time, next to the typo it actually is.
|
|
239
|
-
if ("min" in a) {
|
|
240
|
-
ctx.issues.push({
|
|
241
|
-
path: `actions.${name}.${discriminator}`,
|
|
242
|
-
message: `actions.${name}: "${key}" is a segment palette target and cannot use a bounded stepper (min/max/by) — use "to", "from", or "cycle" instead`,
|
|
243
|
-
line: findKeyLine(ctx.source, ["actions", name, discriminator]),
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// [LAW:one-source-of-truth] THE set of resolvable variable names — a
|
|
248
|
-
// faithful mirror of the runtime store's key set (declareOne in
|
|
249
|
-
// src/dsl/render.ts registers globals under their bare names and segment
|
|
250
|
-
// locals under segName.varName, nothing else). The runtime scope proxy
|
|
251
|
-
// (src/template-engine/scope.ts) resolves only keys literally present in
|
|
252
|
-
// the store, and the depends_on reaction (src/var-system/sources.ts) calls
|
|
253
|
-
// store.read with each listed name verbatim — so exactly the names in this
|
|
254
|
-
// set exist at runtime. One set for every reference surface, template refs
|
|
255
|
-
// and depends_on lists alike: a name's meaning is a pure function of the
|
|
256
|
-
// name string, never of which segment declares or renders it.
|
|
257
|
-
const templateScope = new Set<string>(Object.keys(cfg.variables));
|
|
258
|
-
for (const [segName, seg] of Object.entries(cfg.segments)) {
|
|
259
|
-
if (!seg.vars) continue;
|
|
260
|
-
for (const v of Object.keys(seg.vars)) templateScope.add(`${segName}.${v}`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// [LAW:single-enforcer] ONE pre-order walk over the canonical node tree owns
|
|
264
|
-
// every layout cross-ref: each cells node's segment names must resolve to a
|
|
265
|
-
// declared segment, and any node's `when` predicate (a template like any
|
|
266
|
-
// other) must reference only existing variables. Cross-ref runs on the MERGED
|
|
267
|
-
// config so a node can name default-provided segments without re-declaring
|
|
268
|
-
// them. It traverses the canonical tree — the raw `layout`-vs-`root` authoring
|
|
269
|
-
// form is already collapsed and unrecoverable post-merge — so the path
|
|
270
|
-
// describes the tree and `line` points at whichever layout key the user wrote.
|
|
271
|
-
// [LAW:one-source-of-truth] Which top-level layout surface the user authored
|
|
272
|
-
// is read from the PARSED structure, not a text probe: a nested key named
|
|
273
|
-
// `root` (a variable, a segment) — or `layout` (a `time` var's `layout`
|
|
274
|
-
// field) — would fool a raw `findKeyLine` search and misclassify the config.
|
|
275
|
-
// Validation is cold-path, so reading the source's top-level keys is exact.
|
|
276
|
-
// The reported path/message then point at the surface the user wrote.
|
|
277
|
-
//
|
|
278
|
-
// [LAW:one-type-per-behavior] A PRESET's `root` is a root: it gets this exact
|
|
279
|
-
// walk, not a reduced copy. The only thing that varies between the config's
|
|
280
|
-
// own tree and a preset's is the diagnostic key + line — data threaded in,
|
|
281
|
-
// never a second traversal that could learn a different idea of what a valid
|
|
282
|
-
// layout is. This is what makes `cc-candybar check` catch a preset staging a
|
|
283
|
-
// segment nobody declared.
|
|
284
|
-
//
|
|
285
|
-
// [LAW:single-enforcer] The menu precondition is asked once, of the same
|
|
286
|
-
// predicate synthesizeSettingsMenu gates on, and read by every tree walked.
|
|
287
|
-
const menuWillSynthesize = canHostSessionState(cfg);
|
|
288
|
-
const checkLayoutTree = (
|
|
289
|
-
root: LayoutNode,
|
|
290
|
-
layoutKey: string,
|
|
291
|
-
layoutLine: number | undefined,
|
|
292
|
-
): void => {
|
|
293
|
-
// [LAW:one-source-of-truth] The global settings menu's anchor is a POSITION
|
|
294
|
-
// an author may place and this walk must therefore accept, even though no
|
|
295
|
-
// config declares a segment by that name — synthesizeSettingsMenu provides
|
|
296
|
-
// it unconditionally, immediately after these checks pass. Two placements is
|
|
297
|
-
// the real error: one state key holds one open state, so a second anchor
|
|
298
|
-
// would be a second toggle writing one disclosure, with two bodies claiming
|
|
299
|
-
// to be it. Counted over the SAME census the synthesis reads, so "placed"
|
|
300
|
-
// means one thing [LAW:single-enforcer].
|
|
301
|
-
if (countAnchors(root) > 1) {
|
|
302
|
-
ctx.issues.push({
|
|
303
|
-
path: layoutKey,
|
|
304
|
-
message: `${layoutKey} places the global settings menu anchor "${SETTINGS_ANCHOR}" ${countAnchors(root)} times — it may appear at most once per layout (it is one disclosure, and one state key holds one open state). Remove all but the placement you want; removing every placement puts the menu at its default position.`,
|
|
305
|
-
line: layoutLine,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
for (const node of walkNodes(root)) {
|
|
309
|
-
// [LAW:locality-or-seam] A node's `when` reads the global scope (bare
|
|
310
|
-
// globals + namespaced segment vars) — the same existence-check shape as a
|
|
311
|
-
// segment template, surfaced at load time.
|
|
312
|
-
if (node.when !== undefined) {
|
|
313
|
-
checkTemplateRefs(ctx, `${layoutKey}.when`, node.when, templateScope, {
|
|
314
|
-
line: layoutLine,
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
if (node.kind !== "segment") continue;
|
|
318
|
-
if (isSettingsAnchor(node.name)) {
|
|
319
|
-
// [LAW:one-source-of-truth] Accepting the anchor asserts that
|
|
320
|
-
// synthesizeSettingsMenu WILL declare a segment by this name, so the
|
|
321
|
-
// acceptance reads the very predicate that pass decides by rather than
|
|
322
|
-
// assuming its answer. When it is false the reference is genuinely
|
|
323
|
-
// dangling, and the error names the unmet precondition: the author
|
|
324
|
-
// placed a documented anchor, they did not typo a segment name, and the
|
|
325
|
-
// generic "does not match any declared segment" would teach the wrong
|
|
326
|
-
// lesson [LAW:no-silent-failure].
|
|
327
|
-
if (!menuWillSynthesize) {
|
|
328
|
-
ctx.issues.push({
|
|
329
|
-
path: layoutKey,
|
|
330
|
-
message: `${layoutKey} places the global settings menu anchor "${SETTINGS_ANCHOR}", but this config declares no "${SESSION_ID_VAR}" variable — the menu is a click surface and every click composes a URL from "${SESSION_ID_VAR}", so it is not synthesized for a config without it. Declare a "${SESSION_ID_VAR}" variable (any config merged onto the bundled default inherits one) or remove the anchor placement.`,
|
|
331
|
-
line: layoutLine,
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
if (!Object.prototype.hasOwnProperty.call(cfg.segments, node.name)) {
|
|
337
|
-
const renamed = RENAMED_SEGMENTS[node.name];
|
|
338
|
-
const hint =
|
|
339
|
-
renamed !== undefined
|
|
340
|
-
? ` (the built-in segment "${node.name}" was renamed to "${renamed}" — update this reference)`
|
|
341
|
-
: "";
|
|
342
|
-
ctx.issues.push({
|
|
343
|
-
path: layoutKey,
|
|
344
|
-
message: `${layoutKey} entry "${node.name}" does not match any declared segment${hint}`,
|
|
345
|
-
line: layoutLine,
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
const layoutKey = authoredLayoutKey(ctx.source);
|
|
351
|
-
checkLayoutTree(cfg.root, layoutKey, findKeyLine(ctx.source, [layoutKey]));
|
|
352
|
-
for (const [name, preset] of Object.entries(cfg.presets)) {
|
|
353
|
-
if (preset.root === undefined) continue;
|
|
354
|
-
checkLayoutTree(
|
|
355
|
-
preset.root,
|
|
356
|
-
`presets.${name}.root`,
|
|
357
|
-
findKeyLine(ctx.source, ["presets", name, "root"]),
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// For each variable's template/cache.key, every dotted ref must exist
|
|
362
|
-
// (full path OR a prefix that matches an existing variable's namespace).
|
|
363
|
-
for (const [name, v] of Object.entries(cfg.variables)) {
|
|
364
|
-
checkVarRefs(ctx, `variables.${name}`, v, templateScope);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
for (const [segName, seg] of Object.entries(cfg.segments)) {
|
|
368
|
-
// [LAW:one-source-of-truth] Segment templates check against the SAME
|
|
369
|
-
// templateScope as everything else — segment locals resolve via the
|
|
370
|
-
// namespaced segName.varName form only, exactly as the runtime store
|
|
371
|
-
// keys them. The segment name is passed purely as a diagnostic hint: a
|
|
372
|
-
// bare ref to an own local is rejected with a message naming the
|
|
373
|
-
// namespaced form the author should write.
|
|
374
|
-
if (seg.vars) {
|
|
375
|
-
for (const [vName, vDecl] of Object.entries(seg.vars)) {
|
|
376
|
-
checkVarRefs(
|
|
377
|
-
ctx,
|
|
378
|
-
`segments.${segName}.vars.${vName}`,
|
|
379
|
-
vDecl,
|
|
380
|
-
templateScope,
|
|
381
|
-
segName,
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
// [LAW:locality-or-seam] Variable refs AND `{{ action }}`/`{{ picker }}` refs
|
|
386
|
-
// are checked across EVERY template-bearing field, not just `template` —
|
|
387
|
-
// bg/fg/when are templates too, so an unknown ref in them is a load error, not
|
|
388
|
-
// a render-time surprise. Same existence-check shape as layout→segments; runs
|
|
389
|
-
// on the merged config so a segment can reference a default-provided action.
|
|
390
|
-
for (const field of ["template", "bg", "fg", "when"] as const) {
|
|
391
|
-
const tpl = seg[field];
|
|
392
|
-
if (typeof tpl !== "string") continue;
|
|
393
|
-
checkTemplateRefs(
|
|
394
|
-
ctx,
|
|
395
|
-
`segments.${segName}.${field}`,
|
|
396
|
-
tpl,
|
|
397
|
-
templateScope,
|
|
398
|
-
{
|
|
399
|
-
segCtx: segName,
|
|
400
|
-
},
|
|
401
|
-
);
|
|
402
|
-
// [LAW:locality-or-seam] `{{ action "name" … }}` refs resolve against the
|
|
403
|
-
// action table on the merged config so a segment can reference a
|
|
404
|
-
// default-provided action.
|
|
405
|
-
for (const aref of extractActionRefs(tpl)) {
|
|
406
|
-
if (!Object.prototype.hasOwnProperty.call(cfg.actions, aref)) {
|
|
407
|
-
ctx.issues.push({
|
|
408
|
-
path: `segments.${segName}.${field}`,
|
|
409
|
-
message: `${field} references unknown action "${aref}"`,
|
|
410
|
-
line: findKeyLine(ctx.source, ["segments", segName, field]),
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
// [LAW:locality-or-seam] A `{{ picker "apply" "page" … }}` OR `{{ menu
|
|
415
|
-
// "apply" "page" … }}` references two named actions — both resolve against
|
|
416
|
-
// the action table at load, same existence-check shape as a bare action ref.
|
|
417
|
-
// A menu binds the same pair as a picker, so it routes through the SAME
|
|
418
|
-
// check rather than failing only when the disclosure is opened.
|
|
419
|
-
for (const pref of extractPickerMenuRefs(tpl)) {
|
|
420
|
-
if (!Object.prototype.hasOwnProperty.call(cfg.actions, pref)) {
|
|
421
|
-
ctx.issues.push({
|
|
422
|
-
path: `segments.${segName}.${field}`,
|
|
423
|
-
message: `${field} references unknown action "${pref}" (in a picker or menu)`,
|
|
424
|
-
line: findKeyLine(ctx.source, ["segments", segName, field]),
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// depends_on lists must point at declared variables — checked against the
|
|
432
|
-
// SAME templateScope as template refs, since both resolve against the one
|
|
433
|
-
// runtime store. The segment name is a diagnostic hint only, never a
|
|
434
|
-
// resolution rule, exactly as for templates.
|
|
435
|
-
for (const [name, v] of Object.entries(cfg.variables)) {
|
|
436
|
-
checkDependsOn(ctx, `variables.${name}`, v, templateScope);
|
|
437
|
-
}
|
|
438
|
-
for (const [segName, seg] of Object.entries(cfg.segments)) {
|
|
439
|
-
if (!seg.vars) continue;
|
|
440
|
-
for (const [vName, vDecl] of Object.entries(seg.vars)) {
|
|
441
|
-
checkDependsOn(
|
|
442
|
-
ctx,
|
|
443
|
-
`segments.${segName}.vars.${vName}`,
|
|
444
|
-
vDecl,
|
|
445
|
-
templateScope,
|
|
446
|
-
segName,
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// [LAW:verifiable-goals] state-kind variables have an implicit dependency
|
|
452
|
-
// on the canonical session-id input variable. Same shape as the
|
|
453
|
-
// depends_on / template-ref existence checks above — surface a missing
|
|
454
|
-
// anchor at load time so the user fixes the config from a config-file
|
|
455
|
-
// error message, not from a render-time ReferenceError.
|
|
456
|
-
//
|
|
457
|
-
// [LAW:types-are-the-program] Check against `cfg.variables` directly: the
|
|
458
|
-
// accept/reject table for this predicate is "GLOBAL session.id declared".
|
|
459
|
-
// A segment-local declaration named "session.id" registers at runtime as
|
|
460
|
-
// `<seg>.session.id` and does NOT satisfy declareState's read of the
|
|
461
|
-
// global `session.id` box.
|
|
462
|
-
// [LAW:verifiable-goals] A widget `set` action composes a set-state click URL
|
|
463
|
-
// whose first segment is `session.id` (read from the store at render). Without
|
|
464
|
-
// a global session.id the URL is malformed and the daemon rejects the click
|
|
465
|
-
// (requireSessionId is the single enforcer — it rejects empty/slash session
|
|
466
|
-
// ids loudly, so there is no silent corruption; this surfaces the SAME
|
|
467
|
-
// requirement at load instead of at first click). Same anchor + same shape as
|
|
468
|
-
// the state-kind requirement above; OR them so either trigger fires it once.
|
|
469
|
-
// [LAW:dataflow-not-control-flow] A `set` action composes a set-state click URL
|
|
470
|
-
// whose first segment is session.id. OR it into the same requirement so an
|
|
471
|
-
// actions-only config (no state vars) still demands the anchor it needs. A
|
|
472
|
-
// picker's ✕/←/→/apply-close all go through `set` actions, so this covers them.
|
|
473
|
-
if (
|
|
474
|
-
(hasStateKind(cfg) || hasActionSetAction(cfg)) &&
|
|
475
|
-
!Object.prototype.hasOwnProperty.call(cfg.variables, "session.id")
|
|
476
|
-
) {
|
|
477
|
-
ctx.issues.push({
|
|
478
|
-
path: "variables.session.id",
|
|
479
|
-
message: `state reads and action set-writes require a global "session.id" variable (segment-local declarations do not satisfy this — declareState/set-state both read the global box; conventionally { kind: "input", path: "session_id" })`,
|
|
480
|
-
line: findKeyLine(ctx.source, ["variables"]),
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// [LAW:no-silent-failure] brandon-layout-edit-2gc.1's structural-edit target
|
|
486
|
-
// check, one arm of the persist/reset key cross-ref above. Three things must
|
|
487
|
-
// hold at load time, same spirit as the segment-palette check just above it:
|
|
488
|
-
// the preset name must be real (mirrors globals.preset's check earlier in
|
|
489
|
-
// this function), the arm pairing must make sense for this scope (only
|
|
490
|
-
// removeSegment/insertSegment address a tree — a `to`/`from`/cycle/bounded
|
|
491
|
-
// literal has no meaning as "the current op log"), and every segment name
|
|
492
|
-
// the op names must be declared.
|
|
493
|
-
function checkPresetRootOpsTarget(
|
|
494
|
-
ctx: ValidateCtx,
|
|
495
|
-
cfg: DslConfig,
|
|
496
|
-
name: string,
|
|
497
|
-
discriminator: "persist" | "reset",
|
|
498
|
-
key: string,
|
|
499
|
-
presetName: string,
|
|
500
|
-
a: ActionDecl,
|
|
501
|
-
): void {
|
|
502
|
-
const at = `actions.${name}.${discriminator}`;
|
|
503
|
-
const line = findKeyLine(ctx.source, ["actions", name, discriminator]);
|
|
504
|
-
if (!presetNames(cfg.presets).includes(presetName)) {
|
|
505
|
-
ctx.issues.push({
|
|
506
|
-
path: at,
|
|
507
|
-
message: `actions.${name}: "${key}" names preset "${presetName}" which is not declared (have: ${presetNames(cfg.presets).join(", ")})`,
|
|
508
|
-
line,
|
|
509
|
-
});
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
// [LAW:one-source-of-truth] `reset` has no value-source arm to check — its
|
|
513
|
-
// shape is a bare `{ reset: key }` — so the arm-pairing/segment checks
|
|
514
|
-
// below are `persist`-only, exactly as the "reset" action's clean-slate
|
|
515
|
-
// undo is meant to be: it clears the whole op log regardless of what wrote
|
|
516
|
-
// it.
|
|
517
|
-
if (discriminator === "reset") return;
|
|
518
|
-
const hasRemove = "removeSegment" in a;
|
|
519
|
-
const hasInsert = "insertSegment" in a;
|
|
520
|
-
// [LAW:one-source-of-truth] brandon-layout-edit-2gc.3's domain-sourced
|
|
521
|
-
// sibling — the segment name is picked at render, so only `anchor` (still
|
|
522
|
-
// literal at author time) needs the declared-segment check below.
|
|
523
|
-
const hasInsertFrom = "insertSegmentFrom" in a;
|
|
524
|
-
if (!hasRemove && !hasInsert && !hasInsertFrom) {
|
|
525
|
-
ctx.issues.push({
|
|
526
|
-
path: at,
|
|
527
|
-
message: `actions.${name}: "${key}" is a "presets.<name>.rootOps" target and can only be paired with "removeSegment", "insertSegment", or "insertSegmentFrom" (not "to"/"from"/"cycle"/bounded — those have no meaning as a tree op)`,
|
|
528
|
-
line,
|
|
529
|
-
});
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
const missing = (segName: string): boolean =>
|
|
533
|
-
!Object.prototype.hasOwnProperty.call(cfg.segments, segName);
|
|
534
|
-
if (hasRemove && "removeSegment" in a && missing(a.removeSegment)) {
|
|
535
|
-
ctx.issues.push({
|
|
536
|
-
path: at,
|
|
537
|
-
message: `actions.${name}: removeSegment "${a.removeSegment}" is not a declared segment (have: ${Object.keys(cfg.segments).join(", ")})`,
|
|
538
|
-
line,
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
if (hasInsert && "insertSegment" in a) {
|
|
542
|
-
if (missing(a.insertSegment)) {
|
|
543
|
-
ctx.issues.push({
|
|
544
|
-
path: at,
|
|
545
|
-
message: `actions.${name}: insertSegment "${a.insertSegment}" is not a declared segment (have: ${Object.keys(cfg.segments).join(", ")})`,
|
|
546
|
-
line,
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
if (missing(a.anchor)) {
|
|
550
|
-
ctx.issues.push({
|
|
551
|
-
path: at,
|
|
552
|
-
message: `actions.${name}: anchor "${a.anchor}" is not a declared segment (have: ${Object.keys(cfg.segments).join(", ")})`,
|
|
553
|
-
line,
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
if (hasInsertFrom && "insertSegmentFrom" in a && missing(a.anchor)) {
|
|
558
|
-
ctx.issues.push({
|
|
559
|
-
path: at,
|
|
560
|
-
message: `actions.${name}: anchor "${a.anchor}" is not a declared segment (have: ${Object.keys(cfg.segments).join(", ")})`,
|
|
561
|
-
line,
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// [LAW:one-source-of-truth] Every `state` variable a config declares, in BOTH
|
|
567
|
-
// scopes — global `variables` and each segment's own `vars`. A segment-local
|
|
568
|
-
// state variable is fully legitimate: src/dsl/render.ts's stateKeyToVar
|
|
569
|
-
// registers them (as `<segment>.<var>`) and it is the map a dual's selector is
|
|
570
|
-
// resolved through at render, so a load-time check that scanned only the
|
|
571
|
-
// global scope would reject configs that work.
|
|
572
|
-
function stateVars(cfg: DslConfig): VariableDecl[] {
|
|
573
|
-
return [
|
|
574
|
-
...Object.values(cfg.variables),
|
|
575
|
-
...Object.values(cfg.segments).flatMap((seg) =>
|
|
576
|
-
Object.values(seg.vars ?? {}),
|
|
577
|
-
),
|
|
578
|
-
].filter((v) => v.kind === "state");
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function hasStateKind(cfg: DslConfig): boolean {
|
|
582
|
-
return stateVars(cfg).length > 0;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Does any declared `state` variable hold this key? The question a dual's
|
|
586
|
-
// `persistWhen` selector has to answer, asked over the same two scopes.
|
|
587
|
-
function declaresStateKey(cfg: DslConfig, key: string): boolean {
|
|
588
|
-
return stateVars(cfg).some((v) => v.kind === "state" && v.key === key);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// [LAW:dataflow-not-control-flow] A config emits a set-state, set-config,
|
|
592
|
-
// reset-config, undo, OR redo click — and so needs session.id — when any
|
|
593
|
-
// declared action is a `set` (literal/option/bounded/cycle), a `persist`
|
|
594
|
-
// (its config-overrides twin), a `reset` (persist's gated undo), or an
|
|
595
|
-
// `undo`/`redo` (the overrides layer's global history step) — all five
|
|
596
|
-
// carry session.id on the wire for click-error surfacing (an empty history
|
|
597
|
-
// stack is a loud, session-scoped miss, not a silent no-op). copy/open
|
|
598
|
-
// actions write nothing, so they embed no session.id.
|
|
599
|
-
function hasActionSetAction(cfg: DslConfig): boolean {
|
|
600
|
-
return Object.values(cfg.actions).some(
|
|
601
|
-
(a) =>
|
|
602
|
-
actionBindsSet(a) ||
|
|
603
|
-
actionBindsPersist(a) ||
|
|
604
|
-
actionBindsReset(a) ||
|
|
605
|
-
actionBindsUndo(a) ||
|
|
606
|
-
actionBindsRedo(a),
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function checkVarRefs(
|
|
611
|
-
ctx: ValidateCtx,
|
|
612
|
-
declPath: string,
|
|
613
|
-
v: VariableDecl,
|
|
614
|
-
allVars: Set<string>,
|
|
615
|
-
segCtx?: string,
|
|
616
|
-
): void {
|
|
617
|
-
if (v.kind === "template") {
|
|
618
|
-
checkTemplateRefs(ctx, `${declPath}.template`, v.template, allVars, {
|
|
619
|
-
segCtx,
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
if (hasCacheField(v)) {
|
|
623
|
-
if (v.cache && "key" in v.cache) {
|
|
624
|
-
checkTemplateRefs(ctx, `${declPath}.cache.key`, v.cache.key, allVars, {
|
|
625
|
-
segCtx,
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function checkDependsOn(
|
|
632
|
-
ctx: ValidateCtx,
|
|
633
|
-
declPath: string,
|
|
634
|
-
v: VariableDecl,
|
|
635
|
-
allVars: Set<string>,
|
|
636
|
-
segCtx?: string,
|
|
637
|
-
): void {
|
|
638
|
-
if (!hasCacheField(v)) return;
|
|
639
|
-
if (!v.cache) return;
|
|
640
|
-
if (!("depends_on" in v.cache)) return;
|
|
641
|
-
for (let i = 0; i < v.cache.depends_on.length; i++) {
|
|
642
|
-
const target = v.cache.depends_on[i]!;
|
|
643
|
-
// [LAW:one-source-of-truth] Exact membership, not refResolves: the
|
|
644
|
-
// depends_on reaction calls store.read(name) with each listed name
|
|
645
|
-
// verbatim, and the store is an exact-key map. A dotted prefix that
|
|
646
|
-
// merely navigates INTO a value (resolvable in a template) is not a
|
|
647
|
-
// store key and would throw at runtime.
|
|
648
|
-
if (allVars.has(target)) continue;
|
|
649
|
-
const namespaced = segCtx !== undefined ? `${segCtx}.${target}` : undefined;
|
|
650
|
-
const hint =
|
|
651
|
-
namespaced !== undefined && allVars.has(namespaced)
|
|
652
|
-
? ` (segment-local vars are namespaced — write "${namespaced}")`
|
|
653
|
-
: "";
|
|
654
|
-
ctx.issues.push({
|
|
655
|
-
path: `${declPath}.cache.depends_on[${i}]`,
|
|
656
|
-
message: `cache.depends_on references unknown variable "${target}"${hint}`,
|
|
657
|
-
line: findKeyLine(ctx.source, [
|
|
658
|
-
...declPath.split("."),
|
|
659
|
-
"cache",
|
|
660
|
-
"depends_on",
|
|
661
|
-
]),
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function checkTemplateRefs(
|
|
667
|
-
ctx: ValidateCtx,
|
|
668
|
-
declPath: string,
|
|
669
|
-
template: string,
|
|
670
|
-
allVars: Set<string>,
|
|
671
|
-
opts?: {
|
|
672
|
-
// [LAW:one-source-of-truth] Callers whose `declPath` is not a literal key
|
|
673
|
-
// path into the source (a node `when`, whose canonical tree position no
|
|
674
|
-
// longer maps to a source key after the layout/root merge) pass the
|
|
675
|
-
// already-resolved line explicitly. Absent, the line is derived from the
|
|
676
|
-
// dotted declPath as before.
|
|
677
|
-
line?: number;
|
|
678
|
-
// The segment whose template is being checked — a diagnostic hint only,
|
|
679
|
-
// never a resolution rule. When a failing bare ref would resolve under
|
|
680
|
-
// this segment's namespace, the message names the namespaced form.
|
|
681
|
-
segCtx?: string;
|
|
682
|
-
},
|
|
683
|
-
): void {
|
|
684
|
-
for (const ref of extractTemplateRefs(template)) {
|
|
685
|
-
if (refResolves(ref, allVars)) continue;
|
|
686
|
-
const namespaced =
|
|
687
|
-
opts?.segCtx !== undefined ? `${opts.segCtx}.${ref}` : undefined;
|
|
688
|
-
const hint =
|
|
689
|
-
namespaced !== undefined && refResolves(namespaced, allVars)
|
|
690
|
-
? ` (segment-local vars are namespaced — write ".${namespaced}")`
|
|
691
|
-
: "";
|
|
692
|
-
ctx.issues.push({
|
|
693
|
-
path: declPath,
|
|
694
|
-
message: `Template references unknown variable ".${ref}"${hint}`,
|
|
695
|
-
line: opts?.line ?? findKeyLine(ctx.source, declPath.split(".")),
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// [LAW:one-source-of-truth] The authored top-level layout surface, read from the
|
|
701
|
-
// PARSED top-level keys (`root` wins; the loader already rejects authoring both).
|
|
702
|
-
// A structural read — not a text search — so a nested key named `root`/`layout`
|
|
703
|
-
// can never misclassify the config. Empty/unparseable source (the bundled
|
|
704
|
-
// default, no file) has no surface; defaults to the historical `layout` label.
|
|
705
|
-
function authoredLayoutKey(source: string): "root" | "layout" {
|
|
706
|
-
try {
|
|
707
|
-
const parsed = JSON5.parse(source);
|
|
708
|
-
if (isPlainObject(parsed) && "root" in parsed) return "root";
|
|
709
|
-
} catch {
|
|
710
|
-
// No source to read (default config) or unparseable — fall through. A real
|
|
711
|
-
// syntax error is already reported by parseDslConfig before cross-ref runs.
|
|
712
|
-
}
|
|
713
|
-
return "layout";
|
|
714
|
-
}
|