Skip to content

Commit b840f1e

Browse files
committed
feat(v7-f56b): DimEnvEditor proposes single-knob snap fixes inline
Per the architect's request: when the editor surfaces a mismatch, it should not just warn — it should suggest a valid replacement and let one click accept the fix, rather than leaving the user to mentally solve nh*head_dim=H. vbgui/src/components/DimEnvEditor.tsx: - Two new buttons appear next to the inline mismatch indicator: * 'Snap H → N' (data-testid='dim-env-fix-set-H') — sets H to nh*head_dim. Always available when there's a mismatch. * 'Snap head_dim → N' (data-testid='dim-env-fix-set-head_dim') — sets head_dim to H/nh, but ONLY when H is cleanly divisible by nh (otherwise the snap would itself be fractional). - Apply button now turns amber when there's a mismatch (color + border + title) so the architect notices before committing. - applyDraft() factored out so both Apply and Snap-* buttons share one path that reflects the override back into the visible draft. The decoupled-Q convention (H ≠ nh*head_dim with internal projection) is still accepted — the architect can ignore the snap and press Apply consciously; the warning persists as a yellow banner. vbgui/tests/DimEnvEditor.test.tsx — 3 new cases (8 total): - Snap H → forwards 150 when nh=3, head_dim=50. - Snap head_dim → shows "16" when H=128, nh=8. - Snap head_dim hidden when H % nh != 0 (only Snap H shown). 15/15 vitest (DimEnvEditor + App.integration). F56b Playwright e2e unaffected — uses the Apply path which still works the same way. Ref: cppmega-mlx-j3qa.2
1 parent dd77ca2 commit b840f1e

2 files changed

Lines changed: 93 additions & 12 deletions

File tree

vbgui/src/components/DimEnvEditor.tsx

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,45 @@ export function DimEnvEditor({ value, onApply }: DimEnvEditorProps): JSX.Element
2020
return out;
2121
});
2222

23-
const mismatch = (() => {
23+
const parsed = (() => {
2424
const H = Number(draft.H);
2525
const nh = Number(draft.nh);
2626
const hd = Number(draft.head_dim);
2727
if (!Number.isFinite(H) || !Number.isFinite(nh) ||
2828
!Number.isFinite(hd)) return null;
29-
if (nh * hd === H) return null;
30-
return `nh*head_dim = ${nh * hd} ≠ H = ${H}`;
29+
return { H, nh, hd };
3130
})();
31+
const mismatch = parsed && parsed.nh * parsed.hd !== parsed.H
32+
? `nh*head_dim = ${parsed.nh * parsed.hd} ≠ H = ${parsed.H}`
33+
: null;
34+
// Two single-knob fixes that snap to consistency: change H to
35+
// nh*head_dim, or change head_dim to H/nh when H is cleanly
36+
// divisible by nh. The architect can also accept the mismatch
37+
// consciously (the codebase supports decoupled Q via projection).
38+
const fixSetH = mismatch && parsed
39+
? { H: parsed.nh * parsed.hd } : null;
40+
const fixSetHeadDim = mismatch && parsed && parsed.nh > 0
41+
&& parsed.H % parsed.nh === 0
42+
? { head_dim: parsed.H / parsed.nh } : null;
43+
44+
function applyDraft(overrides: Record<string, number> = {}) {
45+
const next: Record<string, number> = { ...value };
46+
for (const k of EDITABLE_KEYS) {
47+
const n = Number(draft[k]);
48+
if (Number.isFinite(n)) next[k] = n;
49+
}
50+
Object.assign(next, overrides);
51+
// Reflect the override back into the visible draft so the user
52+
// sees the suggestion they accepted.
53+
if (Object.keys(overrides).length > 0) {
54+
setDraft({
55+
...draft,
56+
...Object.fromEntries(
57+
Object.entries(overrides).map(([k, v]) => [k, String(v)])),
58+
});
59+
}
60+
onApply(next);
61+
}
3262

3363
return (
3464
<div data-testid="dim-env-editor"
@@ -51,15 +81,13 @@ export function DimEnvEditor({ value, onApply }: DimEnvEditorProps): JSX.Element
5181
))}
5282
<button
5383
data-testid="dim-env-apply"
54-
onClick={() => {
55-
const next: Record<string, number> = { ...value };
56-
for (const k of EDITABLE_KEYS) {
57-
const n = Number(draft[k]);
58-
if (Number.isFinite(n)) next[k] = n;
59-
}
60-
onApply(next);
61-
}}
62-
style={{ padding: "2px 8px" }}
84+
onClick={() => applyDraft()}
85+
style={{ padding: "2px 8px",
86+
background: mismatch ? "#fef3c7" : undefined,
87+
borderColor: mismatch ? "#d97706" : undefined }}
88+
title={mismatch
89+
? "dim_env is inconsistent — see the suggestions on the right"
90+
: "Push dim_env to verify"}
6391
>
6492
Apply
6593
</button>
@@ -69,6 +97,26 @@ export function DimEnvEditor({ value, onApply }: DimEnvEditorProps): JSX.Element
6997
{mismatch}
7098
</span>
7199
)}
100+
{fixSetH && (
101+
<button data-testid="dim-env-fix-set-H"
102+
onClick={() => applyDraft(fixSetH)}
103+
title={`Snap H to ${fixSetH.H} (= nh*head_dim)`}
104+
style={{ padding: "2px 8px", background: "#dbeafe",
105+
border: "1px solid #2563eb",
106+
color: "#1e40af" }}>
107+
Snap H → {fixSetH.H}
108+
</button>
109+
)}
110+
{fixSetHeadDim && (
111+
<button data-testid="dim-env-fix-set-head_dim"
112+
onClick={() => applyDraft(fixSetHeadDim)}
113+
title={`Snap head_dim to ${fixSetHeadDim.head_dim} (= H/nh)`}
114+
style={{ padding: "2px 8px", background: "#dbeafe",
115+
border: "1px solid #2563eb",
116+
color: "#1e40af" }}>
117+
Snap head_dim → {fixSetHeadDim.head_dim}
118+
</button>
119+
)}
72120
</div>
73121
);
74122
}

vbgui/tests/DimEnvEditor.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,37 @@ describe("V7-F56b/F53 DimEnvEditor", () => {
6262
const warn = screen.getByTestId("dim-env-inline-mismatch");
6363
expect(warn.textContent).toContain("192");
6464
});
65+
66+
it("offers Snap H → fix that applies the consistent value", () => {
67+
const onApply = vi.fn();
68+
render(<DimEnvEditor
69+
value={{ H: 128, nh: 3, head_dim: 50, B: 1, S: 8 }}
70+
onApply={onApply}
71+
/>);
72+
const snap = screen.getByTestId("dim-env-fix-set-H");
73+
expect(snap.textContent).toContain("150");
74+
fireEvent.click(snap);
75+
expect(onApply).toHaveBeenCalledTimes(1);
76+
expect(onApply.mock.calls[0]?.[0]).toMatchObject({ H: 150 });
77+
});
78+
79+
it("offers Snap head_dim → fix only when H is divisible by nh", () => {
80+
// H=128 / nh=8 = 16 → head_dim snap available.
81+
render(<DimEnvEditor
82+
value={{ H: 128, nh: 8, head_dim: 32, B: 1, S: 8 }}
83+
onApply={() => {}}
84+
/>);
85+
const snap = screen.getByTestId("dim-env-fix-set-head_dim");
86+
expect(snap.textContent).toContain("16");
87+
});
88+
89+
it("hides Snap head_dim when H is not divisible by nh", () => {
90+
// H=128 / nh=3 = 42.66 → head_dim snap NOT available; only H snap.
91+
render(<DimEnvEditor
92+
value={{ H: 128, nh: 3, head_dim: 50, B: 1, S: 8 }}
93+
onApply={() => {}}
94+
/>);
95+
expect(screen.queryByTestId("dim-env-fix-set-head_dim")).toBeNull();
96+
expect(screen.getByTestId("dim-env-fix-set-H")).toBeDefined();
97+
});
6598
});

0 commit comments

Comments
 (0)