Skip to content

Commit 7e1a031

Browse files
committed
UX#8: hex color ratchet — lock baseline, migration starts with DimensionsTab
Adds tests/HexColorRatchet.test.ts that ratchets per-file hex-literal counts against tests/fixtures/hex_color_baseline.json. Two guarantees: 1) New files cannot introduce hex literals — must use theme tokens (T.surface, T.text, T.success, T.danger, etc. from @/theme). 2) Existing files cannot regress past their current count — the baseline only goes down. To intentionally add hex, the migrator must edit the baseline JSON, which is visible in code review. Current baseline: 592 hex literals across 45 component files. The test passes today; future PRs will fail if they bloat any file or add a new offender. DimensionsTab.tsx migrated as a proof of pattern: every "#6b7280" replaced with T.textSecondary, baseline count down from 18 to 12. This is a ratchet, not a mass rewrite. Migration continues file by file in future commits; the test guarantees the tide only goes out.
1 parent 5a4e314 commit 7e1a031

3 files changed

Lines changed: 163 additions & 6 deletions

File tree

vbgui/src/components/sidebar/DimensionsTab.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { useState } from "react";
1010
import { DimEnvEditor } from "@/components/DimEnvEditor";
11+
import { T } from "@/theme";
1112

1213
export interface InferenceEntryClient {
1314
brick: string;
@@ -56,14 +57,14 @@ export function DimensionsTab({
5657

5758
<header style={{ marginBottom: 8 }}>
5859
<h4 style={{ margin: 0, fontSize: 13 }}>Inferred Dimensions</h4>
59-
<div style={{ color: "#6b7280", marginTop: 2 }}>
60+
<div style={{ color: T.textSecondary, marginTop: 2 }}>
6061
{visible.length} of {log.length} entries
6162
</div>
6263
</header>
6364

6465
<div style={{ display: "flex", gap: 6, marginBottom: 8 }}>
6566
<label style={{ display: "flex", flexDirection: "column" }}>
66-
<span style={{ color: "#6b7280", fontSize: 11 }}>Source</span>
67+
<span style={{ color: T.textSecondary, fontSize: 11 }}>Source</span>
6768
<select data-testid="dimensions-filter-source"
6869
value={filterSource}
6970
onChange={(e) =>
@@ -74,7 +75,7 @@ export function DimensionsTab({
7475
</select>
7576
</label>
7677
<label style={{ display: "flex", flexDirection: "column", flex: 1 }}>
77-
<span style={{ color: "#6b7280", fontSize: 11 }}>Brick</span>
78+
<span style={{ color: T.textSecondary, fontSize: 11 }}>Brick</span>
7879
<input data-testid="dimensions-filter-brick"
7980
placeholder="filter…"
8081
value={filterBrick}
@@ -115,7 +116,7 @@ export function DimensionsTab({
115116
fontSize: 10, textTransform: "uppercase",
116117
}}>{e.source}</span>
117118
</td>
118-
<td style={{ ...td, color: "#6b7280", fontSize: 11 }}>
119+
<td style={{ ...td, color: T.textSecondary, fontSize: 11 }}>
119120
{e.reason}
120121
</td>
121122
<td style={td}>
@@ -164,7 +165,7 @@ export function DimensionsTab({
164165
? "#1e40af" : "#111827" }}>
165166
<strong>{e.brick}.{e.param}</strong> ={" "}
166167
<code>{String(e.value)}</code>{" "}
167-
<em style={{ color: "#6b7280" }}>
168+
<em style={{ color: T.textSecondary }}>
168169
({e.source}; {e.reason})
169170
</em>
170171
</li>
@@ -177,7 +178,7 @@ export function DimensionsTab({
177178

178179
const th: React.CSSProperties = {
179180
textAlign: "left", padding: "4px 6px",
180-
color: "#6b7280", fontSize: 11, fontWeight: 600,
181+
color: T.textSecondary, fontSize: 11, fontWeight: 600,
181182
borderBottom: "1px solid #e5e7eb",
182183
};
183184
const td: React.CSSProperties = { padding: "4px 6px" };
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// UX#8 — ratchet test for hex colour literals in vbgui components.
2+
//
3+
// 518 hex literals across 45 component files (commit 5a4e314 baseline).
4+
// Replacing all in one go would be a giant risky diff, so this test
5+
// runs as a ratchet: every file has a max-allowed hex count locked in
6+
// tests/fixtures/hex_color_baseline.json. New code is forbidden from:
7+
//
8+
// - Exceeding the baseline count for an existing file (ratchet down).
9+
// - Adding a NEW file with hex literals not present in the baseline.
10+
//
11+
// Migration path is to replace `#xxxxxx` literals with `T.surface`,
12+
// `T.text`, `T.success`, etc. from `@/theme`, then drop the file's
13+
// baseline number. Drop the entry entirely when count hits 0.
14+
15+
import { describe, it, expect } from "vitest";
16+
import { readFileSync, readdirSync, statSync } from "node:fs";
17+
import { dirname, join, resolve } from "node:path";
18+
import { fileURLToPath } from "node:url";
19+
import baseline from "./fixtures/hex_color_baseline.json";
20+
21+
const HERE = dirname(fileURLToPath(import.meta.url));
22+
const COMPONENTS_DIR = resolve(HERE, "..", "src", "components");
23+
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
24+
const IGNORE_FILES = new Set(["theme.ts"]);
25+
26+
function walk(dir: string, rel = ""): string[] {
27+
const out: string[] = [];
28+
for (const entry of readdirSync(dir)) {
29+
if (IGNORE_FILES.has(entry)) continue;
30+
const full = join(dir, entry);
31+
const relPath = rel ? `${rel}/${entry}` : entry;
32+
if (statSync(full).isDirectory()) {
33+
out.push(...walk(full, relPath));
34+
} else if (entry.endsWith(".ts") || entry.endsWith(".tsx")) {
35+
out.push(`components/${relPath}`);
36+
}
37+
}
38+
return out;
39+
}
40+
41+
function countHex(absPath: string): number {
42+
const src = readFileSync(absPath, "utf-8");
43+
// Ignore line + block comments so docs/JSDoc with example colours
44+
// don't dominate the count.
45+
const stripped = src
46+
.replace(/\/\*[\s\S]*?\*\//g, "")
47+
.replace(/(^|[^:])\/\/[^\n]*/g, (_m, p1) => p1);
48+
return (stripped.match(HEX_RE) ?? []).length;
49+
}
50+
51+
const baselineMap = baseline as Record<string, number>;
52+
53+
describe("UX#8: hex color ratchet (only counts can decrease)", () => {
54+
const files = walk(COMPONENTS_DIR);
55+
56+
it("does not introduce hex colors in new files", () => {
57+
const offenders: string[] = [];
58+
for (const f of files) {
59+
if (f in baselineMap) continue;
60+
const abs = join(COMPONENTS_DIR, f.replace(/^components\//, ""));
61+
const n = countHex(abs);
62+
if (n > 0) {
63+
offenders.push(`${f}: ${n} hex literal${n === 1 ? "" : "s"}`);
64+
}
65+
}
66+
if (offenders.length > 0) {
67+
throw new Error(
68+
"UX#8: new files contain hex literals — use theme tokens " +
69+
"from @/theme (T.surface, T.text, T.success, etc.) instead. " +
70+
"If you must add a hex literal, bump the baseline at " +
71+
"tests/fixtures/hex_color_baseline.json:\n " +
72+
offenders.join("\n "),
73+
);
74+
}
75+
});
76+
77+
it("does not exceed per-file baseline hex counts", () => {
78+
const offenders: string[] = [];
79+
for (const [rel, limit] of Object.entries(baselineMap)) {
80+
const abs = join(COMPONENTS_DIR, rel.replace(/^components\//, ""));
81+
try {
82+
const n = countHex(abs);
83+
if (n > limit) {
84+
offenders.push(`${rel}: ${n} > baseline ${limit}`);
85+
}
86+
} catch {
87+
// File deleted — that's fine, baseline becomes stale but
88+
// doesn't fail; CI can drop the entry on next commit.
89+
}
90+
}
91+
if (offenders.length > 0) {
92+
throw new Error(
93+
"UX#8: hex literal counts exceed baseline — please replace " +
94+
"with theme tokens (T.surface/T.text/etc) from @/theme, or " +
95+
"if the addition is intentional, update the baseline at " +
96+
"tests/fixtures/hex_color_baseline.json:\n " +
97+
offenders.join("\n "),
98+
);
99+
}
100+
});
101+
102+
it("baseline file is non-empty and tracks > 0 violations " +
103+
"(starting reality, not aspiration)", () => {
104+
const total = Object.values(baselineMap)
105+
.reduce((a, b) => a + b, 0);
106+
expect(Object.keys(baselineMap).length).toBeGreaterThan(0);
107+
expect(total).toBeGreaterThan(0);
108+
});
109+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"components/AppTabs.tsx": 4,
3+
"components/AutoGroupButton.tsx": 13,
4+
"components/BottomStrip.tsx": 7,
5+
"components/BrickContextPanel.tsx": 32,
6+
"components/BrickNode.tsx": 1,
7+
"components/CompileTracePanel.tsx": 20,
8+
"components/DataInspector.tsx": 47,
9+
"components/DimEnvEditor.tsx": 2,
10+
"components/ErrorDetailsPanel.tsx": 10,
11+
"components/ExplainModal.tsx": 1,
12+
"components/FlowCanvas.tsx": 24,
13+
"components/GalleryScaleDownSlider.tsx": 9,
14+
"components/GalleryTab.tsx": 5,
15+
"components/GenerationPanel.tsx": 10,
16+
"components/GradAttnPanel.tsx": 5,
17+
"components/HFQuickStartModal.tsx": 6,
18+
"components/HelpIcon.tsx": 30,
19+
"components/InsertIntoEdgeBar.tsx": 2,
20+
"components/LLMGalleryWizardModal.tsx": 24,
21+
"components/LiveGenPanel.tsx": 9,
22+
"components/LossChart.tsx": 8,
23+
"components/LossGhostNode.tsx": 2,
24+
"components/LossSurfaceModal.tsx": 3,
25+
"components/MemoryBar.tsx": 9,
26+
"components/ParallelComposeBar.tsx": 2,
27+
"components/RunResultModal.tsx": 15,
28+
"components/ScheduleEditor.tsx": 14,
29+
"components/Sidebar.tsx": 5,
30+
"components/SweepPanel.tsx": 6,
31+
"components/TokenizerMatrixTab.tsx": 16,
32+
"components/TokenizerPlayground.tsx": 28,
33+
"components/Tooltip.tsx": 4,
34+
"components/TopBar.tsx": 27,
35+
"components/TrainExtrasOverlay.tsx": 40,
36+
"components/TrainLiveControls.tsx": 46,
37+
"components/TransplantBar.tsx": 2,
38+
"components/diagrams/TensorDiagram.tsx": 20,
39+
"components/sidebar/AblationsTab.tsx": 19,
40+
"components/sidebar/DimensionsTab.tsx": 12,
41+
"components/sidebar/GotchasTab.tsx": 17,
42+
"components/sidebar/MemoryMatrixTab.tsx": 11,
43+
"components/sidebar/OptimTab.tsx": 2,
44+
"components/sidebar/RewritersTab.tsx": 1,
45+
"components/sidebar/ShardingTab.tsx": 6,
46+
"components/sidebar/SideChannelsTab.tsx": 16
47+
}

0 commit comments

Comments
 (0)