# 06 — Design Implications for CoachCarouselRecruitTool

Documentation suite for **CoachCarouselRecruitTool**. Synthesized 2026-07-14. This document turns the
research (docs 01–05) into concrete architecture and write recipes. Source tags as in doc 01. Recipes marked
**PROVEN** are lifted from tools with real users; recipes marked **DESIGNED (UNVERIFIED)** are ours and need
scratch-save write tests before shipping.

---

## Table of contents

1. [What the tool must do (recap) and what the save gives us](#1-what-the-tool-must-do-recap)
2. [Recommended architecture: single-save detection + local snapshot DB](#2-recommended-architecture)
3. [Table registry (resolve-by-uniqueId list)](#3-table-registry)
4. [Stage gating](#4-stage-gating)
5. [Detecting coaching changes](#5-detecting-coaching-changes)
6. [Selecting decommit candidates](#6-selecting-decommit-candidates)
7. [Write-back recipes](#7-write-back-recipes)
8. [Save/write pipeline (backups, verification)](#8-savewrite-pipeline)
9. [Risk register — what could corrupt a save or dynasty](#9-risk-register)
10. [Open questions (numbered, prioritized)](#10-open-questions)
11. [Sources](#sources)

---

## 1. What the tool must do (recap)

1. **Track coaching changes** after the in-game carousel (fired / hired elsewhere / retired / promoted) in
   its own database.
2. At the **first offseason recruiting stage (transfer portal)**, apply configurable consequences:
   - a % of committed recruits **decommit** when their school had a coaching change (higher % for HC change;
     OC changes affect offensive players, DC changes defensive players);
   - some recruits **follow** a departing coach to his new school (scholarship space + similar-or-better
     prestige checks);
   - decommitted recruits **keep the old school in their top-schools list** so it can re-recruit them with a
     small edge.
3. **Write the changes back** into the dynasty save.

The save supports all three. The decisive research facts:

- Coaching changes are fully reconstructable **at the portal stage from a single save** via
  `CoachTransactionHistoryEntry` + the Coach-table movement signature (doc 03 §10–11). `[dump §6][diff §2]`
- Commitment = `Recruit.RecruitStage` + TopSchools slot 0; there is a native `RecruitStageAdvance='Decommit'`
  engine command (behavior UNVERIFIED) and a proven manual commit recipe to invert (doc 04 §5, §12). `[dump §2,§4][fc §4]`
- The portal stage is precisely detectable from `SeasonInfo` (doc 05 §8–9). `[diff §1]`

## 2. Recommended architecture

### 2.1 Detection strategy: in-save first, snapshot DB as enrichment

**Primary: single-save detection at the portal stage.** When the user loads a save at the portal stage, the
save itself contains the full current-cycle carousel history (`CoachTransactionHistoryEntry` persists until
the *next* carousel) plus the movement signature on Coach rows. No prior snapshot is required for the core
feature — this makes the tool work on the first run, on copied saves, and after missed cycles. `[dump §6][diff §6b]`

**Secondary: local snapshot DB** (JSON in Electron `userData`, the pipeline tool's proven pattern
`[pipe §3]`), keyed `dynastyCode = String(Franchise.LeagueID)` → `seasonYear` → data:

- store each cycle's detected coaching changes + the consequences we applied (audit/idempotency log);
- store a per-cycle Team.HC/OC/DC ref snapshot so a *later* run can diff across cycles if the user skipped a
  year (the in-save history table only covers the most recent carousel `[dump §6]`);
- optionally store an in-window `JobOpening` snapshot (Team, Position, PrevCoach, SelectedCoach, Reason) if
  the user happens to open the tool during weeks 15–20 — the only way to get exact `CoachLeaveReason` per
  opening. `[diff §2.4]`

Behavioral rules to copy from the pipeline tool `[pipe §3]`: idempotent per (dynasty, season) — re-running
overwrites that cycle's entry; shape-sniffing readers instead of migrations; warn (don't block) when the
loaded save is older than recorded history; history failure ≠ save-write failure.

### 2.2 Process shape

Electron GUI following the two reference GUIs `[pipe §4][carousel §0]`: main process owns madden-franchise +
fs; `contextIsolation` + preload `contextBridge`; renderer is dumb; all IPC request/response via
`ipcMain.handle`. Save discovery via the force-commit picker recipe (OneDrive-aware, FBCHUNKS magic,
`dynasty*` no-dot filenames, skip `-autosave`) `[fc §9]`. Plan pipeline: **preview → confirm → apply-to-copy
→ verify → report** (pipeline tool + eric-levinson save-editor precedent `[pipe §1][web §8]`).

## 3. Table registry

Resolve strictly by `getTableByUniqueId` (null-check!); numbers verified in docs 02–05:

```js
const UID = {
  Franchise: 2226370608,            // LeagueID (dynasty key), Transfers, SeasonInfo refs
  SeasonInfo: 3123991521,
  Team: 3359508968,                 // 9 decoys share the name!
  Conference: 3820706130,
  ConferenceTeamSlots: 2477738738,  // Team[] used by Conference.TeamSlots
  Coach: 1860529246,                // decoy: 2796679390
  Player: 1612938518,
  Recruit: 1873209313,
  RecruitTarget: 59043175,
  RecruitTargetArr: 2412159097,     // RecruitTarget[] 138×35
  UserRecruitTarget: 3987156317,
  RecruitingBoard: 220276943,
  ProspectTargetSchoolA: 3789266353,
  ProspectTargetSchoolB: 3843719174, // spare pool, purpose UNVERIFIED
  ProspectTargetSchoolArr: 2332540366,
  JobOpening: 263453863,
  StaffPersonContractOffer: 674348040,
  StaffPersonContractOfferArr: 4119397260, // decoy: 1063701971
  CoachTransactionHistoryEntry: 2701814500,
  MySchoolTrackingTable: 349376083,
  LeagueSetting: 87558994,
  SchoolPipelineInfluence: 4261714800,
  SchoolPipelineInfluenceArr: 3284177001,
  StaffHiringEval: 1794614061,
};
```

Cross-build uniqueId stability is UNVERIFIED (consistent across all saves of this build) — keep the
name+largest-recordCapacity heuristic as a fallback resolver. `[diff §5][fc §1]`

## 4. Stage gating

From doc 05 §9 (verified-live combination):

```js
const runnable = cal.stage === 'OffSeason'
              && cal.offseasonStage === 2
              && si.records[0].getValueByKey('IsTransferSignPeriodActive') === true;
```

- If `JobOpening.header.nextRecordToUse === 0` the dynasty has never run a carousel — nothing to do. `[diff §6b]`
- If the tool is opened mid-carousel (live JobOpening rows / `IsCarouselPeriodActive`), offer to snapshot
  openings (reason data!) but do NOT apply consequences yet — hires aren't final until the offseason. `[diff §2.4]`
- Respect `LeagueSetting.CoachFiring === 'OFF'` (no firings will exist) and surface
  `IsRecruitFlippingEnabled` in the UI, since our re-recruit mechanic interacts with flipping. `[dump §12]`

## 5. Detecting coaching changes

### 5.1 Primary: CoachTransactionHistoryEntry (single save, portal stage)

Iterate non-empty rows (skip row-0 sentinel), decode with the verified pattern table (doc 03 §10.2):

| OldTeam | NewTeam | ContractStatus raw | changeType |
|---|---|---|---|
| set | null | 2 (PendingFire) | `fired` |
| set | set | 6 (PendingHire) | `moved` (promotion/demotion/lateral from Old/NewCoachPosition) |
| null | set | 6 (PendingHire) | `hiredFromPool` |
| set | null | 3 (PendingNFL) | `wentPro` |
| set | null | 5 (PendingRetire) | `retired` |

Compare `ContractStatus` on **raw values** (formatted reads return aliases `First_Pending`/`Last_Pending`/
`Last_Active`). Filter to `SeasonYear === cal.dynastyYear` (the table can retain the previous cycle through
preseason). `[dump §6]`

### 5.2 Cross-check: Coach movement signature + Team refs

For each history entry (and as a safety net for anything the log missed), verify against Coach rows
(doc 03 §11): movers have `PrevTeamIndex !== 255 && SeasonsWithTeam <= 1`; departed-unhired are
`FreeAgent + PrevTeamIndex set`; retirees are `Retired`. Team-side `HeadCoach/OffensiveCoordinator/
DefensiveCoordinator` refs are already final at the portal stage — the current ref at the vacated role is
the replacement coach. `[diff §2.1–2.2]`

### 5.3 Derive per-school "coaching change events" for the consequence engine

For each Team: `{hcChanged, ocChanged, dcChanged, departures: [{coachRow, role, changeType, newTeamRow|null}]}`.
A school counts as changed for a role if the history log shows a departure from it at that role OR its
current role ref differs from our previous-cycle snapshot (when one exists). Note: how `ContractEnding`
departures appear in the history table is UNVERIFIED — the FreeAgent-signature sweep (5.2) catches them.
`[dump §6][diff §2.2]`

## 6. Selecting decommit candidates

1. Committed recruits of a changed school: `Recruit.RecruitStage ∈ {SoftCommitted(4), HardCommitted(5),
   Signed(6 raw)}` AND TopSchools slot0 `TeamId === school.TeamIndex` (doc 04 §5). Decide policy for
   `Signed`: real-world signees rarely flip; recommend default = only Soft/HardCommitted are eligible, Signed
   optionally with a much lower %. Compare stage on raw values (Signed aliases `Last_`). `[dump §2,§4]`
2. Side-of-ball filter for OC/DC changes: classify `Player.Position` via the PositionE sets (offense 0–9,
   defense 10–18, special 19–21, explicit handling for pseudo-positions 22–34 which DO occur). HC change =
   all positions. `[jersey §5][dump §9]`
3. Weighting knobs (all readable from the save): `Player.RecruitingDealbreaker ∈ {CoachPrestige,
   CoachStability}` → raise decommit odds; `MySchoolTrackingTable.CoachStabilityGrade/CoachPrestigeGrade` →
   scale odds by how hard the game itself dinged the school; departing coach's `CoachPrestige[Score]` →
   bigger loss, bigger effect. `[dump §8,§9][schema §10]`
4. Determinism: seed RNG per `(LeagueID, seasonYear, recruitRow)` so re-runs are idempotent (eric-levinson
   precedent: `sha256(seed|counter|label)` `[web §8]`).

## 7. Write-back recipes

### 7.1 Decommit a recruit — two candidate mechanisms

**Option A — native engine command. DESIGNED (UNVERIFIED, test first):**

```js
recruit.RecruitStageAdvance = 'Decommit';   // enum raw 2 — first-class engine action [dump §2]
```

Let the next weekly advance process it. Unknowns: does the engine honor it on Soft/Hard/Signed stages; what
does it do to TopSchoolsList/board rows; does it fire `RecruitStatusUpdateEvent`/news. This is the top
write-test priority — if it works, it is the safest mechanism because the engine keeps all denormalized
state consistent itself.

**Option B — direct state edit (inverse of the PROVEN force-commit recipe). DESIGNED (UNVERIFIED):**

```js
// 1. Recruit side: stage back to an uncommitted value.
recruit.RecruitStage = 'Top3';        // keeps old school prominent; 'Top10' for a harder reset [dump §2]
// CAUTION [1SRC]: RecruitStage is engine-ratcheted upward; whether a manual downward write sticks is
// UNVERIFIED [web §5a]. If the engine re-commits next advance, also create CommitScore headroom:
//   ensure slot0.TeamInfluence < commit threshold (see step 2) or raise recruit.CommitScore.

// 2. Top-schools side: demote the old school but KEEP it listed (our "re-recruit edge" spec):
//    - set old school's TeamInfluence to a configurable fraction of CommitScore (e.g. 40–60% — below the
//      50%/75%/100% Top5/Top3/Commit thresholds [schema §3]) — this IS the "small edge";
//    - re-sort all 10 slots to influence-descending (UI reads physical order [web §5a, 1SRC]);
//    - remember other slots may be 0 for previously-committed recruits [dump §3] — refill sensibly.

// 3. Board side: find the old school's RecruitTarget row for this recruit (walk Team.RecruitingBoard);
//    set ProspectInfluenceTotal to the same demoted value (it is the MASTER store the weekly advance
//    re-derives top-school entries from [web §5a, 1SRC]). Keep ScholarshipStatus='Offered'.

// 4. Denormalized list: remove the recruit's Player ref from Team.CommittedPlayers (Player[] row) —
//    UNVERIFIED whether the engine rebuilds this weekly; safer to keep it consistent [schema §3].
//    (Array-write pattern: slots + record.arraySize + table.arraySizes[row] + isChanged [carousel §3.4].)

// 5. Optional flavor: clear RecruitTarget.CommittedWeekNumber; leave NIL fields untouched.
```

### 7.2 "Follow the coach" — re-commit to the new school

Eligibility check (all live-readable): new school `Team.TeamPrestige >= oldPrestige - tolerance`
(configurable; the engine's own poach gates: `PoachThresholdHC=2`, `PoachThresholdCoord=1` `[schema §2]`);
scholarship space = committed count at the new school `< 35` (count recruits whose slot0 == new school and
stage ≥ SoftCommitted; also respect board capacity 35) `[web §5a][dump §4]`.

Then apply the **PROVEN** force-commit write-set (doc 04 §12) targeting the new school, with two deltas:

```js
// board side: hijack a free/dangling RecruitTarget row on the NEW school's board  [fc §4]
// (mid-cycle the RecruitTarget pool has only ~75 spare rows league-wide — prefer rows whose Recruit ref
//  is empty/dangling on that board; if none, reuse the recruit's own old-school row? UNVERIFIED) [dump §1]
target.Recruit = recruitRef; target.CommittedWeekNumber = seasonInfo.CurrentWeek;
target.ScholarshipStatus = 'Offered';
// NIL: force-commit zeroes all three NIL fields; for followers consider preserving NILExpectation instead
// (UNVERIFIED whether nonzero values affect AI decommit odds).

recruit.RecruitStage = 'HardCommitted';           // or 'SoftCommitted' for a softer follow
recruit.RecruitStageAdvance = 'InstantCommit';    // engine processes on next advance [fc §4]

// top-schools: new school into slot 0 with TeamInfluence = CommitScore; old school demoted to the
// configured "edge" value in a later slot; re-sort slots by influence desc.
slot0.TeamId = newSchool.TeamIndex; slot0.TeamInfluence = recruit.CommitScore;
```

Alternative commit route (community-tested, **[1SRC]**): make the new school the influence leader and set
`CommitScore ≤ leaderInfluence` → engine hard-commits on next advance. `[web §5a]`

### 7.3 Edit a top-schools list (the re-recruit edge, standalone)

```js
const listRef = recruit.getFieldByKey('TopSchoolsList').referenceData;      // {tableId, rowNumber}
const listT  = await getT(listRef.tableId);                                  // ProspectTargetSchool[] table
const listRow = listT.records[listRef.rowNumber];
const slots = listT.offsetTable.map(o => o.name);                            // ProspectTargetSchool0..9
// resolve all 10 element rows, edit TeamId/TeamInfluence as plain ints (TeamId = Team.TeamIndex),
// then RE-SORT: permute element VALUES so influence is descending across slots 0..9.
// Validate each slot ref's tableId against the resolved element table (two ProspectTargetSchool pools!).
```
`[fc §4][dump §3][web §5a]`

### 7.4 Durability pass

CPU boards are re-curated **every weekly advance** and injected low-influence targets get dropped
**[1SRC]** `[web §5a]`; force-commit's README likewise warns some forced commits decommit later `[fc §11]`.
Our writes land once at the portal stage, so: (a) prefer engine-native mechanisms (`RecruitStageAdvance`)
that resolve in one advance; (b) after the user advances one week in-game, offer a "verify & re-apply"
pass that re-checks our intended state (cheap, since we log every intended change in the local DB).

## 8. Save/write pipeline

1. **Never write the user's original file.** Copy → open the copy → mutate → `franchise.save()` on the copy
   (or open original read-only and `save(newPath)`). The pipeline tool's copy-then-write flow round-tripped
   cleanly through the game. `[pipe §1][diff §4]`
2. **Backup anyway** before any operation, with a SHORT name (long filenames make the game refuse to load —
   jersey-tool changelog) — e.g. `backups/<save>-<yyyymmddHHMM>` in a subfolder. `[jersey §4]`
3. **Post-write verification** (pipeline tool v0.5.0 pattern): re-open the written file fresh, re-read every
   mutated table, compare intended field values, report `{verified, verificationError}`. `[pipe §1]`
4. **Chunk-2 integrity check** (until the corruption question is settled): compare the byte region after
   `0x52 + u32@0x4A` between original and written file; if it shifted/changed, halt and switch to a
   libdeflate-based chunk-1 re-wrap. `[web §1a]`
5. Write-set hygiene: batch all mutations, then a single `save()`; never enable `saveOnChange`; validate int
   ranges against `offset.minValue/maxValue` (the library does not clamp `[api §6]`); enum writes by member
   name with raw-value comparison on reads.
6. Run with the game closed / at main menu; never target `-autosave` files. `[fc §8]`

## 9. Risk register

| # | Risk | Severity | Mitigation |
|---|---|---|---|
| 1 | Stock `save()` recompressed stream outgrows chunk-1 slot → silently clobbers CharacterVisuals (chunk 2) | save corruption (latent, data-dependent) | write to copy; chunk-2 byte check after every save; libdeflate fallback `[web §1a]` |
| 2 | Free-list corruption: editing empty rows without un-emptying (fields outside first 32 bits), or `empty()` leaving stale bodies, or double-headed chains | game crash | `autoUnempty: true` when allocating; zero bodies on empty; `recalculateEmptyRecordReferences()` sanity check `[api §2]` |
| 3 | Writing through a generic schema (table whose schema was silently dropped on attribute-count mismatch) | corrupt table | assert `table.schema` exists before any write `[api §1]` |
| 4 | Array-table writes missing `arraySize`/`table.arraySizes` sync | game reads wrong list length | canonical array-write helper `[carousel §3.4]` |
| 5 | Downward `RecruitStage` write not honored / re-ratcheted by engine | feature silently no-ops | write test on scratch save; fall back to `RecruitStageAdvance='Decommit'`; verify after one advance `[web §5a]` |
| 6 | `Team.CommittedPlayers` / TopSchools / board left inconsistent with RecruitStage | AI confusion, phantom commits | mutate all four sides together (recipe 7.1); post-advance verify pass |
| 7 | TopSchools slots not re-sorted to influence-desc | UI misrenders recruit's school list | always re-sort after edits **[1SRC]** `[web §5a]` |
| 8 | RecruitTarget pool exhaustion (cap 4870; ~75 spare mid-cycle) / ActiveRecruitingPitch[] exhaustion | allocation failure | hijack existing rows (never allocate); check `nextRecordToUse < recordCapacity` `[dump §1][web §5a]` |
| 9 | TeamIndex confusion: FCS pseudo-teams share 255; TeamIndex ≠ Team row; ProspectTargetSchool.TeamId is a TeamIndex | consequences applied to wrong school | key by Team row internally; single TeamIndex→row map built per save, collision-aware `[diff §5]` |
| 10 | Enum alias misreads (`First_Active` vs `Signed`, `Top10` vs `First_`, raw `'0000'` Motivations) | misclassification | compare raw values / alias sets everywhere `[dump §15]` |
| 11 | table2 string overflow | silent truncation | length-check against `maxLength` (we mostly don't write strings) `[api §4a]` |
| 12 | int overflow (no clamping) e.g. TeamInfluence > 1023 scale, NumContractOffers > 12 | undefined engine behavior | clamp to schema max ourselves; keep TeamInfluence within the observed 0–1023 scale `[api §6][dump §3]` |
| 13 | Backup filename too long → game won't load it | user confusion / apparent data loss | short names, subfolder `[jersey §4]` |
| 14 | Running at the wrong stage (boards empty at preseason; carousel mid-flight) | writes to nonexistent rows | hard stage gate (§4) + "tables never populated" handling `[diff §6c]` |
| 15 | User juggling save copies → applying against an older save | history/DB divergence | max-season sanity check with warning (pipeline-tool pattern) `[pipe §3]` |
| 16 | RAM: whole save (and copies) in memory during save | OOM on low-end machines | one franchise instance at a time; avoid full Player reads `[api §6][dump §15]` |

## 10. Open questions

Prioritized; each needs either a specially captured save or a scratch-save write test.

1. **Does `RecruitStageAdvance='Decommit'` cleanly decommit a Soft/Hard/Signed recruit on week advance**, and
   what does it do to TopSchoolsList/board rows/news? (Top priority — decides recipe 7.1 A vs B.) `[dump §14]`
2. **Does a manual downward `RecruitStage` write stick**, or does the engine ratchet it back on advance?
   ([1SRC] claims ratchet; unproven for manual writes.) `[web §5a]`
3. **Chunk-2 corruption**: does stock `save()` overflow the chunk-1 slot for our (larger) write-sets? Test:
   save a heavily edited copy, byte-compare the region after `0x52 + u32@0x4A`. `[web §1a]`
4. **In-window JobOpening lifecycle**: capture a save during season weeks 15–20 to see live rows
   (Team/PrevCoach/SelectedCoach populated, `IsCarouselPeriodActive` presumably true, IsEmergent behavior),
   and confirm the exact tick openings are created/freed. `[diff remaining-unknowns]`
5. **When do Transfer_\* recruits materialize** in the Recruit table / `Franchise.Transfers`? (Week 1 of the
   portal stage had zero.) Needs a portal-stage week-2+ save. `[diff §3]`
6. How do **ContractEnding** departures appear in `CoachTransactionHistoryEntry` (same PendingFire pattern as
   Fired, separate pattern, or absent)? `[dump §6]`
7. Does the engine read **`OfferIndex` or array slot order** when resolving carousel hires? (Matters only if
   we ever edit offers.) `[schema §13]`
8. **`Coach.AlmaMater` id space**: schema range 1100–1300 vs raw reads 88/42/67/103 — is true value
   `raw + 1100`, and what does it index? `[schema §5][dump §5]`
9. Is **`Team.CommittedPlayers`** rebuilt by the engine weekly, or must we maintain it? `[schema §3]`
10. Engine tolerance for **`TeamInterestInStaffPerson` > 100** (schema max 280) and
    **`NumContractOffers` > 12** — relevant only for carousel-editing extensions. `[schema §13]`
11. Why do 34 carousel movers show `SeasonsWithTeam == 1` (vs 0) right after the carousel — in-season vs
    offseason hire waves? (Affects the `sw <= 1` heuristic's precision.) `[diff §2.2]`
12. Do `Retired` Coach rows persist across seasons or get recycled to FreeAgent/Deleted? `[diff §2.2]`
13. What populates **ProspectTargetSchool pool B** (uid 3843719174; nextRecordToUse 23589 in later saves,
    yet no sampled TopSchoolsList slot points there)? `[diff §3]`
14. Is `UserRecruitTarget` **writable in practice** identically to RecruitTarget (schema says yes)? `[schema §3]`
15. Does zeroing vs preserving **NIL fields** on a hijacked board entry change AI decommit odds for our
    followed recruits? `[fc §4]`
16. **uniqueId stability across game patches** (holds within this build; unproven across builds). `[diff §5]`
17. Are `MySchoolTrackingTable.CoachStabilityGrade/CoachPrestigeGrade` recomputed by the engine each week
    (read-only signal) or writable levers our tool could also adjust? `[dump §8]`
18. `IsEmergentJobOpening` trigger semantics (mid-carousel chain openings?). `[schema §13]`
19. Exact in-window values of `IsCarouselPeriodActive` / `IsStaffHiring*PeriodActive` (inferred true,
    never observed). `[diff §1.3]`
20. `Player.AbsoluteTransferChance` semantics (−1 default; transfer-out propensity?) — relevant if we extend
    consequences to roster players following coaches. `[web §10]`

## Sources

- Docs 01–05 of this suite (and transitively every `research/notes-*.md` + `research/dumps/*` file)
- Architecture patterns: `research/notes-pipeline-tool.md` (§1 write+verify, §3 history DB, §4 Electron),
  `research/notes-carousel-tool.md` (§3 array-write recipe, §4 session lifecycle),
  `research/notes-force-commit.md` (§4 commit recipe, §8 backup/dry-run, §9 save picker),
  `research/notes-jersey-tool.md` (§4 backup naming gotcha)
- Risk/write-safety: `research/notes-madden-franchise-api.md` (§2 empty records, §6 save pipeline),
  `research/notes-web.md` (§1a chunk-2 issue, §5a write-survival semantics, §8 save-editor blueprint)
- Detection evidence: `research/notes-save-dump.md` (§6 CoachTransactionHistoryEntry),
  `research/notes-save-diff.md` (§2 movement signatures, §6 recipes)
