@bitmagic/cli 0.1.59-dev.2 → 0.1.59-dev.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +99 -0
  2. package/dist/assets/materials.d.ts +165 -0
  3. package/dist/assets/materials.js +395 -0
  4. package/dist/assets/materials.js.map +1 -0
  5. package/dist/assets/vxl-summary.js +12 -5
  6. package/dist/assets/vxl-summary.js.map +1 -1
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +2 -0
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/assets.d.ts +25 -0
  11. package/dist/commands/assets.js +245 -2
  12. package/dist/commands/assets.js.map +1 -1
  13. package/dist/commands/generate.d.ts +4 -0
  14. package/dist/commands/generate.js +33 -2
  15. package/dist/commands/generate.js.map +1 -1
  16. package/dist/commands/theme.d.ts +3 -0
  17. package/dist/commands/theme.js +201 -0
  18. package/dist/commands/theme.js.map +1 -0
  19. package/dist/scaffold/project-files.d.ts +1 -0
  20. package/dist/scaffold/project-files.js +47 -1
  21. package/dist/scaffold/project-files.js.map +1 -1
  22. package/dist/scaffold/project.js +2 -1
  23. package/dist/scaffold/project.js.map +1 -1
  24. package/dist/theme/hud-theme-catalog.d.ts +236 -0
  25. package/dist/theme/hud-theme-catalog.js +386 -0
  26. package/dist/theme/hud-theme-catalog.js.map +1 -0
  27. package/dist/theme/theme-color-math.d.ts +21 -0
  28. package/dist/theme/theme-color-math.js +99 -0
  29. package/dist/theme/theme-color-math.js.map +1 -0
  30. package/dist/theme/theme-io.d.ts +49 -0
  31. package/dist/theme/theme-io.js +190 -0
  32. package/dist/theme/theme-io.js.map +1 -0
  33. package/dist/theme/theme-merge.d.ts +36 -0
  34. package/dist/theme/theme-merge.js +81 -0
  35. package/dist/theme/theme-merge.js.map +1 -0
  36. package/dist/theme/theme-report.d.ts +28 -0
  37. package/dist/theme/theme-report.js +142 -0
  38. package/dist/theme/theme-report.js.map +1 -0
  39. package/dist/theme/theme-stored.d.ts +21 -0
  40. package/dist/theme/theme-stored.js +50 -0
  41. package/dist/theme/theme-stored.js.map +1 -0
  42. package/dist/theme/theme-validate.d.ts +41 -0
  43. package/dist/theme/theme-validate.js +406 -0
  44. package/dist/theme/theme-validate.js.map +1 -0
  45. package/dist/verify/hud-theme.d.ts +5 -3
  46. package/dist/verify/hud-theme.js +5 -3
  47. package/dist/verify/hud-theme.js.map +1 -1
  48. package/package.json +4 -4
@@ -0,0 +1,190 @@
1
+ /**
2
+ * The rules behind `bitmagic theme show | check | set | patch` — everything that
3
+ * is not flags and printing. Split from the command file the way `levels/`
4
+ * splits registry from command, so tests exercise the rules without a
5
+ * filesystem or a citty run.
6
+ *
7
+ * The vocabulary, validator, merge semantics and report come from `src/theme/`,
8
+ * a byte-identical mirror of game-play-agent's theme kit (the drift gate in
9
+ * game-play-agent/scripts/check-hud-catalog.ts enforces the byte identity
10
+ * against both the engine and this copy). Same inputs, same verdicts, same
11
+ * report text in both lanes.
12
+ */
13
+ import { CliError } from '../errors.js';
14
+ import { DEFAULT_THEME_TOKENS, HUD_PRESETS, isEngineFatalIssue, presetTheme, suggestClosest, validateThemeStrict, } from './hud-theme-catalog.js';
15
+ import { resolveStoredTheme } from './theme-stored.js';
16
+ import { applyMergePatch, deleteAtPath } from './theme-merge.js';
17
+ import { buildThemeReport } from './theme-report.js';
18
+ /** worldProfileData.hud.theme — the ONLY path the engine reads a theme from. */
19
+ export function readStoredTheme(world) {
20
+ const profile = world.worldProfileData;
21
+ const hud = typeof profile === 'object' && profile !== null
22
+ ? profile.hud
23
+ : undefined;
24
+ const stored = typeof hud === 'object' && hud !== null ? hud.theme : undefined;
25
+ // The stored-value→tokens mapping lives in the gated kit (theme-stored.ts),
26
+ // shared with the hosted write tool — this wrapper only digs the value out
27
+ // of the world shape and keeps it for `show`.
28
+ const resolved = resolveStoredTheme(stored);
29
+ return {
30
+ kind: resolved.kind,
31
+ stored: stored === undefined ? null : stored,
32
+ effective: resolved.tokens,
33
+ label: resolved.label,
34
+ };
35
+ }
36
+ /**
37
+ * Parse a `set`/`patch` argument: a preset name, inline JSON, `@file`, or (for
38
+ * `set`) the literal `null`. Files keep long inline themes off the shell line,
39
+ * where quoting mangles them.
40
+ */
41
+ export function parseThemeArgument(raw, readFile) {
42
+ if (raw === 'null' || raw === 'default')
43
+ return { kind: 'reset' };
44
+ const text = raw.startsWith('@') ? readFile(raw.slice(1)) : raw;
45
+ const trimmed = text.trim();
46
+ if (!trimmed.startsWith('{')) {
47
+ if (HUD_PRESETS.includes(trimmed)) {
48
+ return { kind: 'preset', name: trimmed };
49
+ }
50
+ const close = suggestClosest(trimmed, HUD_PRESETS);
51
+ throw new CliError(`"${trimmed}" is not a preset name, JSON object, @file, or null. `
52
+ + `Presets: ${HUD_PRESETS.join(', ')}.${close ? ` Closest match: "${close}".` : ''}`);
53
+ }
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(trimmed);
57
+ }
58
+ catch (error) {
59
+ throw new CliError(`Not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
60
+ }
61
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
62
+ throw new CliError('Expected a JSON object (the theme, or a merge patch in the theme\'s shape).');
63
+ }
64
+ return { kind: 'object', value: parsed };
65
+ }
66
+ /** Validation failure formatted the way every issue leaves this command. */
67
+ function rejected(errors) {
68
+ const lines = errors.map(e => ` ${e.path}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ''}`);
69
+ return new CliError(`Theme rejected — nothing was written:\n${lines.join('\n')}`);
70
+ }
71
+ /** `set`: full replace with a preset name, inline object, or null. */
72
+ export function prepareSet(argument, current) {
73
+ if (argument.kind === 'reset') {
74
+ // RESET writes the default tokens INLINE rather than clearing the key.
75
+ // `null` cannot be stored: the GameData schema types worldProfileData.hud.theme
76
+ // as anyOf [string, object] (shared/world-forger .../game-data-schema.ts), so the
77
+ // applier's post-write schema gate rejects it and nothing lands. Deleting the key
78
+ // is not available either — `remove` in both world.json editors filters ARRAY
79
+ // entries by predicate and cannot drop an object key. Writing the default tokens
80
+ // is schema-valid and renders the identical default look; the difference is that
81
+ // the values are now explicit, which also means an agent can read and patch them.
82
+ const tokens = structuredClone(DEFAULT_THEME_TOKENS);
83
+ return {
84
+ value: tokens,
85
+ effective: tokens,
86
+ baseLabel: 'the engine default look (reset)',
87
+ warnings: [],
88
+ };
89
+ }
90
+ if (argument.kind === 'preset') {
91
+ return {
92
+ value: argument.name,
93
+ effective: presetTheme(argument.name),
94
+ baseLabel: `preset ${argument.name}`,
95
+ warnings: [],
96
+ };
97
+ }
98
+ const theme = structuredClone(argument.value);
99
+ const result = validateThemeStrict(theme);
100
+ if (!result.ok) {
101
+ // The one mistake worth a dedicated redirect: a partial object passed to
102
+ // `set` where `patch` was meant.
103
+ const errors = result.errors.map(e => e.message.includes('required')
104
+ ? { ...e, suggestion: `${e.suggestion ? `${e.suggestion} ` : ''}If you meant to change just these fields, use \`bitmagic theme patch\` — set replaces the whole theme.` }
105
+ : e);
106
+ throw rejected(errors);
107
+ }
108
+ void current;
109
+ return { value: theme, effective: theme, baseLabel: 'inline theme', warnings: result.warnings };
110
+ }
111
+ /** `patch`: merge onto the current effective theme, validate the whole result. */
112
+ export function preparePatch(patch, current) {
113
+ const base = structuredClone(current.effective);
114
+ // A stored custom base may carry pre-strict-era leaves the engine already
115
+ // ignores (unknown color tokens, bad per-element leaves). Left in place they
116
+ // would fail validation of the merged result at paths the patch never
117
+ // touched — prune them with a warning instead; they render as nothing today,
118
+ // so removing them changes no pixels. Same behaviour as the hosted tool.
119
+ const prunedWarnings = [];
120
+ if (current.kind === 'custom') {
121
+ const baseCheck = validateThemeStrict(structuredClone(base));
122
+ for (const issue of baseCheck.errors) {
123
+ if (deleteAtPath(base, issue.path)) {
124
+ prunedWarnings.push({
125
+ path: issue.path,
126
+ message: `pre-existing issue in the stored theme (${issue.message}) — the engine was already ignoring this leaf; it was removed so your patch could apply`,
127
+ });
128
+ }
129
+ }
130
+ }
131
+ const merged = applyMergePatch(base, patch);
132
+ const result = validateThemeStrict(merged);
133
+ if (!result.ok)
134
+ throw rejected(result.errors);
135
+ return {
136
+ value: merged,
137
+ effective: merged,
138
+ baseLabel: `${current.label} + patch`,
139
+ warnings: [...prunedWarnings, ...result.warnings],
140
+ };
141
+ }
142
+ /** The report both lanes print — identical text to the hosted tool's. */
143
+ export function themeReport(prepared, before) {
144
+ return buildThemeReport(prepared.effective, {
145
+ before: before.effective,
146
+ baseLabel: prepared.baseLabel,
147
+ });
148
+ }
149
+ /** `check`/`show`: verdict + report for what is stored right now. */
150
+ export function checkStoredTheme(current) {
151
+ if (current.kind === 'unknown-preset') {
152
+ const close = typeof current.stored === 'string'
153
+ ? suggestClosest(current.stored, HUD_PRESETS)
154
+ : undefined;
155
+ return {
156
+ valid: false,
157
+ problems: [
158
+ `hud.theme is "${String(current.stored)}", which is not a preset — the game silently renders the default look.`
159
+ + `${close ? ` Closest match: "${close}".` : ''} Presets: ${HUD_PRESETS.join(', ')}.`,
160
+ ],
161
+ report: buildThemeReport(current.effective, { baseLabel: current.label }),
162
+ };
163
+ }
164
+ if (current.kind === 'custom') {
165
+ const probe = structuredClone(current.effective);
166
+ const result = validateThemeStrict(probe);
167
+ // Only ENGINE-FATAL errors mean the theme will not apply. The strict
168
+ // validator also errors on leaves the engine warn-and-drops (per-element
169
+ // colors/shape/font) or ignores outright (unknown color tokens) — the
170
+ // theme renders fine minus those, and failing `check` over them blocks a
171
+ // working theme in CI and misleads agents into repairing it.
172
+ const fatal = result.errors.filter(isEngineFatalIssue);
173
+ const droppable = result.errors.filter(e => !isEngineFatalIssue(e));
174
+ const format = (e) => `${e.path}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ''}`;
175
+ return {
176
+ valid: fatal.length === 0,
177
+ problems: [
178
+ ...fatal.map(format),
179
+ ...droppable.map(e => `${format(e)} [leaf issue — the engine drops this leaf and still renders the theme]`),
180
+ ],
181
+ report: buildThemeReport(current.effective, { baseLabel: current.label }),
182
+ };
183
+ }
184
+ return {
185
+ valid: true,
186
+ problems: [],
187
+ report: buildThemeReport(current.effective, { baseLabel: current.label }),
188
+ };
189
+ }
190
+ //# sourceMappingURL=theme-io.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-io.js","sourceRoot":"","sources":["../../src/theme/theme-io.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * RFC 7386 JSON merge patch, for HUD theme edits.
3
+ *
4
+ * Why merge patch and not path-based `{path, value}` edits: a merge patch IS
5
+ * the theme's own shape — `{ colors: { primary: "#FF8800" } }` — so the model
6
+ * emits the one notation it already knows from reading themes, instead of
7
+ * inventing dotted path strings it can typo. Deletion is `null` at the key
8
+ * (RFC 7386), which is how "remove the outline" and "clear glowColor" are
9
+ * expressed without a second vocabulary.
10
+ *
11
+ * The patch applies to a base and the MERGED result is validated as a whole
12
+ * theme, so a patch can never sneak an invalid leaf past the checks that a
13
+ * full write would catch.
14
+ */
15
+ export declare function applyMergePatch(base: unknown, patch: unknown): unknown;
16
+ export interface ThemeDiffEntry {
17
+ path: string;
18
+ from: unknown;
19
+ to: unknown;
20
+ }
21
+ /**
22
+ * Leaf-level differences between two themes, as dotted paths. Feeds the
23
+ * report's `changed:` line — the model's confirmation of what its edit
24
+ * actually did (and, on a full replace, of everything it changed without
25
+ * meaning to).
26
+ */
27
+ export declare function diffThemes(before: unknown, after: unknown, prefix?: string): ThemeDiffEntry[];
28
+ /**
29
+ * Delete the leaf at a dotted validator path ("elements.healthBar.colors.background").
30
+ * Returns false when the path does not resolve — the caller then leaves the
31
+ * error in place rather than pretending it fixed something. Companion to the
32
+ * patch flows: a STORED base can carry pre-strict-era leaves the engine already
33
+ * ignores, and pruning them (with a warning) is what lets an unrelated patch
34
+ * apply instead of failing at paths it never touched.
35
+ */
36
+ export declare function deleteAtPath(target: Record<string, unknown>, dottedPath: string): boolean;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * RFC 7386 JSON merge patch, for HUD theme edits.
3
+ *
4
+ * Why merge patch and not path-based `{path, value}` edits: a merge patch IS
5
+ * the theme's own shape — `{ colors: { primary: "#FF8800" } }` — so the model
6
+ * emits the one notation it already knows from reading themes, instead of
7
+ * inventing dotted path strings it can typo. Deletion is `null` at the key
8
+ * (RFC 7386), which is how "remove the outline" and "clear glowColor" are
9
+ * expressed without a second vocabulary.
10
+ *
11
+ * The patch applies to a base and the MERGED result is validated as a whole
12
+ * theme, so a patch can never sneak an invalid leaf past the checks that a
13
+ * full write would catch.
14
+ */
15
+ export function applyMergePatch(base, patch) {
16
+ // Per RFC 7386: a non-object patch replaces the target outright.
17
+ if (typeof patch !== 'object' || patch === null || Array.isArray(patch))
18
+ return patch;
19
+ const target = typeof base === 'object' && base !== null && !Array.isArray(base)
20
+ ? { ...base }
21
+ : {};
22
+ for (const [key, value] of Object.entries(patch)) {
23
+ if (value === null) {
24
+ delete target[key];
25
+ }
26
+ else {
27
+ target[key] = applyMergePatch(target[key], value);
28
+ }
29
+ }
30
+ return target;
31
+ }
32
+ /**
33
+ * Leaf-level differences between two themes, as dotted paths. Feeds the
34
+ * report's `changed:` line — the model's confirmation of what its edit
35
+ * actually did (and, on a full replace, of everything it changed without
36
+ * meaning to).
37
+ */
38
+ export function diffThemes(before, after, prefix = '') {
39
+ if (Object.is(before, after))
40
+ return [];
41
+ const bothObjects = typeof before === 'object' && before !== null && !Array.isArray(before)
42
+ && typeof after === 'object' && after !== null && !Array.isArray(after);
43
+ if (!bothObjects) {
44
+ // Arrays and scalars diff as one leaf; deep-equal arrays are not a change.
45
+ if (JSON.stringify(before) === JSON.stringify(after))
46
+ return [];
47
+ return [{ path: prefix || '(root)', from: before, to: after }];
48
+ }
49
+ const b = before;
50
+ const a = after;
51
+ const out = [];
52
+ for (const key of new Set([...Object.keys(b), ...Object.keys(a)])) {
53
+ const childPrefix = prefix ? `${prefix}.${key}` : key;
54
+ out.push(...diffThemes(b[key], a[key], childPrefix));
55
+ }
56
+ return out;
57
+ }
58
+ /**
59
+ * Delete the leaf at a dotted validator path ("elements.healthBar.colors.background").
60
+ * Returns false when the path does not resolve — the caller then leaves the
61
+ * error in place rather than pretending it fixed something. Companion to the
62
+ * patch flows: a STORED base can carry pre-strict-era leaves the engine already
63
+ * ignores, and pruning them (with a warning) is what lets an unrelated patch
64
+ * apply instead of failing at paths it never touched.
65
+ */
66
+ export function deleteAtPath(target, dottedPath) {
67
+ const segments = dottedPath.split('.');
68
+ let node = target;
69
+ for (const segment of segments.slice(0, -1)) {
70
+ const next = node[segment];
71
+ if (typeof next !== 'object' || next === null || Array.isArray(next))
72
+ return false;
73
+ node = next;
74
+ }
75
+ const leaf = segments[segments.length - 1];
76
+ if (leaf === undefined || !(leaf in node))
77
+ return false;
78
+ delete node[leaf];
79
+ return true;
80
+ }
81
+ //# sourceMappingURL=theme-merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-merge.js","sourceRoot":"","sources":["../../src/theme/theme-merge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The text report that rides every successful theme read/write — the model's
3
+ * eyes. An LLM cannot see the rendered HUD, so the one moment it can learn
4
+ * "your literal value will not render" or "your edit also changed X" is inside
5
+ * the tool response, in text, immediately.
6
+ *
7
+ * Two sections:
8
+ * changed: leaf-level diff of what the write actually did — the confirmation
9
+ * of the intended edit AND of anything a full replace changed
10
+ * without meaning to.
11
+ * contrast: the derived-ink pairings computed with EXACTLY the engine's
12
+ * arithmetic (theme-color-math mirrors engine colorMath, gate-
13
+ * checked), so "SUBSTITUTED -> renders #X" is a promise about the
14
+ * pixels, not an estimate.
15
+ *
16
+ * Deliberately report-only: the engine self-heals every pairing marked
17
+ * SUBSTITUTED, so nothing here blocks a write. The report exists so the agent
18
+ * can decide whether the substitute serves the user's ask — a lime accent that
19
+ * renders as forest green might be exactly right, or reason to pick a colour
20
+ * the ground can carry.
21
+ */
22
+ export interface ThemeReportOptions {
23
+ /** The theme before this write, for the diff section. Omit for read-only reports. */
24
+ before?: unknown;
25
+ /** How the written theme was produced, e.g. `preset rift-raider + patch`. */
26
+ baseLabel: string;
27
+ }
28
+ export declare function buildThemeReport(after: unknown, options: ThemeReportOptions): string;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The text report that rides every successful theme read/write — the model's
3
+ * eyes. An LLM cannot see the rendered HUD, so the one moment it can learn
4
+ * "your literal value will not render" or "your edit also changed X" is inside
5
+ * the tool response, in text, immediately.
6
+ *
7
+ * Two sections:
8
+ * changed: leaf-level diff of what the write actually did — the confirmation
9
+ * of the intended edit AND of anything a full replace changed
10
+ * without meaning to.
11
+ * contrast: the derived-ink pairings computed with EXACTLY the engine's
12
+ * arithmetic (theme-color-math mirrors engine colorMath, gate-
13
+ * checked), so "SUBSTITUTED -> renders #X" is a promise about the
14
+ * pixels, not an estimate.
15
+ *
16
+ * Deliberately report-only: the engine self-heals every pairing marked
17
+ * SUBSTITUTED, so nothing here blocks a write. The report exists so the agent
18
+ * can decide whether the substitute serves the user's ask — a lime accent that
19
+ * renders as forest green might be exactly right, or reason to pick a colour
20
+ * the ground can carry.
21
+ */
22
+ import { accentInkOn, contrastRatio, inkOnSurface, mutedInkOnSurface, readableTextColor, } from './theme-color-math.js';
23
+ import { diffThemes } from './theme-merge.js';
24
+ const HEX = /^#[0-9a-fA-F]{6}$/;
25
+ const MAX_DIFF_LINES = 12;
26
+ function hex(value) {
27
+ return typeof value === 'string' && HEX.test(value) ? value : null;
28
+ }
29
+ function ratio(a, b) {
30
+ return `${(Math.round(contrastRatio(a, b) * 10) / 10).toFixed(1)}:1`;
31
+ }
32
+ function formatValue(v) {
33
+ if (typeof v === 'string')
34
+ return v;
35
+ if (typeof v === 'number' || typeof v === 'boolean')
36
+ return String(v);
37
+ if (v === undefined)
38
+ return '(absent)';
39
+ const json = JSON.stringify(v) ?? '(absent)';
40
+ return json.length > 40 ? `${json.slice(0, 37)}…` : json;
41
+ }
42
+ function formatDiff(entries) {
43
+ if (entries.length === 0)
44
+ return 'changed: nothing — the write produced the same theme';
45
+ const shown = entries.slice(0, MAX_DIFF_LINES)
46
+ .map(e => `${e.path} ${formatValue(e.from)}→${formatValue(e.to)}`);
47
+ const more = entries.length > MAX_DIFF_LINES ? `; +${entries.length - MAX_DIFF_LINES} more` : '';
48
+ return `changed: ${shown.join('; ')}${more}`;
49
+ }
50
+ function pair(label, ink, ground, substituted) {
51
+ const shown = substituted ?? ink;
52
+ const verdict = substituted === null ? 'OK' : `SUBSTITUTED -> renders ${substituted}`;
53
+ return { label, detail: `${ratio(shown, ground)} ${verdict}` };
54
+ }
55
+ function contrastSection(theme) {
56
+ const c = theme.colors ?? {};
57
+ const background = hex(c.background);
58
+ const surface = hex(c.surface);
59
+ const text = hex(c.text);
60
+ const textMuted = hex(c.textMuted);
61
+ const primary = hex(c.primary);
62
+ const danger = hex(c.danger);
63
+ const pairings = [];
64
+ if (text !== null && surface !== null) {
65
+ const derived = inkOnSurface(surface, text);
66
+ pairings.push(pair('text on elements', text, surface, derived === text ? null : derived));
67
+ }
68
+ if (textMuted !== null && surface !== null) {
69
+ const derived = mutedInkOnSurface(surface, textMuted);
70
+ pairings.push(pair('muted text on elements', textMuted, surface, derived === textMuted ? null : derived));
71
+ }
72
+ if (text !== null && background !== null) {
73
+ // The ground pairing has no engine substitute — `text` IS the ground ink —
74
+ // so a low value here is the one the agent must fix by hand.
75
+ const r = contrastRatio(text, background);
76
+ pairings.push({
77
+ label: 'text on panels',
78
+ detail: `${ratio(text, background)} ${r >= 4.5 ? 'OK' : 'LOW — no auto-fix for this pairing; adjust colors.text or colors.background'}`,
79
+ });
80
+ }
81
+ if (primary !== null) {
82
+ // The engine picks the label by luminance threshold, not by best
83
+ // contrast, so mid-luminance fills (a strong orange, a mid blue) can
84
+ // land the label on the LOW side. That is what will render — surface
85
+ // it instead of blessing it. 3:1 is the large-text floor; button
86
+ // labels are display-scale.
87
+ const label = readableTextColor(primary);
88
+ const r = contrastRatio(label, primary);
89
+ pairings.push({
90
+ label: `button label (auto ${label} on primary)`,
91
+ detail: `${ratio(label, primary)} ${r >= 3 ? 'OK' : 'LOW — the auto label lands on the low-contrast side of this fill; nudge colors.primary lighter or darker so the derived label clears'}`,
92
+ });
93
+ }
94
+ if (primary !== null && background !== null) {
95
+ const derived = accentInkOn(background, primary);
96
+ pairings.push(pair('primary as heading/type', primary, background, derived));
97
+ }
98
+ if (danger !== null && background !== null) {
99
+ const derived = accentInkOn(background, danger);
100
+ pairings.push(pair('danger as error text', danger, background, derived));
101
+ }
102
+ if (primary !== null) {
103
+ // Bar fill against its trough. Nothing engine-side heals this pairing, so
104
+ // it is advisory: below 2:1 a fill cannot be told from its own channel.
105
+ const trough = hex(theme.elements?.progressBar?.colors?.background) ?? background;
106
+ if (trough !== null) {
107
+ const r = contrastRatio(primary, trough);
108
+ pairings.push({
109
+ label: 'bar fill vs its track',
110
+ detail: `${ratio(primary, trough)} ${r >= 2 ? 'OK' : 'LOW — the fill will blend into its track; darken the track via elements.progressBar.colors.background'}`,
111
+ });
112
+ }
113
+ }
114
+ const healthTrough = hex(theme.elements?.healthBar?.colors?.background);
115
+ const healthDanger = hex(theme.elements?.healthBar?.colors?.danger) ?? danger;
116
+ if (healthTrough !== null && healthDanger !== null) {
117
+ const r = contrastRatio(healthDanger, healthTrough);
118
+ pairings.push({
119
+ label: 'healthBar critical fill vs its track',
120
+ detail: `${ratio(healthDanger, healthTrough)} ${r >= 2 ? 'OK' : 'LOW'}`,
121
+ });
122
+ }
123
+ if (pairings.length === 0)
124
+ return [];
125
+ const width = Math.max(...pairings.map(p => p.label.length));
126
+ return ['contrast:', ...pairings.map(p => ` ${p.label.padEnd(width)} ${p.detail}`)];
127
+ }
128
+ export function buildThemeReport(after, options) {
129
+ const theme = (typeof after === 'object' && after !== null ? after : {});
130
+ const name = typeof theme.name === 'string' ? theme.name : '(unnamed)';
131
+ const lines = [];
132
+ const diff = options.before === undefined ? null : diffThemes(options.before, after);
133
+ lines.push(`theme: "${name}" (${options.baseLabel}${diff ? `, ${diff.length} change${diff.length === 1 ? '' : 's'}` : ''})`);
134
+ if (diff)
135
+ lines.push(formatDiff(diff));
136
+ lines.push(...contrastSection(theme));
137
+ if (lines.some(l => l.includes('SUBSTITUTED'))) {
138
+ lines.push('SUBSTITUTED = the engine will not render your literal value there; it derives the shown replacement. Patch the colour only if the substitute is wrong for the ask.');
139
+ }
140
+ return lines.join('\n');
141
+ }
142
+ //# sourceMappingURL=theme-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-report.js","sourceRoot":"","sources":["../../src/theme/theme-report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * One resolver for "what does this stored hud.theme value render as" — the
3
+ * logic both lanes need before a patch (the base), a report (the `before`
4
+ * side), or a verdict (`show`/`check`) can exist.
5
+ *
6
+ * It lived twice: the write tool's currentEffectiveTheme and the CLI's
7
+ * readStoredTheme each hand-rolled the stored-value→tokens mapping, and they
8
+ * had already diverged — the CLI copy named an unknown preset truthfully while
9
+ * the agent copy silently folded it into "the engine default". A new stored
10
+ * form (or a label fix) had to land twice with nothing watching. This file is
11
+ * part of the byte-identical kit mirror, so the gate now watches.
12
+ */
13
+ export interface ResolvedStoredTheme {
14
+ kind: 'preset' | 'custom' | 'none' | 'unknown-preset';
15
+ /** The tokens the engine renders for this stored value. */
16
+ tokens: Record<string, unknown>;
17
+ /** Human-readable description of where `tokens` came from. */
18
+ label: string;
19
+ }
20
+ /** Resolve a raw `worldProfileData.hud.theme` value to what the engine renders. */
21
+ export declare function resolveStoredTheme(stored: unknown): ResolvedStoredTheme;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * One resolver for "what does this stored hud.theme value render as" — the
3
+ * logic both lanes need before a patch (the base), a report (the `before`
4
+ * side), or a verdict (`show`/`check`) can exist.
5
+ *
6
+ * It lived twice: the write tool's currentEffectiveTheme and the CLI's
7
+ * readStoredTheme each hand-rolled the stored-value→tokens mapping, and they
8
+ * had already diverged — the CLI copy named an unknown preset truthfully while
9
+ * the agent copy silently folded it into "the engine default". A new stored
10
+ * form (or a label fix) had to land twice with nothing watching. This file is
11
+ * part of the byte-identical kit mirror, so the gate now watches.
12
+ */
13
+ import { DEFAULT_THEME_TOKENS, HUD_PRESETS, presetTheme } from './hud-theme-catalog.js';
14
+ /** Resolve a raw `worldProfileData.hud.theme` value to what the engine renders. */
15
+ export function resolveStoredTheme(stored) {
16
+ if (stored === undefined || stored === null) {
17
+ return {
18
+ kind: 'none',
19
+ tokens: structuredClone(DEFAULT_THEME_TOKENS),
20
+ label: 'the engine default (no theme set)',
21
+ };
22
+ }
23
+ if (typeof stored === 'string') {
24
+ if (HUD_PRESETS.includes(stored)) {
25
+ return {
26
+ kind: 'preset',
27
+ tokens: presetTheme(stored),
28
+ label: `preset ${stored}`,
29
+ };
30
+ }
31
+ return {
32
+ kind: 'unknown-preset',
33
+ tokens: structuredClone(DEFAULT_THEME_TOKENS),
34
+ label: `the engine default (unknown preset "${stored}")`,
35
+ };
36
+ }
37
+ if (typeof stored === 'object' && !Array.isArray(stored)) {
38
+ return {
39
+ kind: 'custom',
40
+ tokens: stored,
41
+ label: 'the current custom theme',
42
+ };
43
+ }
44
+ return {
45
+ kind: 'unknown-preset',
46
+ tokens: structuredClone(DEFAULT_THEME_TOKENS),
47
+ label: `the engine default (hud.theme has unexpected type ${typeof stored})`,
48
+ };
49
+ }
50
+ //# sourceMappingURL=theme-stored.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-stored.js","sourceRoot":"","sources":["../../src/theme/theme-stored.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,WAAW,EAAsB,MAAM,wBAAwB,CAAC;AAU5G,mFAAmF;AACnF,MAAM,UAAU,kBAAkB,CAAC,MAAe;IAC9C,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QAC1C,OAAO;YACH,IAAI,EAAE,MAAM;YACZ,MAAM,EAAE,eAAe,CAAC,oBAAoB,CAA4B;YACxE,KAAK,EAAE,mCAAmC;SAC7C,CAAC;IACN,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC7B,IAAK,WAAqC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1D,OAAO;gBACH,IAAI,EAAE,QAAQ;gBACd,MAAM,EAAE,WAAW,CAAC,MAAuB,CAAC;gBAC5C,KAAK,EAAE,UAAU,MAAM,EAAE;aAC5B,CAAC;QACN,CAAC;QACD,OAAO;YACH,IAAI,EAAE,gBAAgB;YACtB,MAAM,EAAE,eAAe,CAAC,oBAAoB,CAA4B;YACxE,KAAK,EAAE,uCAAuC,MAAM,IAAI;SAC3D,CAAC;IACN,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACvD,OAAO;YACH,IAAI,EAAE,QAAQ;YACd,MAAM,EAAE,MAAiC;YACzC,KAAK,EAAE,0BAA0B;SACpC,CAAC;IACN,CAAC;IACD,OAAO;QACH,IAAI,EAAE,gBAAgB;QACtB,MAAM,EAAE,eAAe,CAAC,oBAAoB,CAA4B;QACxE,KAAK,EAAE,qDAAqD,OAAO,MAAM,GAAG;KAC/E,CAAC;AACN,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The strict write-time HUD theme validator.
3
+ *
4
+ * STRICTER than the engine's load-time validator on purpose, and that asymmetry
5
+ * is the design: at WRITE time a bad per-element leaf is an error the agent can
6
+ * fix in the same turn, while at LOAD time the engine must keep rendering
7
+ * already-published themes, so it warns and drops the leaf instead. Everything
8
+ * this validator accepts, the engine accepts; the reverse is not required.
9
+ *
10
+ * This replaced `lightValidateTheme`, which skipped glow keys, every numeric
11
+ * range, the whole `elements` block, decoration slot suitability and decoration
12
+ * size/position/repeat — six ways a theme could pass the write tool and then
13
+ * silently render as the DEFAULT look at engine load, which reads as "theming
14
+ * is broken" rather than "the value was wrong". Parity with the engine is
15
+ * enforced by scripts/check-hud-catalog.ts, which runs both validators over a
16
+ * fixture corpus under tsx and compares verdicts.
17
+ *
18
+ * Validation rules mirror game/src/engine/hud/validateTheme.ts; vocabulary
19
+ * comes from the drift-checked catalog next door.
20
+ */
21
+ import { type CatalogValidationIssue } from './hud-theme-catalog.js';
22
+ export interface StrictValidationResult {
23
+ ok: boolean;
24
+ errors: CatalogValidationIssue[];
25
+ warnings: CatalogValidationIssue[];
26
+ }
27
+ export declare function suggestClosest(value: string, candidates: ReadonlyArray<string>): string | undefined;
28
+ /**
29
+ * Whether an issue would make the ENGINE reject the whole theme, or only drop a
30
+ * leaf while the theme keeps rendering.
31
+ *
32
+ * This validator is deliberately stricter than the engine at two spots, and the
33
+ * difference matters when DESCRIBING a stored theme: per-element
34
+ * font/colors/shape leaves are warn-and-dropped at engine load (the theme still
35
+ * renders minus the leaf), and unknown top-level color tokens are ignored
36
+ * outright. Telling a user "the engine is falling back to the default look"
37
+ * over one droppable leaf describes a fallback that is not happening — and an
38
+ * agent acting on that "repairs" a theme the user is actually seeing.
39
+ */
40
+ export declare function isEngineFatalIssue(issue: CatalogValidationIssue): boolean;
41
+ export declare function validateThemeStrict(theme: unknown): StrictValidationResult;