moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,335 @@
# Phase 2D — Onboarding Chain · Build Plan
> Scope: the day-0 onboarding/first-run chain (PRD §12.12 / DoD #6). Execution model is LOCKED:
> **in-place incremental refactor** of `apps/web` + targeted sidecar extension. PRD acceptance criteria
> win over mockups (PRD §24). Every claim below is grounded in live source under
> `D:/Projects/waggle-os-ux-refactor` (paths cited inline).
>
> **Critical reuse note:** Harvest preview-classification (C33 unreviewed + B2 confidence + B6 kind) was
> already built in **Phase 2B.3** (`harvest-classify.ts`, `harvest.ts` preview/commit, `harvest-kind-map.ts`)
> and the Memory Center "Needs review" review surface was built in **Phase 2B-FE.2** (`MemoryCenterTab.tsx`).
> 2D **reuses** these; it does NOT rebuild classification, confidence, or the review queue.
---
## 1. CURRENT vs TARGET
### Current OnboardingWizard (8 steps, `apps/web/src/components/os/overlays/OnboardingWizard.tsx`)
`STEP_NAMES` (`OnboardingWizard.tsx:35`):
`welcome → why-waggle → tier → memory-import → template → persona → api-key → ready` (steps 07).
| Step | Component | Wired vs hardcoded |
|---|---|---|
| 0 welcome | `WelcomeStep` | Hardcoded brand copy; 3s auto-advance (`:136-141`). |
| 1 why-waggle | `WhyWaggleStep` | Hardcoded `VALUE_PROPS` (`constants.ts:111`); "Skip and set me up" escape hatch (`:271-304`). |
| 2 tier | `TierStep` | `selectedTier``onUpdate({tier})` (UI density, not billing). |
| 3 memory-import | `ImportStep` | **Wired**: `harvestPreview`/`harvestCommit`/`scanClaudeCode` (`:156-202`). Commits inline then `goToStep(4)`. |
| 4 template | `TemplateStep` | Hardcoded 15 `TEMPLATES` (`constants.ts:10`), `TEMPLATE_PERSONA` map. |
| 5 persona | `PersonaStep` | `ALL_ONBOARDING_PERSONAS` (`constants.ts:46`) + custom-persona create. |
| 6 api-key | `ModelTierStep` | Model/key; calls `handleFinish``adapter.createWorkspace` (`:306-354`). |
| 7 ready | `ReadyStep` | Auto-finish after 2s. |
State backbone: `useOnboarding.ts` `OnboardingState` (`:10-19`) = `{ completed, step, tier?, workspaceId?,
apiKeySet?, templateId?, personaId?, tooltipsDismissed? }`, localStorage key `waggle:onboarding`. Resume is
half-built: `state.step` persists; returning-user auto-complete via Tauri flag + `getWorkspaces().length>0`
(`:102-176`). `?forceWizard=true` (DEV) forces step 0.
**There is NO profile/"Who Are You" step, NO Tool Discovery step.** The current IA (tier/api-key/model) is the
OLD onboarding, not the PRD §12.12 flow.
### Target — PRD §12.12 5-step chain (S12S17)
PRD order: **First Launch (S12) → Who Are You (S13) → Tool Discovery (S14) → Memory Import (S15) →
Memory Review (S16) → Workspace Creation (S17) → Home Cockpit**. Per the ratified C33 decision, Memory
**Review (S16) is NOT a blocking onboarding step** — import commits immediately as `status:'unreviewed'`,
and review happens later in the Memory Center "Needs review" filter / J08 queue. So the in-wizard chain is
effectively **5 interactive steps**: First Launch → Who Are You → Tool Discovery → Memory Import →
Workspace Creation, with a Ready/handoff terminal.
Target step mapping (keep the existing wizard shell + AnimatePresence switch; re-key `STEP_NAMES`):
```
0 first-launch (S12) rework WelcomeStep
1 who-are-you (S13) NEW WhoAreYouStep ← B8 profile + identity seed
2 tool-discovery (S14) NEW ToolDiscoveryStep (DEFERRABLE — see §6 note)
3 memory-import (S15) rework ImportStep — commit-as-unreviewed (C33)
4 workspace-create (S17) route through CreateWorkspaceDialog step bodies (or inline)
5 ready keep ReadyStep
```
> S16 Memory Review is delivered by the **already-shipped** Memory Center "Needs review" filter
> (`MemoryCenterTab.tsx:25-31`), NOT a wizard step. This is the C33 ratification (below).
### Explicit gate ratifications (verbatim, from `deltas/open-questions.md`)
- **B8** (`open-questions.md:28, :350`): *"Onboarding writes profile AND seeds the `identity` table."* /
*"Write profile + seed identity in the same onboarding commit. Rationale: prevents the two-store drift…
makes the Phase-1 Home greeting correct."* **[BLOCKS P1 greeting]**
- **C33** — RESOLVED to the middle path (`open-questions.md:45-53`): *"Commit-as-unreviewed, non-blocking
review: onboarding Import commits immediately (memory feels alive on first run), but frames land with
`status:'unreviewed'` + the B2 confidence score; Review is a **non-blocking** curation surface (Memory
Center "needs review" filter + the standing J08 queue), NOT a blocking onboarding step… Supersedes the
original §C C33 'blocking split' recommendation."*
- **B2** (`:39`): *"cheap heuristic at preview (source-trust × adapter-type × dedup), persisted in `metadata`
LLM scoring reserved for the standing J08 queue."*
- **B6** (`:41`): *"PRD §15.2 canonical in `@waggle/shared`; pure harvest + display-category mapping helpers.
Drop FE `event`/`insight` drift."*
- **C28** (`:420`): language selector → *"static disabled `English (US)` chip (no i18n exists…)."*
- **C29** (`:422`): *"drop the auto-advance"* on the first-launch privacy screen.
- **C30** (`:424`): *"keep Work type as a distinct personalization signal; don't re-ask what the template
already implies."*
- **C31** (`:426`): Tool Discovery id-space → *"namespaced scheme (`connector:gmail` / `tool:cursor`)."*
- **C32** (`:428`): *"pre-select from `GET /api/tools/detect` with a visible 'detected' badge."*
- **C34** (`:433`): Hermes/Codex/Cursor tiles → *"render as `upload`/'Other' file pickers or 'coming soon' v1."*
- **C35** (`:435`): Workspace Creation → *"record connector/MCP ids as workspace *intent* on the config; do
NOT run install/OAuth at create time."*
- **C6** (`:373`): `WorkspaceType` = *"`project | client | research | personal` (+`team`/`organization`
reserved); `type` coexists with the free-string `group`."*
---
## 2. B8 — Identity seed ("capture who-you-are + greet by name")
### What already exists
- **Profile store** (`profile.ts`): `UserProfile` (`:41-94`) carries `name/role/company/industry/bio` +
writingStyle/brand/interests/language/timezone. `GET /api/profile` (`:162`) + `PUT /api/profile` (`:167`,
partial-merge allow-list `:172-196`). On save it ALSO mirrors a `User identity: …` P/I frame to personal
memory (`:201-224`). Stored in `profile.json` (file, no SQLite). Adapter: `getProfile`/`updateProfile`
(`adapter.ts:1357-1365`, typed `any`).
- **Identity table** (`identity.ts` route + `hive-mind-core/src/mind/identity.ts` `IdentityLayer`):
single-row per-mind `identity` table (`name/role/department/personality/capabilities/system_prompt`).
`GET /api/identity` (`:62`) + `POST /api/identity` upsert (`:104-156`). `IdentityLayer` exposes
`create/get/exists/update/toContext` (`identity.ts:18-73`). This is what backs the **Home greeting name**
via `adapter.getIdentity()` (`adapter.ts:742`).
- **UserProfileApp** (`apps/web/.../apps/UserProfileApp.tsx`, "My Profile") already renders a "Who Are You?"
Identity tab capturing Name/Role/Company/Industry/Bio with `INDUSTRIES` + chip-toggle patterns —
the **reuse target for the form fields**.
### The GAP to close (B8)
1. **No `setIdentity` adapter method.** `adapter.getIdentity()` exists (`:742`) but there is **no**
`adapter.setIdentity(...)` calling `POST /api/identity`. → ADD it.
2. **Onboarding never writes profile or identity.** The wizard has no profile step; `OnboardingState` has no
profile fields. → NEW `WhoAreYouStep` posts to `PUT /api/profile` AND `POST /api/identity` in one save.
3. **3 new profile fields** (`workType?`, `teamSize?`, `goals?: string[]`) are net-new on `UserProfile`
(S13 §5). `PUT /api/profile` allow-list (`:172-196`) must be EXTENDED to merge them; `DEFAULT_PROFILE`
(`:96-135`) extended; FE `UserProfile`/types updated.
4. **Identity-seed payload:** map profile → identity fields: `name→name`, `role→role`,
`industry→department` (or leave department blank), optionally `bio→personality`. `system_prompt` left
empty (the agent path already injects the profile frame). No schema change — `IdentityLayer.create/update`
already upserts.
Backend substrate: `profile.json` + the existing identity table. **No `.mind` migration.**
---
## 3. C33 — Import → review (the EXACT existing contract to reuse)
### Existing harvest preview/commit contract (Phase 2B.3 — DO NOT rebuild)
**`POST /api/harvest/preview`** (`harvest.ts:227-258`). Request `{ data, source }`. Response:
```ts
{
source: ImportSourceType,
itemCount: number, // true total
types: Record<string, number>, // count by ImportItemType
preview: ClassifiedItem[], // back-compat: first 10
items: ClassifiedItem[], // NEW: full list (capped at PREVIEW_ITEM_CAP=5000)
itemsTruncated: boolean,
}
// ClassifiedItem = { id, title, type, source, kind: MemoryKind, confidence: number } (harvest.ts:239-246)
```
`kind` from `importItemTypeToMemoryKind` (`harvest-classify.ts:32`); `confidence` (0100) from
`harvestConfidence` (`harvest-classify.ts:66`, B2 heuristic = source-trust × item-type, no LLM).
**`POST /api/harvest/commit`** (`harvest.ts:265-582`). Request:
```ts
{ data?, source?, resumeFromRun?, selectedIds?: Array<string|number> } // harvest.ts:266-275
```
- `selectedIds` present + non-empty → filters items to that subset (`:344-347`); absent → commits ALL
(the C33 onboarding default).
- Each committed frame is stamped (`:449-456`), guarded on empty metadata, with:
```ts
{ kind: importItemTypeToMemoryKind(item.type), confidence: harvestConfidence(item),
status: 'unreviewed', sourceId: item.id }
```
→ exactly the C33 "commit-as-unreviewed" behavior. Response carries `saved`, `cognifySkippedReason`,
`wikiSkippedReason` (`no_real_embedder`), `runId`, etc.
**The review surface already exists.** `MemoryCenterTab.tsx` (Phase 2B-FE.2) has a **"Needs review"
status filter** (`:25-31`, `value:'unreviewed'`) + `MemoryCard` renders an "Needs review" badge
(`MemoryCard.tsx:18`). So S16's review is satisfied by Memory Center — **2D does not build a Review step.**
### The GAP the wizard must close
1. **Adapter `harvestCommit` cannot pass `selectedIds`.** Current signature
`harvestCommit(data, source)` (`adapter.ts:1898-1901`) — the body omits `selectedIds`. → WIDEN to
`harvestCommit(data, source, opts?: { selectedIds?, resumeFromRun? })`. (Backend already accepts it.)
2. **`harvestPreview`/`harvestCommit` typed `any`** (`adapter.ts:1893,1898`) — tighten to the shared
`HarvestPreview`/`HarvestCommitResult` shapes (repo bans `any`; CLAUDE.md §10 — `no-explicit-any` is error).
3. **`ImportStep` reads `result.preview` (first 10), not the classified `items[]`** with kind/confidence
(`OnboardingWizard.tsx:164` sets `importPreview` from `result.preview`). → rework to consume `items[]`
and render kind chips + confidence so the user sees what is being imported (matches S15 §4). Selection is
OPTIONAL per C33 (default = import all as unreviewed); if a "deselect" affordance is added it posts
`selectedIds`. **Minimum viable 2D = keep commit-all; just surface kind/confidence + the
"you can review later in Memory Center" pointer.**
4. **`ImportStepProps.importPreview` is `readonly unknown[]`** (`types.ts:39-40`) — widen to the classified
item type.
No backend change is required for C33 — the contract shipped in 2B.3. 2D is **FE wiring only** here.
---
## 4. S17 — Workspace creation: exists vs 2D adds
### Exists
- **Backend `POST /api/workspaces`** (`workspaces.ts:162-230`): accepts `name/group/icon/model/personaId/
templateId/storageType/storagePath/teamId/...`; enforces tier workspace-limit (403, `:194-203`);
auto-creates storage structure + starter skills + template starter-memory. `WorkspaceManager.create`
writes `workspace.json` (no SQLite).
- **Adapter `createWorkspace`** (`adapter.ts:253-260`): bridges `persona`→`personaId`.
- **Onboarding already creates a workspace** via `handleFinish`/`handleSkipSetup`
(`OnboardingWizard.tsx:278-326`) — bespoke `adapter.createWorkspace` calls, NOT the dialog.
- **`CreateWorkspaceDialog.tsx`** (1129 LOC): the full standalone modal (template/persona/storage/folder
picker). The richer S17 wizard (type picker, suggested capabilities, summary card) is a **separate S17
card** — most of that is the standalone-modal rework, not the onboarding embed.
### What 2D adds (minimal, onboarding-scoped)
2D needs a workspace-create **step in the chain**, not the full S17 modal rework. Minimal:
- Reuse the existing `handleFinish` create path (name + template + persona) OR render a trimmed
type-picker + name. Per **C6** add `type?: WorkspaceType` (`project|client|research|personal`) coexisting
with `group`; per **C35** record any selected connector/MCP ids as **intent** (config fields), not installs.
- Backend: EXTEND `POST /api/workspaces` Body + `WorkspaceManager.create` with the additive optional V2
fields (`description?, type?, status?, agentIds?, connectorIds?, mcpIds?`) — **JSON file, no migration**.
(This field extension is shared with the Phase-0 `WorkspaceConfig` V2 work and the standalone S17 card.)
- Adapter `createWorkspace` body type widened to carry `description/type` (+ capability-id arrays if used).
> The full stepped S17 wizard (Suggested Skills/Agents/Connectors/MCPs panels + summary card) is the
> **standalone S17 card's** scope. 2D's workspace-create is the onboarding-embedded variant; keep it thin
> and route it through the SAME create call so onboarding and the modal converge later (S17 §4 parity note).
---
## 5. WHAT ALREADY EXISTS (do-not-rebuild) vs NET-NEW
### Do-not-rebuild (verified present)
| Capability | Where | Status |
|---|---|---|
| Harvest preview w/ per-item `kind`+`confidence` | `harvest.ts:227-258` + `harvest-classify.ts` | DONE (2B.3) |
| Harvest commit w/ `selectedIds` + `status:'unreviewed'` stamp | `harvest.ts:265-456` | DONE (2B.3) |
| B2 heuristic confidence | `harvest-classify.ts:66` | DONE |
| B6 `ImportItemType→MemoryKind` (server) | `harvest-classify.ts:32` | DONE |
| B6 FE display-category map | `apps/web/src/lib/harvest-kind-map.ts` | DONE (2B.3) |
| Memory Center "Needs review" filter (the S16 review surface) | `MemoryCenterTab.tsx:25-31`, `MemoryCard.tsx:18` | DONE (2B-FE.2) |
| Profile store + `GET/PUT /api/profile` + identity-frame mirror | `profile.ts` | EXISTS |
| Identity table + `GET/POST /api/identity` + `IdentityLayer` | `identity.ts`, `mind/identity.ts` | EXISTS |
| `adapter.getProfile/updateProfile/getIdentity` | `adapter.ts:1357,1362,742` | EXISTS |
| Profile form fields/INDUSTRIES/chip-toggle to copy | `UserProfileApp.tsx` | EXISTS |
| Claude-Code auto-detect + harvest | `harvest.ts:831`, wizard `:182-202` | EXISTS |
| Connector catalog `GET /api/connectors`; tool-detect `GET /api/tools/detect` (S14) | `connectors.ts`, `tools.ts` | EXISTS |
| `POST /api/workspaces` + tier-limit + starter skills | `workspaces.ts:162` | EXISTS |
| Wizard shell (progress/dots/Back/Skip/AnimatePresence) | `OnboardingWizard.tsx:399-575` | EXISTS — keep |
| Resume backbone (`state.step` persist) | `useOnboarding.ts` | EXISTS |
### Net-new in 2D
- FE: `WhoAreYouStep.tsx` (S13); `ToolDiscoveryStep.tsx` + `lib/tool-recommendations.ts` (S14, deferrable);
reworked `WelcomeStep`/`FirstLaunchStep` (S12); reworked `ImportStep` (S15 surface kind/confidence);
workspace-create step (S17 thin); `lib/onboarding-profile.ts` (option constants + `buildProfilePreview`).
- Adapter: **`setIdentity(...)`** (new); `harvestCommit` 3rd `opts` arg; tighten `harvestPreview/harvestCommit`
types; widen `createWorkspace` body type.
- Backend: EXTEND `PUT /api/profile` allow-list (+`workType/teamSize/goals`) + `DEFAULT_PROFILE` + `UserProfile`;
EXTEND `POST /api/workspaces` + `WorkspaceManager.create` V2 fields. **No new route, no `.mind` migration.**
- Types: `OnboardingState` += `profileSeeded?`, `toolsUsed?: string[]` (localStorage, additive); shared
`WorkspaceType` (C6) + `HarvestPreview`/`HarvestCommitResult`; FE `UserProfile` (+3 fields).
- Wizard: re-key `STEP_NAMES`, the step switch (`:498-572`), progress math (`progressPct :396`, dots `:472`,
`aria-valuemax :426`), Back-range (`:444`), and the `goToStep(N)` auto-advances.
---
## 6. Minimal sub-phase build plan (each independently tsc + test verifiable)
> Verify gate per sub-phase (CLAUDE.md §2): `tsc` on touched packages
> (`shared → hive-mind-core → server`, then `npm run build` for apps/web), `npm run test -- --run`, `npm run lint`.
> Build order: shared → hive-mind-core → core → agent → server.
### 2D.1 — Backend + shared + adapter (no UI)
Ordered, each file with the exact change + contract:
1. `packages/shared/src/types.ts` — add `WorkspaceType = 'project'|'client'|'research'|'personal'` (C6;
`team`/`organization` reserved). (`MemoryKind` already canonical here from B6/2B.3 — do NOT redefine.)
2. `packages/server/src/local/routes/profile.ts` —
- `UserProfile` (`:41-94`): add `workType?: string; teamSize?: string; goals?: string[]`.
- `DEFAULT_PROFILE` (`:96-135`): add the three defaults (`''`, `''`, `[]`).
- `PUT` allow-list (`:172-196`): merge the three (`if (updates.workType != null) …`).
- Identity-frame mirror string (`:205-210`): optionally append role/industry (pure edit).
3. `packages/server/src/local/routes/workspaces.ts` — extend the `POST` Body (`:163-181`) + the
`WorkspaceManager.create(...)` call (`:223-230`) with additive optional `description?, type?, status?,
agentIds?, connectorIds?, mcpIds?` (JSON, no migration). (Coordinate with Phase-0 V2 field work.)
4. `packages/hive-mind-core/src/workspace-manager.ts` — add the same optional fields to `WorkspaceConfig` +
`CreateWorkspaceOptions`; persist in `create()`/`update()` (stamp `updatedAt`). **No DB migration.**
5. `apps/web/src/lib/adapter.ts` —
- NEW `async setIdentity(body): Promise<IdentityResponse>` → `POST /api/identity` (mirror `getIdentity`
`:742`).
- `harvestCommit` (`:1898`): add `opts?: { selectedIds?: Array<string|number>; resumeFromRun?: number }`
and include in the POST body.
- Tighten `harvestPreview`/`harvestCommit` return types to shared `HarvestPreview`/`HarvestCommitResult`
(drop `any`); `getProfile`/`updateProfile` to `UserProfile` (drop `any`).
- `createWorkspace` body type (`:253`): add `description?, type?: WorkspaceType, connectorIds?, mcpIds?`.
**Verify 2D.1:** tsc shared + hive-mind-core + server (+ apps/web build for adapter); add/extend route tests
(profile merge of 3 fields; identity upsert; commit `selectedIds` filter — the last already covered by 2B.3
tests, just confirm green).
### 2D.2 — Frontend (the chain)
1. `apps/web/src/lib/onboarding-profile.ts` (NEW, pure + `.test.ts`) — `WORK_TYPES`, `TEAM_SIZES`, `GOALS`
option constants + `buildProfilePreview(partial)` (keeps the step thin).
2. `apps/web/src/lib/types.ts` — promote shared `UserProfile` (+ `workType/teamSize/goals`); import
`WorkspaceType`, `HarvestPreview`/`HarvestCommitResult` from `@waggle/shared`.
3. `apps/web/src/hooks/useOnboarding.ts` — `OnboardingState` (`:10-19`): add additive optional
`profileSeeded?: boolean`, `toolsUsed?: string[]` (localStorage only).
4. `apps/web/.../onboarding/WelcomeStep.tsx` (rework → S12): align copy; add static `English (US)` chip
(C28) + privacy note; consume `useOfflineStatus`. `OnboardingWizard.tsx:136-141`: **drop the 3s
auto-advance** (C29).
5. `apps/web/.../onboarding/WhoAreYouStep.tsx` (NEW → S13/B8): Name/Role/Industry (reuse `UserProfileApp`
`INDUSTRIES`/chips) + WorkType/TeamSize/Goals + live preview. On Continue: `adapter.updateProfile(...)`
**AND** `adapter.setIdentity({name, role, department: industry})`. Props `{ profile, onChange,
onContinue, onBack, saving }`. Add `WhoAreYouStepProps` to `onboarding/types.ts`.
6. `apps/web/.../onboarding/ToolDiscoveryStep.tsx` (NEW → S14): grouped grid from `getConnectors` +
`detectTools` (pre-check detected, C32); namespaced ids `connector:*`/`tool:*` (C31); writes
`toolsUsed[]`. **DEFERRABLE** — if descoping 2D, omit this step and renumber; S14 has zero backend and
can land in a 2D follow-up. (Flag in the chain as optional.)
7. `apps/web/.../onboarding/ImportStep.tsx` (rework → S15): consume `result.items[]` (kind+confidence) not
`preview[]`; render kind chips + `ConfidenceBadge` (reuse `components/ui/confidence-badge`); add the
"review anytime in Memory → Needs review" pointer (C33). Hermes/Codex/Cursor = upload/"Other"/coming-soon
(C34). Keep commit-all default; commit still lands `unreviewed` server-side. Widen `ImportStepProps`.
8. `apps/web/.../onboarding/` workspace-create step (S17 thin): trimmed type-picker (C6) + name → existing
`handleFinish` create path; record connector/MCP intent ids (C35). (Full stepped S17 = separate card.)
9. `apps/web/.../overlays/OnboardingWizard.tsx` — re-key `STEP_NAMES` (`:35`) to
`first-launch/who-are-you/tool-discovery/memory-import/workspace-create/ready`; rewrite the step switch
(`:498-572`); fix progress math (`:396`, dots `:472`, `aria-valuemax :426`, Back-range `:444`); add
`handleProfileSave`; point import commit at the new step order; **remove the dead tier/api-key/model
steps from the chain** (or keep behind a flag — surgical, prefer removal of the now-orphaned wiring).
**Verify 2D.2:** `npm run build` (apps/web tsc), `npm run test -- --run` (new `onboarding-profile.test.ts`
+ any wizard tests), `npm run lint`. Manual: `?forceWizard=true` walks First Launch → Who Are You (writes
profile+identity → Home greets by name) → [Tool Discovery] → Import (commits unreviewed; appears in Memory
"Needs review") → Workspace Create → Home.
### Largest single risk
**The `OnboardingWizard.tsx` step-index rewire.** It re-keys `STEP_NAMES`, the AnimatePresence step switch,
progress %, the dot array, `aria-valuemax`, the Back-button range, AND every `goToStep(N)` literal
(`:174, :199, :300, :350` etc.) — plus removal of the orphaned tier/api-key/model handlers and their
step-local state (`selectedTier/apiKey/keyValid/...`, `:47-60`). An off-by-one or a stale `goToStep(4)`
strands the user mid-flow or skips identity-seed/commit. Mitigation: change the step set in ONE pass, drive
all navigation off `STEP_NAMES.indexOf(name)` (not magic numbers), keep an E2E that asserts the full chain
reaches Home, and verify resume (`state.step`) lands on the correct re-keyed step. This is exactly the churn
S13/S14/S15/S16 §7 all warn must be "rewired once, not per-card."