Two interlocking fixes for StepComponents per operator feedback (#175):
1. **Transitive-mandatory promotion** (Fix A) — at module-load time walk
the dependency graph from every mandatory-tier component and promote
every reached component to mandatory. cnpg + valkey are lifted from
recommended → mandatory because Harbor / Gitea / PowerDNS / Keycloak
(mandatory or transitively mandatory) cannot run without them. They
no longer surface in Tab 1 ("Choose Your Stack"); they appear in Tab 2
("Always Included") under the FABRIC product section.
2. **Product-family model** (Fix B) — new `Product` type in
`componentGroups.ts` with `tier`, `components`, `familyDependencies`,
and `cascadeOnMemberSelection`. CORTEX is flagged as
cascade-on-member-selection (operator: "BGE alone doesn't have much
meaning unless we have Cortex... when chosen the entire family needs
to be selected"). Selecting any CORTEX member or Specter (whose deps
reach into CORTEX) cascades the rest of CORTEX plus FABRIC (CORTEX's
familyDependency). À-la-carte products (FABRIC, RELAY) keep
independent member selection.
UX additions:
- Product header per family in Tab 1 with "Select entire X family" CTA
(selectable via product-cta-<id> testid)
- Cascade-add toast surfaces both component-deps and family additions
- Cascade-remove confirmation modal lists every dependent that will go
- All operator-visible strings sourced from new
`stepComponentsCopy.ts` i18n module — no inline literals in JSX
Store actions: `addProduct(id)` / `removeProduct(id)` plus a
member-selection cascade in `addComponent` that respects the product
flag. Mandatory components are protected from any cascade-remove path.
Documentation: `docs/PRODUCT-FAMILIES.md` describes the dependency
model, every product entry, and worked examples (Specter, BGE, Harbor,
ClickHouse).
Vitest: 43 new test cases including transitive-promotion verification,
cross-product cascade, product CTA flow, and i18n wiring. All 146
tests pass; typecheck + build green.
Closes#175.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>