From e5594b0d3cca56d9e33849b4ed5869f171d0a1a1 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 21 Apr 2026 15:43:04 -0600 Subject: [PATCH] fix(ui): hide engine-internal presets from user authoring surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Modifier Profile editor's Presets tab surfaced the plumbing preset '__modifier-profile-integration__' — an internal hook bundle that wires modifier-profile facts (CaptureFlags, DirectionAdditions, DamageResistance, on-damage / on-capture / aura triggers) into the engine runtime. ChessEngine auto-activates it when a profile is supplied; toggling it from a profile has no meaningful effect and confuses users who see 'Modifier Profile Integration' as a bundle-able rule variant. Filter any preset id matching the '__x__' double-underscore naming convention out of: - PresetPanel's allPresets list (the Presets tab in the editor) - LayoutPicker's suggestedPresets chip row (in case a layout ever accidentally lists one) Generalized so future internal presets following the same convention are auto-hidden without maintaining an exclusion list. New unit test (PresetPanel.filter.test.ts) pins the contract: registry contains the internal preset, filter yields a non-empty user-safe list, known public preset (extinction-chess) survives. --- packages/chess/src/ui/LayoutPicker.tsx | 9 +++- .../chess/src/ui/PresetPanel.filter.test.ts | 46 +++++++++++++++++++ packages/chess/src/ui/PresetPanel.tsx | 18 +++++++- 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 packages/chess/src/ui/PresetPanel.filter.test.ts diff --git a/packages/chess/src/ui/LayoutPicker.tsx b/packages/chess/src/ui/LayoutPicker.tsx index fe2a41c..eb38740 100644 --- a/packages/chess/src/ui/LayoutPicker.tsx +++ b/packages/chess/src/ui/LayoutPicker.tsx @@ -144,7 +144,14 @@ export function LayoutPicker({ value.suggestedPresets.length > 0 && (
Suggested rules: - {value.suggestedPresets.map((presetId) => { + {value.suggestedPresets + // Never surface engine-internal plumbing presets + // (__x__ naming convention) even if a layout + // accidentally lists one — they auto-activate via + // the engine constructor and can't be meaningfully + // toggled by the user. + .filter((id) => !(id.startsWith('__') && id.endsWith('__'))) + .map((presetId) => { const presetDef = PRESET_REGISTRY.get(presetId); const label = presetDef?.name ?? presetId; const isActive = activations.some(a => a.id === presetId); diff --git a/packages/chess/src/ui/PresetPanel.filter.test.ts b/packages/chess/src/ui/PresetPanel.filter.test.ts new file mode 100644 index 0000000..59c43d5 --- /dev/null +++ b/packages/chess/src/ui/PresetPanel.filter.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from "vitest"; +import { PRESET_REGISTRY } from "../presets/registry.js"; +import "../presets/index.js"; +// Side-effect import: registers the __modifier-profile-integration__ +// pseudo-preset (top-level register call in apply.ts). The modifier +// barrel doesn't pull this in, so we import directly to mirror the +// engine constructor's boot ordering. +import "../modifiers/apply.js"; + +/** + * Regression guard — the Modifier Profile editor's Presets tab used + * to surface the engine-internal `__modifier-profile-integration__` + * preset because it calls PRESET_REGISTRY.getAll() directly. That + * preset is plumbing (it wires modifier facts into move-gen / damage + * pipelines) and is auto-activated by the ChessEngine constructor, + * so it should never appear as something a user can toggle on a + * profile. + * + * The panel now filters any id matching the `__x__` double-underscore + * convention. This test pins that contract. + */ +describe("Preset authoring surface excludes internal presets", () => { + const isInternal = (id: string) => id.startsWith("__") && id.endsWith("__"); + + it("registry contains the modifier-profile-integration internal preset", () => { + const all = PRESET_REGISTRY.getAll(); + const internal = all.filter((p) => isInternal(p.id)); + expect(internal.length).toBeGreaterThan(0); + expect(internal.map((p) => p.id)).toContain( + "__modifier-profile-integration__", + ); + }); + + it("filtering internal presets yields a user-authoring-safe list", () => { + const all = PRESET_REGISTRY.getAll(); + const userFacing = all.filter((p) => !isInternal(p.id)); + expect(userFacing.length).toBeGreaterThan(0); + for (const p of userFacing) { + expect(isInternal(p.id)).toBe(false); + } + // Sanity: at least one well-known user-facing preset survives + // the filter. extinction-chess is a stable long-standing id so + // this assertion won't churn with preset-registry shuffling. + expect(userFacing.map((p) => p.id)).toContain("extinction-chess"); + }); +}); diff --git a/packages/chess/src/ui/PresetPanel.tsx b/packages/chess/src/ui/PresetPanel.tsx index a3f3291..fc3a89e 100644 --- a/packages/chess/src/ui/PresetPanel.tsx +++ b/packages/chess/src/ui/PresetPanel.tsx @@ -121,7 +121,23 @@ export function PresetPanel({ onChange, boundLayout, }: PresetPanelProps) { - const allPresets = useMemo(() => PRESET_REGISTRY.getAll(), []); + // Filter engine-internal presets out of the authoring surface. + // Convention: any id wrapped in double-underscores (`__x__`) is + // reserved for plumbing presets the engine auto-activates — e.g. + // `__modifier-profile-integration__` wires modifier facts into + // move-gen / damage pipelines. Those should never appear in a + // profile's bundled activations because they can't be meaningfully + // toggled; they either flip on via the engine constructor or they + // don't. Same guard protects against any future internal presets + // that follow the naming convention without our having to + // hand-maintain an exclusion list. + const allPresets = useMemo( + () => + PRESET_REGISTRY.getAll().filter( + (p) => !(p.id.startsWith('__') && p.id.endsWith('__')), + ), + [], + ); const activeById = useMemo(() => { const m = new Map(); for (const a of activations) m.set(a.id, a);