@bitmagic/cli 0.1.59-dev.1 → 0.1.59-dev.3
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/cli.d.ts +1 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/theme.d.ts +3 -0
- package/dist/commands/theme.js +201 -0
- package/dist/commands/theme.js.map +1 -0
- package/dist/scaffold/project-files.d.ts +1 -0
- package/dist/scaffold/project-files.js +47 -1
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/scaffold/project.js +2 -1
- package/dist/scaffold/project.js.map +1 -1
- package/dist/theme/hud-theme-catalog.d.ts +236 -0
- package/dist/theme/hud-theme-catalog.js +386 -0
- package/dist/theme/hud-theme-catalog.js.map +1 -0
- package/dist/theme/theme-color-math.d.ts +21 -0
- package/dist/theme/theme-color-math.js +99 -0
- package/dist/theme/theme-color-math.js.map +1 -0
- package/dist/theme/theme-io.d.ts +49 -0
- package/dist/theme/theme-io.js +190 -0
- package/dist/theme/theme-io.js.map +1 -0
- package/dist/theme/theme-merge.d.ts +36 -0
- package/dist/theme/theme-merge.js +81 -0
- package/dist/theme/theme-merge.js.map +1 -0
- package/dist/theme/theme-report.d.ts +28 -0
- package/dist/theme/theme-report.js +142 -0
- package/dist/theme/theme-report.js.map +1 -0
- package/dist/theme/theme-stored.d.ts +21 -0
- package/dist/theme/theme-stored.js +50 -0
- package/dist/theme/theme-stored.js.map +1 -0
- package/dist/theme/theme-validate.d.ts +41 -0
- package/dist/theme/theme-validate.js +406 -0
- package/dist/theme/theme-validate.js.map +1 -0
- package/dist/verify/classify.js +19 -0
- package/dist/verify/classify.js.map +1 -1
- package/dist/verify/hud-theme.d.ts +55 -0
- package/dist/verify/hud-theme.js +70 -0
- package/dist/verify/hud-theme.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-mirror of game/src/engine/hud/colorMath.ts — the WCAG arithmetic behind
|
|
3
|
+
* the derived HUD inks. The theme report computes its contrast pairings with
|
|
4
|
+
* EXACTLY the maths the engine will apply, so a SUBSTITUTED line in the report
|
|
5
|
+
* is a promise about what will render, not an estimate.
|
|
6
|
+
*
|
|
7
|
+
* Mirrored by hand because agent tools cannot import engine source (separate
|
|
8
|
+
* tsconfig include sets). Drift-checked BEHAVIOURALLY by scripts/
|
|
9
|
+
* check-hud-catalog.ts: the gate imports both copies under tsx and compares
|
|
10
|
+
* their outputs over every preset palette plus adversarial pairs, so a changed
|
|
11
|
+
* threshold or mix factor on either side goes red in CI.
|
|
12
|
+
*/
|
|
13
|
+
export declare function relLuminance(hex: string): number;
|
|
14
|
+
export declare function contrastRatio(a: string, b: string): number;
|
|
15
|
+
export declare function mixHex(from: string, to: string, amount: number): string;
|
|
16
|
+
export declare function readableTextColor(hex: string): string;
|
|
17
|
+
export declare const ON_SURFACE_MIN_CONTRAST = 3;
|
|
18
|
+
export declare function inkOnSurface(surface: string, preferred: string): string;
|
|
19
|
+
export declare const ACCENT_INK_MIN_CONTRAST = 4.5;
|
|
20
|
+
export declare function accentInkOn(ground: string, accent: string): string | null;
|
|
21
|
+
export declare function mutedInkOnSurface(surface: string, preferred: string): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-mirror of game/src/engine/hud/colorMath.ts — the WCAG arithmetic behind
|
|
3
|
+
* the derived HUD inks. The theme report computes its contrast pairings with
|
|
4
|
+
* EXACTLY the maths the engine will apply, so a SUBSTITUTED line in the report
|
|
5
|
+
* is a promise about what will render, not an estimate.
|
|
6
|
+
*
|
|
7
|
+
* Mirrored by hand because agent tools cannot import engine source (separate
|
|
8
|
+
* tsconfig include sets). Drift-checked BEHAVIOURALLY by scripts/
|
|
9
|
+
* check-hud-catalog.ts: the gate imports both copies under tsx and compares
|
|
10
|
+
* their outputs over every preset palette plus adversarial pairs, so a changed
|
|
11
|
+
* threshold or mix factor on either side goes red in CI.
|
|
12
|
+
*/
|
|
13
|
+
export function relLuminance(hex) {
|
|
14
|
+
const lin = (v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));
|
|
15
|
+
return 0.2126 * lin(parseInt(hex.slice(1, 3), 16) / 255)
|
|
16
|
+
+ 0.7152 * lin(parseInt(hex.slice(3, 5), 16) / 255)
|
|
17
|
+
+ 0.0722 * lin(parseInt(hex.slice(5, 7), 16) / 255);
|
|
18
|
+
}
|
|
19
|
+
export function contrastRatio(a, b) {
|
|
20
|
+
const la = relLuminance(a);
|
|
21
|
+
const lb = relLuminance(b);
|
|
22
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
23
|
+
}
|
|
24
|
+
export function mixHex(from, to, amount) {
|
|
25
|
+
const channel = (i) => {
|
|
26
|
+
const f = parseInt(from.slice(i, i + 2), 16);
|
|
27
|
+
const t = parseInt(to.slice(i, i + 2), 16);
|
|
28
|
+
return Math.round(f * amount + t * (1 - amount)).toString(16).padStart(2, '0');
|
|
29
|
+
};
|
|
30
|
+
return `#${channel(1)}${channel(3)}${channel(5)}`;
|
|
31
|
+
}
|
|
32
|
+
export function readableTextColor(hex) {
|
|
33
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(hex))
|
|
34
|
+
return '#ffffff';
|
|
35
|
+
return relLuminance(hex) > 0.5 ? '#000000' : '#ffffff';
|
|
36
|
+
}
|
|
37
|
+
// The ink for anything painted ON `surface`, plus its muted companion.
|
|
38
|
+
//
|
|
39
|
+
// Same species as --hud-color-on-primary: the engine picks the readable pair so a
|
|
40
|
+
// theme author does not have to hand-author contrast per element. It exists
|
|
41
|
+
// because `text` carries TWO jobs that only agree on a uniformly dark theme. It is
|
|
42
|
+
// the ink on the `background` canvas — a game's own dialogs paint themselves with
|
|
43
|
+
// that pair — AND the ink on every counter, timer, toast, bubble and panel filled
|
|
44
|
+
// with `surface`. A style whose big UI is a light canvas and whose HUD furniture is
|
|
45
|
+
// darker cannot satisfy both from one value: with a single ink the furniture caps
|
|
46
|
+
// out around #9E9276, which is 1.4:1 against a sunny world and so not furniture at
|
|
47
|
+
// all. Village Keep spent three rounds oscillating between a washed-out HUD and a
|
|
48
|
+
// uniformly dark one for exactly this reason.
|
|
49
|
+
//
|
|
50
|
+
// It PREFERS the theme's own ink, because a tinted parchment or a cold cyan is part
|
|
51
|
+
// of a style's identity and must survive. It substitutes only where the declared
|
|
52
|
+
// pairing is unusable — below 3:1, the WCAG floor for large text, and far below
|
|
53
|
+
// every pairing in the catalog: the presets measure 9.6 to 17.8 for `text` and 5.7
|
|
54
|
+
// to 9.3 for `textMuted`, so this cannot fire on a theme that was already coherent.
|
|
55
|
+
export const ON_SURFACE_MIN_CONTRAST = 3;
|
|
56
|
+
export function inkOnSurface(surface, preferred) {
|
|
57
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(surface) || !/^#[0-9a-fA-F]{6}$/.test(preferred))
|
|
58
|
+
return preferred;
|
|
59
|
+
if (contrastRatio(preferred, surface) >= ON_SURFACE_MIN_CONTRAST)
|
|
60
|
+
return preferred;
|
|
61
|
+
return readableTextColor(surface);
|
|
62
|
+
}
|
|
63
|
+
// Target 4.5:1 rather than the 3:1 the on-surface inks use: this one paints
|
|
64
|
+
// headings and status lines, which are body-scale on some surfaces, and the
|
|
65
|
+
// margin is free — the lowest any shipped preset measures is 6.02:1, so raising
|
|
66
|
+
// the bar cannot make the token fire on a theme that was already fine.
|
|
67
|
+
export const ACCENT_INK_MIN_CONTRAST = 4.5;
|
|
68
|
+
export function accentInkOn(ground, accent) {
|
|
69
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(ground) || !/^#[0-9a-fA-F]{6}$/.test(accent))
|
|
70
|
+
return null;
|
|
71
|
+
if (contrastRatio(accent, ground) >= ACCENT_INK_MIN_CONTRAST)
|
|
72
|
+
return null;
|
|
73
|
+
// Walk the accent toward the ground's opposite pole and stop at the FIRST value
|
|
74
|
+
// that clears, so the result keeps as much of the accent as the ground allows.
|
|
75
|
+
// Mixing toward black or white preserves the HUE, which is the part that makes
|
|
76
|
+
// an accent an accent: village-keep's candy lime lands on a forest green rather
|
|
77
|
+
// than on brown, and a forest green on parchment is what the reference draws.
|
|
78
|
+
const pole = relLuminance(ground) > 0.5 ? '#000000' : '#ffffff';
|
|
79
|
+
for (let keep = 0.95; keep > 0.05; keep -= 0.05) {
|
|
80
|
+
const candidate = mixHex(accent, pole, keep);
|
|
81
|
+
if (contrastRatio(candidate, ground) >= ACCENT_INK_MIN_CONTRAST)
|
|
82
|
+
return candidate;
|
|
83
|
+
}
|
|
84
|
+
return readableTextColor(ground);
|
|
85
|
+
}
|
|
86
|
+
export function mutedInkOnSurface(surface, preferred) {
|
|
87
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(surface) || !/^#[0-9a-fA-F]{6}$/.test(preferred))
|
|
88
|
+
return preferred;
|
|
89
|
+
if (contrastRatio(preferred, surface) >= ON_SURFACE_MIN_CONTRAST)
|
|
90
|
+
return preferred;
|
|
91
|
+
// A secondary label steps BACK toward its own backdrop rather than taking the
|
|
92
|
+
// full-strength ink — otherwise the substitution would make muted text the
|
|
93
|
+
// loudest thing on the element it was meant to sit quietly on. 0.78 is the
|
|
94
|
+
// point where it still clears AA on the lightest surface that can reach this
|
|
95
|
+
// branch at all (a mid-brown at 4.65:1) while reading as clearly quieter than
|
|
96
|
+
// the full ink beside it.
|
|
97
|
+
return mixHex(readableTextColor(surface), surface, 0.78);
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=theme-color-math.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-color-math.js","sourceRoot":"","sources":["../../src/theme/theme-color-math.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { JsonObject } from '../project/world-json.js';
|
|
2
|
+
export interface StoredTheme {
|
|
3
|
+
kind: 'preset' | 'custom' | 'none' | 'unknown-preset';
|
|
4
|
+
stored: unknown;
|
|
5
|
+
/** The tokens the engine renders for this stored value. */
|
|
6
|
+
effective: Record<string, unknown>;
|
|
7
|
+
/** Human-readable description of where `effective` came from. */
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
/** worldProfileData.hud.theme — the ONLY path the engine reads a theme from. */
|
|
11
|
+
export declare function readStoredTheme(world: JsonObject): StoredTheme;
|
|
12
|
+
/**
|
|
13
|
+
* Parse a `set`/`patch` argument: a preset name, inline JSON, `@file`, or (for
|
|
14
|
+
* `set`) the literal `null`. Files keep long inline themes off the shell line,
|
|
15
|
+
* where quoting mangles them.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseThemeArgument(raw: string, readFile: (path: string) => string): {
|
|
18
|
+
kind: 'preset';
|
|
19
|
+
name: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'reset';
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'object';
|
|
24
|
+
value: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
export interface PreparedWrite {
|
|
27
|
+
/** What to store at worldProfileData.hud.theme. */
|
|
28
|
+
value: unknown;
|
|
29
|
+
/** The tokens that value renders as (for the report). */
|
|
30
|
+
effective: Record<string, unknown>;
|
|
31
|
+
baseLabel: string;
|
|
32
|
+
warnings: Array<{
|
|
33
|
+
path: string;
|
|
34
|
+
message: string;
|
|
35
|
+
suggestion?: string;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
/** `set`: full replace with a preset name, inline object, or null. */
|
|
39
|
+
export declare function prepareSet(argument: ReturnType<typeof parseThemeArgument>, current: StoredTheme): PreparedWrite;
|
|
40
|
+
/** `patch`: merge onto the current effective theme, validate the whole result. */
|
|
41
|
+
export declare function preparePatch(patch: Record<string, unknown>, current: StoredTheme): PreparedWrite;
|
|
42
|
+
/** The report both lanes print — identical text to the hosted tool's. */
|
|
43
|
+
export declare function themeReport(prepared: PreparedWrite, before: StoredTheme): string;
|
|
44
|
+
/** `check`/`show`: verdict + report for what is stored right now. */
|
|
45
|
+
export declare function checkStoredTheme(current: StoredTheme): {
|
|
46
|
+
valid: boolean;
|
|
47
|
+
problems: string[];
|
|
48
|
+
report: string;
|
|
49
|
+
};
|
|
@@ -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;
|