# 04 — Recruiting: Tables, Commitment, Boards, Portal, Pipelines

Documentation suite for **CoachCarouselRecruitTool**. Synthesized 2026-07-14. Source tags as in doc 01
(`[api] [fc] [pipe] [jersey] [carousel] [web] [schema] [dump] [diff]`). UNVERIFIED = not yet proven on a
live save / write test.

---

## Table of contents

1. [Data-model overview](#1-data-model-overview)
2. [The Recruit table](#2-the-recruit-table)
3. [Recruit-linked Player fields & position classification](#3-recruit-linked-player-fields--position-classification)
4. [Top schools: ProspectTargetSchool](#4-top-schools-prospecttargetschool)
5. [How commitment is actually represented](#5-how-commitment-is-actually-represented)
6. [Recruiting boards: RecruitingBoard → RecruitTarget / UserRecruitTarget](#6-recruiting-boards)
7. [Scholarships, NIL, and hard limits](#7-scholarships-nil-and-hard-limits)
8. [Motivations, dealbreakers, pitches](#8-motivations-dealbreakers-pitches)
9. [MySchoolTrackingTable — the school grades recruits check](#9-myschooltrackingtable)
10. [Pipelines](#10-pipelines)
11. [The transfer portal](#11-the-transfer-portal)
12. [The force-commit mutation recipe (proven writes)](#12-the-force-commit-mutation-recipe)
13. [Write-survival semantics (community-tested)](#13-write-survival-semantics)
14. [Gotchas checklist](#14-gotchas-checklist)
15. [Sources](#sources)

---

## 1. Data-model overview

Reference graph (all edges schema-verified; `[schema §12]`, live-verified where noted):

```
Franchise ─┬─ RecruitingClassPlayers : Recruit[]     ← the HS/JUCO class
           └─ Transfers : Recruit[]                  ← THE TRANSFER PORTAL master list

Recruit ─┬─ Player → Player row (recruit prospects share the Player table; TeamIndex = 255)
         └─ TopSchoolsList → ProspectTargetSchool[] row (10 slots)
                └─ each slot → ProspectTargetSchool { TeamId (=Team.TeamIndex), TeamInfluence }

Team ─┬─ RecruitingBoard → RecruitingBoard row ─ Recruits → RecruitTarget[] row (35 slots)
      │      └─ slots → RecruitTarget rows (AI schools) or UserRecruitTarget rows (the human school)
      ├─ CommittedPlayers : Player[]
      ├─ MySchoolTrackingTable → grades (CoachPrestigeGrade, CoachStabilityGrade, …)
      └─ SchoolPipelineInfluenceList → SchoolPipelineInfluence[] → 10 pipeline rows

RecruitTarget ─ Recruit → Recruit    (board rows point AT recruits; not vice versa)
ProspectTargetSchool.TeamId ──(int join)── Team.TeamIndex   (NOT the Team row index)
```

## 2. The Recruit table

`Recruit`, uniqueId **1873209313**, tableId 4269, cap 7600, 4101 non-empty (row 0 = `Invalid` sentinel —
skip it). Unique by name. `[dump §1,§2][diff §3]`

### 2.1 Fields (15; bits from live offset table)

| Field | Type | Bits | Range | Notes |
|---|---|---|---|---|
| `TopSchoolsList` | ref → ProspectTargetSchool[] | 32 | | offset 0 (⇒ destroyed on freed rows) |
| `Player` | ref → Player | 32 | | validate `ref.tableId === playerT.header.tableId` |
| `RecruitStage` | enum RecruitStage | 4 | | §2.2 |
| `RecruitStageAdvance` | enum RecruitStageAdvance | 3 | | §2.3 |
| `Class` | enum RecruitingClass | 4 | | §2.4 |
| `CommitScore` | int | 10 | 0–1023 | commitment threshold; same scale as TeamInfluence |
| `NationalRank` | int | 13 | 0–4500 | 1 = best |
| `PositionRank` / `StateRank` | int | 12 | 0–4000 | |
| `ProductionGrade` | int | 7 | 0–127 | obs ~84–92 |
| `TotalScholarshipOffers` | int | 6 | 0–63 | NOT a lifetime counter (Signed recruits can read 0) |
| `QualityModifier` | enum GemBust | 3 | | NORMAL=0, GEM=1, BUST=2, HIDDEN=3, Invalid=4 |
| `AlternatePosition1/2` | enum DraftPositionE | 5 | | def Invalid_ |
| `SurnameAudioID` | int | 15 | 0–32767 | |

`[dump §2][schema §3]` Real rows (QQ): row 512 = `{Top10, HighSchool, NationalRank 1821, CommitScore 519,
QualityModifier BUST, TopSchoolsList → PTS[]#1820, Player → Player#8762}`. `[dumps/QQ/Recruit sample]`

### 2.2 RecruitStage enum (4 bits) — the commitment state machine

Raw-value map (formatted names; aliases share raws): `[dump §2]`

```
First_/Top10 = 0   Top5 = 1   Top3 = 2   Battle = 3
SoftCommitted = 4  HardCommitted = 5   Last_/Signed = 6   Count_ = 7   Invalid = 8
```

- **There is NO 'uncommitted/None' literal — `Top10` (raw 0) IS the uncommitted base state.** Every recruit
  in a fresh class reads `Top10`. A decommit writer sets the stage back to `'Top10'`/`'Top5'`/`'Top3'`
  (Top3 keeps the old school prominently re-recruitable). `[dump §2,§14]`
- Live distribution at the portal stage (QQ2): Top10 1965, Top5 363, Top3 154, Battle 29, SoftCommitted 27,
  HardCommitted 5, Signed 1557, Invalid 1. `[dumps/QQ2/scan.json]`
- Stage progression thresholds (leader TeamInfluence as % of CommitScore): Top5 at 50, Top3 at 75, Commit at
  100 (`RecruitingTunables.PointsPercentageThresholdCommit/Top3/Top5` = 100/75/50) `[schema §3]`. (The
  brooksg357 docs quote Top5 at 35% `[web §5a, 1SRC]` — the schema default is 50; trust the save's tunables.)
- Stage school-count caps: Top10=10, Top5=5, Top3=3, Battle=2, SoftCommitted=1, HardCommitted=0, Signed=1. `[schema §3]`
- **[1SRC]** `RecruitStage` is a *ratchet* — the engine never recomputes it downward; whether a manual
  downward write sticks in-game is UNVERIFIED (top open question for our decommit feature). `[web §5a,§9]`

### 2.3 RecruitStageAdvance enum (3 bits) — one-shot engine commands

```
First_/None = 0   Advance = 1   Decommit = 2   InstantCommit = 3 (Last_)   Count_ = 4   Invalid = 5
```

- **`Decommit` (2) exists as a first-class engine action.** Live resting values: `Invalid` on ~99% of rows
  (default parked state), a few `Advance`/`None` linger — the field is a one-shot command consumed on week
  advance, then left at Invalid/None. Writing `'Decommit'` on a committed recruit is the top candidate for a
  native decommit (behavior UNVERIFIED — needs a scratch-save write test). `[dump §2,§14][schema §10]`
- force-commit writes `'InstantCommit'` here as part of its proven commit recipe (§12). `[fc §4]`

### 2.4 RecruitingClass enum (4 bits)

```
HighSchool = 0   JuniorCollege_Sophomore = 1   JuniorCollege_Junior = 2   JuniorCollege_Senior = 3
Transfer_Freshman = 4   Transfer_Sophomore = 5   Transfer_Junior = 6   Transfer_Senior = 7   Count_ = 8   Invalid = 9
```

Observed (QQ2): HighSchool 3740, JUCO 219/141/1, **zero Transfer_\*** at portal-stage week 1 — see §11.
Schema default is `Transfer_Junior`, so a blank row is NOT a valid HS recruit (set Class + RecruitStage +
StageAdvance when minting rows). `[dump §2][schema §14]`

## 3. Recruit-linked Player fields & position classification

Recruits live in the shared `Player` table (uid 1612938518; 16,257/16,500 rows; 282 live fields). A recruit's
Player row has `TeamIndex = 255`. `[dump §9]`

### 3.1 Recruiting-relevant Player fields

| Field | Type / bits | Live notes | Source |
|---|---|---|---|
| `Position` | enum PositionE 6b | depth-chart names (ROLB etc.) DO appear on real rows | `[dump §9]` |
| `ProspectStarRating` | enum ProspectQuality 3b {ONE_STAR=0 … FIVE_STAR=4, Invalid=6} | | `[schema §10]` |
| `SchoolYear` | enum 3b {Freshman=0, Sophomore=1, Junior=2, Senior=3, Invalid_=5} | | `[jersey §1][schema §9]` |
| `RedshirtStatus` | enum 2b {Eligible=0, Ineligible=1, Current=2, Previous=3} | | `[jersey §1]` |
| `RecruitingDealbreaker` | enum RecruitingMotivationType 4b | live: ProximityToHome, CoachPrestige, PlayingStyle, BrandExposure, ChampionshipContender, ProPotential; `Invalid` on veterans | `[dump §9]` |
| `IdealRecruitingPitch` | enum RecruitingPitchType 5b | live: Grassroots, Aspirational, HometownHero, TimeToGetToWork; `Invalid` on veterans | `[dump §9]` |
| `Motivation1/2/3` | enum MotivationType 4b | **reads back as raw `'0000'` on every sampled row** (stored value matches no member) — handle raw-bits fallback | `[dump §9,§15]` |
| `HomePipeline` | enum Pipeline 6b | state names + sub-state regions (Tidewater, SouthFlorida) + Invalid | `[dump §9]` |
| `PLYR_HOME_STATE` | enum StateName 6b (Alabama=0…Wyoming=49 alphabetical, NonUS=50, INVALID=51) | | `[schema §9,§10]` |
| `IsNIL` | bool | real-likeness flag; NIL rows can carry odd data (Age 0) | `[jersey §1][dump §9]` |
| `BaseNILValue` | s_int 11b −255–1023 | can be negative (obs −20) | `[schema §9][dump §9]` |
| `CurrentNILCompensation` | int 10b | | `[schema §9]` |
| `AbsoluteTransferChance` | int 7b −1–100, def −1 | UNVERIFIED: transfer-out propensity lever | `[schema §9][web §6]` |
| `PLYR_PREVTEAMID` | int 11b 0–2047 | | `[schema §9]` |
| `TraitDevelopment` | enum 3b {Normal=0, College_Impact=1, College_Star=2, College_Elite=3, Hidden=4, Invalid_=5} | | `[schema §9]` |
| `OverallRating` | int 7b 0–100 | **calculated, not authoritative** (stored 80 vs displayed 84 example) `[web §5b, 1SRC]` | |
| 52 `*Rating` fields | int | exact list enumerated in RO27's settings artifact | `[web §3, CODE]` |

### 3.2 Offense/defense classification (for the OC-change/DC-change decommit rule)

PositionE (6-bit) full domain `[jersey §5][schema §10]`:

```
OFFENSE  (0–9):  QB=0 HB=1 FB=2 WR=3 TE=4 LT=5 LG=6 C=7 RG=8 RT=9
DEFENSE (10–18): LE=10 RE=11 DT=12 LOLB=13 MLB=14 ROLB=15 CB=16 FS=17 SS=18
SPECIAL (19–21): K=19 P=20 LS=21
PSEUDO  (22–34): KR PR KOS 3DRB GAD PWHB SLWR RLE RRE RDT NT SUBLB SLCB   ← classify explicitly if seen
COACH   (35–38): HC_CFM OC_CFM DC_CFM Owner_CFM   (CFM contexts only — NOT Coach.Position)
Invalid_ = 63
```

- Numeric rule: `0–9` offense, `10–18` defense, `19–21` special teams. Depth-chart-style names (ROLB) appear
  on real Player rows, so build the classifier over the full name set, not just "QB/RB/WR". `[dump §9]`
- The jersey tool's name sets (verbatim): OFFENSE `QB HB FB WR TE LT LG C RG RT`, DEFENSE
  `LE RE DT LOLB MLB ROLB CB FS SS`. `[jersey §2]`
- force-commit's position→team-rating-group map (`POSITION_TO_RATING_GROUP`) is another ready mapping. `[fc §10]`

## 4. Top schools: ProspectTargetSchool

- Chain: `Recruit.TopSchoolsList` → `ProspectTargetSchool[]` row (uid 2332540366; fields
  `ProspectTargetSchool0..9`, 10 slots) → `ProspectTargetSchool` rows (pool A uid 3789266353), each exactly
  two fields: **`TeamId: int 11b 0–2047` (= Team.TeamIndex, NOT the Team row) and
  `TeamInfluence: int 16b 0–65535`** (schema widths; live values use the 0–1023 CommitScore scale). `[dump §3][schema §3]`
- 41,000 of 41,010 pool-A rows used = 4,100 recruits × 10 — **every recruit always has 10 populated slots**.
  A second pool (uid 3843719174, cap 35,000) exists; no sampled slot pointed into it (purpose UNVERIFIED).
  Validate slot refs against the resolved table's tableId. `[dump §3][diff §3]`
- **Slots are sorted by TeamInfluence descending; slot 0 = current leader / committed school.** `[dump §3]`
- Real examples `[dumps/QQ2/commit-analysis.json]`:
  - Signed CB Jordan Jacobs (Recruit row 5): CommitScore 431; slot0 = TeamId 16 "California" influence
    **561**; slots 1–9 all influence 0 (zeroed once signed).
  - Uncommitted Top10 WR Uriah Rogers (row 7): CommitScore 459; influences 67, 61, 61, 56, 55, 54, 54, 54,
    52, 51 (dense, sorted).
  - Signed WR Pete Sails IV: CommitScore 771, slot0 TeamId 4 (Arizona State) influence 1000. `[diff §3]`
- **[1SRC]** UI reads physical slot order, not influence order — after any edit **re-permute the 10 slots to
  influence-descending** or the UI misrenders. `[web §5a]`

## 5. How commitment is actually represented

Empirically verified binding (task-critical): `[dump §4][diff §3]`

> A recruit is committed to school S iff `RecruitStage ∈ {SoftCommitted, HardCommitted, Signed}` **and**
> `TopSchoolsList` slot 0's `TeamId == S.TeamIndex`.

Supporting evidence and non-indicators:

- Every sampled Signed/HardCommitted recruit's slot-0 school matches; Signed recruits show
  `slot0.TeamInfluence ≥ CommitScore` (561 ≥ 431; 1000 ≥ 771). `[dump §4][diff §3]`
- **Board entries are NOT authoritative**: a SoftCommitted recruit had zero RecruitTarget rows anywhere;
  HardCommitted recruits still sat on 2 different schools' boards; the winning school's board entry can read
  `ScholarshipStatus='None'`; `CommittedWeekNumber` is 0 on most committed entries. `[dump §4]`
- **Signed ≠ rostered**: all 400 checked Signed recruits' `Player.TeamIndex` = 255 — roster assignment
  happens at season rollover. `[dump §4]`
- `TotalScholarshipOffers` is not a lifetime counter (Signed recruit with 0). `[dump §4]`
- A second, denormalized commit signal exists: `Team.CommittedPlayers: Player[]` — keep it in sync when
  decommitting (UNVERIFIED whether the engine rebuilds it weekly). `[schema §3]`
- By the portal stage most of the signed class's Recruit rows are already **freed** (nextRecordToUse 5064 →
  488); the 106 remaining Signed rows still carry full data. `[diff §3]`

## 6. Recruiting boards

Chain (live-verified): `Team.RecruitingBoard → RecruitingBoard row → .Recruits → RecruitTarget[] row →
RecruitTarget0..34 → RecruitTarget rows`. `[dump §4]`

- **RecruitingBoard** (uid 220276943, 138 rows): `Recruits: ref`, `RecruitingHoursTotal/Assigned/Processed`
  int 12b 0–4095 (350 total at preseason). `[dump §4][schema §3]`
- **RecruitTarget[]** (uid 2412159097): 138 rows × **35 slots**. Row index empirically equals the team's
  TeamIndex, but that's an allocation coincidence — **follow `Team.RecruitingBoard`** (the schema-sanctioned
  route). At preseason every row has `arraySize=0` — **boards are wiped between cycles**. `[dump §4][schema §3]`
- **RecruitTarget** (uid 59043175, cap 4870; 4795 live mid-cycle, 0 otherwise) — 20 fields:

| Field | Type / bits | Notes |
|---|---|---|
| `Recruit` | ref Recruit | empty/dangling ⇒ slot reusable ("hijackable") `[fc §3.4]` |
| `ProspectInfluenceTotal` | int 10b 0–1023 | **the team-side MASTER influence store** `[web §5a, 1SRC]`; matches slot-0 TeamInfluence for winners (561/561) `[dump commit-analysis]` |
| `ProspectInfluenceTotalLastWeek` | int 10b | |
| `ProspectInfluenceDelta` | s_int 11b −200–1023 | can be negative |
| `ProspectHoursSpentCurrent` | int 7b | |
| `ScholarshipStatus` | enum 3b | §7.1 |
| `CommittedWeekNumber` | int 5b 0–31 | usually 0 even for committed entries |
| `CurrentNILOffer` / `NILExpectation` | int 10b 0–1023 | |
| `OriginalNILExpectation` | int 9b 0–510 | |
| `CurrentScholarshipBonus` | s_int 8b **−200–50** | negative legal |
| `SearchSocialMedia` / `ContactHighSchoolCoaches` / `ContactFriendsAndFamily` / `SendTheHouse` / `VisitRecruitsSchool` | bool | weekly action flags |
| `ActivePitches` | ref ActiveRecruitingPitch[] | pool can be exhausted `[web §5a, 1SRC]` |
| `SwayPitch` | enum RecruitingPitchType 5b | |
| `ScheduledVisit` | ref ActiveVisitInfo | |
| `UnlockedIntelBitfield` | int 14b | maps to the 14 motivations |

  `[dump §4][schema §3]`
- **UserRecruitTarget** (uid 3987156317, cap 1120 = 32 users × 35) — subclass: all 20 fields **plus**
  `IsFavorite: bool`, `RecruitingFeedback` + `ImmediateRecruitingFeedback` (refs → RecruitingActionFeedbackEntry[]).
  Confirmed live: the user school's RecruitTarget[] row (row 43 = LSU) holds 31 refs into UserRecruitTarget.
  This is why force-commit flags boards with foreign-table refs as "user school". Structurally editable like
  RecruitTarget (write test UNVERIFIED). `[dump §4][schema §3][fc §3.4]`
- Real linked rows `[dumps/QQ2/commit-analysis.json]`: Signed recruit row 5 ← RecruitTarget row 4146 on board
  16 (California): `{ScholarshipStatus: None, CommittedWeekNumber: 0, ProspectInfluenceTotal: 561,
  CurrentNILOffer: 0, NILExpectation: 55}`; Signed row 11 ← RT row 854 on board 4 (Arizona State):
  `{Offered, CommittedWeekNumber: 13, ProspectInfluenceTotal: 1000, CurrentNILOffer: 200}`.

## 7. Scholarships, NIL, and hard limits

### 7.1 ScholarshipStatus enum (3 bits)

```
First_/None = 0   Revoked = 1   New = 2   Offered = 3   Committed = 4 (Last_)   Count_ = 5   Invalid = 6
```
Live: AI boards show only None (2031) / Offered (2764); **`Committed` was never observed**, even on Signed
recruits' winning entries — commit truth lives on the Recruit side (§5). `Revoked`/`New` unobserved. `[dump §4]`

### 7.2 Limits & tunables (defaults; league-editable in save)

| Limit | Value | Source |
|---|---|---|
| Board size (`MaxRecruitingBoardTargets`, RecruitTarget[] slots) | **35** | `[web §5a][dump §4]` |
| `MaxTeamScholarshipOffers` | 35 | `[web §5a, 1SRC]` |
| `Recruit.TotalScholarshipOffers` field cap | 63 (6 bits) | `[schema §3]` |
| Recruits generated per class | ~4,100 + 1 sentinel (`RecruitsToGenerateMin/Max` 4100) | `[web §5a][dump §1]` |
| `MaxFiveStarsInClass` | 32 | `[web §5a, 1SRC]` |
| Recruiting hours | weekly flow, `RecruitingBoard.RecruitingHoursTotal` (obs 350); refill by prestige × phase splines, 50 h/recruit/week cap **[1SRC]** | `[dump §4][web §5a]` |
| Program points | seasonal stock: `Team.ProgramPointBudget` 0–30000 (obs 1250), `RemainingProgramPoints` −6000–30000, spent buckets (`NILProgramPointsSpent` obs 575) | `[schema §6][dump Team sample]` |
| Commit thresholds | `PointsPercentageThresholdCommit`=100 / Top3=75 / Top5=50 of CommitScore; `HardCommitAdditionalCommitScorePercentage`, `BattleAdditionalCommitScorePercentage` | `[schema §3]` |
| Instant commit | `InstantCommitOddsPerStarLevel: int[]`, `InstantCommitBonusPrestige: int[]` | `[schema §3]` |
| CPU AI | `AIAggressivenessSlider` 1.15, `AINILAggressivenessSlider` 2.25, `CPU_AI_MaxPitchCount` 2 **[1SRC]** | `[web §5a]` |
| League toggle | `LeagueSetting.IsRecruitFlippingEnabled: bool` (affects flipping committed recruits — relevant to our decommit/re-recruit design) | `[dump §12]` |

## 8. Motivations, dealbreakers, pitches

### 8.1 RecruitingMotivationType (4 bits) — the 14 recruiting motivations / dealbreakers

```
AcademicPrestige=0  AthleticFacilities=1  BrandExposure=2   CampusLifestyle=3  ChampionshipContender=4
CoachPrestige=5     CoachStability=6      ConferencePrestige=7  PlayingStyle=8  PlayingTime=9
ProPotential=10     ProgramTradition=11   ProximityToHome=12    StadiumAtmosphere=13 (Last_)  Invalid=15
```
`[schema §10]` — **CoachPrestige(5) and CoachStability(6) are the natural priority targets for
carousel-driven decommits.** `Player.RecruitingDealbreaker` uses this enum (live values confirmed `[dump §9]`).
**[1SRC]** a dealbreaker "fails" when the school's grade in that motivation drops below B−; only 8 of the 14
can be dealbreakers (excluded: AcademicPrestige, AthleticFacilities, CoachStability, PlayingStyle,
ProgramTradition, StadiumAtmosphere). `[web §5a]` (Note: live saves DO show CoachPrestige, ProximityToHome,
PlayingStyle... as dealbreaker values `[dump §9]` — the PlayingStyle sighting contradicts the 1SRC exclusion
list; trust live data.)

### 8.2 MotivationType (Player.Motivation1–3) — a DIFFERENT enum

Pro/Madden-style members (None=1, NoIncomeTax=2, WarmWeatherState=3, BigMarket=4, ChampionshipContender=5,
TeamPrestige=6, SchemeFit=7, ToptheDepthChart=8, TeamHasFranchiseQB=9, MentoratPosition=10,
HeadCoachHistoricRecord=11, CloseToHome=12). Live reads return raw `'0000'` (no member for stored 0) — any
motivation logic must handle the raw-bits fallback. Do not conflate with RecruitingMotivationType. `[schema §10][dump §9]`

### 8.3 RecruitingPitchType (5 bits) — 20 real pitches

```
CollegeExperience=0 TeamPlayer=1 CampusPersonality=2 ItsGameTime=3 Prestigious=4 StudentOfTheGame=5
HometownHero=6 ProveYourself=7 TheClutch=8 TVTime=9 CoachsFavorite=10 Aspirational=11 ToTheHouse=12
FootballInfluencer=13 TimeToGetToWork=14 Starter=15 Grassroots=16 ConferenceSpotlight=17 SundayBound=18
WorkHorse=19 (Last_)   Count_=20  Invalid=22
```
`[schema §10]` Pitch intensity: `RecruitingActionIntensity` {SoftSell=0, HardSell=1, Sway=2}. `[schema §10]`
force-commit's `DEALBREAKER_TO_PITCH` map (10-pitch subset) pairs each dealbreaker with a pitch — e.g.
CoachStability→TheClutch, ChampionshipContender→TVTime, ProximityToHome→ConferenceSpotlight. `[fc §10]`

### 8.4 Visits & scouting (context)

`ActiveVisitInfo {Activity: VisitActivityType(14 values), WeekNumber, WeekType}`; `MaxRecruitVisitsPerWeek=4`
**[1SRC]**; scouting = `ProspectInteraction` rows (max 5 scouts/recruit; 14-bit intel bitfield maps to the 14
motivations; reveal display driven by `TimesScouted` **[1SRC]**). `[schema §10][web §5a]`

## 9. MySchoolTrackingTable

uid 349376083, 138 rows × 36 fields — per-school data behind the motivation grades recruits check. `[dump §8]`

- Ten motivation grades (LetterGrade): `AcademicPrestigeGrade, AthleticFacilitiesGrade, BrandExposureGrade,
  CampusLifestyleGrade, ChampionshipContenderGrade, `**`CoachPrestigeGrade, CoachStabilityGrade`**`,
  ConferencePrestigeGrade, ProgramTraditionGrade, StadiumAtmosphereGrade` + positional
  `ProPotentialGradeQB/RB/WR/TE/OL/DL/LB/DB/K/P`.
- Raw stats: `AthleticFacilitiesScore` 0–2000 (obs 510), `CampusLifestyleScore`, BrandExposure
  TV/streaming/game-of-week counters, `ChampionshipContenderCurrentYearRank` (+Yr+1..3), PlayingStyle
  grade/rank/stat ref tables.
- **`CoachStabilityGrade` / `CoachPrestigeGrade` are the game's own post-carousel recruiting inputs** —
  example row 0: CoachStabilityGrade=F, CoachPrestigeGrade=D+. Our tool can read (or adjust) these to align
  its decommit odds with what the game itself signals. `[dump §8]`
- Reached only via `Team.MySchoolTrackingTable` ref (row alignment with Team rows is NOT guaranteed — FIU
  team row 35 → MySchool row 30). `[dump §7]`
- Grade→dealbreaker mapping keys (force-commit's `GRADE_TO_DEALBREAKER`): the ten `*Grade` columns map 1:1
  onto RecruitingMotivationType members. `[fc §3.9]`

## 10. Pipelines

- **Pipeline enum** (6 bits, 43 real regions + Invalid=44): Alabama=0, Arizona=1, Arkansas=2, BigApple=3,
  BigSky=4, CentralFlorida=5, Colorado=6, EastTexas=7, Hawaii=8, Illinois=9, Indiana=10, Iowa=11, Kansas=12,
  Kentucky=13, Louisiana=14, MetroAtlanta=15, Michigan=16, Minnesota=17, Mississippi=18, Missouri=19,
  Nebraska=20, Nevada=21, NewEngland=22, NewMexico=23, NorthCarolina=24, NorthFlorida=25, NorthTexas=26,
  NorthernCalifornia=27, Ohio=28, Oklahoma=29, PacificNorthwest=30, Pennsylvania=31, SouthCarolina=32,
  SouthFlorida=33, SouthGeorgia=34, SouthernCalifornia=35, SouthwestTexas=36, Tennessee=37, Tidewater=38,
  Utah=39, WestVirginia=40, Wisconsin=41, International=42. `[schema §7]`
- School side: `Team.SchoolPipelineInfluenceList` → `SchoolPipelineInfluence[]` (10 refs) →
  `SchoolPipelineInfluence {Pipeline, InfluenceLevel, InfluenceValue 0–1000}`; tier enum
  `PipelineInfluenceLevel` {Unrecognized=0 < NicheInterest=1 < Respected=2 < Popular=3 < HouseholdName=4 <
  CulturalPillar=5, Invalid=7}; observed tier cutoffs on the 0–1000 scale ≈ [0, 40, 80, 150, 250].
  `[schema §7][pipe §2]`
- Player side: `Player.HomePipeline` (region enum, not a state code); `PLYR_HOME_STATE` is the finer state
  enum; state→region mapping table in the pipeline tool's `data/stateToPipeline.json` (e.g. Texas→EastTexas,
  NonUS→International). `[pipe §2]`
- Coach side: `Coach.PrimaryPipeline` — a personal pipeline region that travels with the coach between jobs;
  weighted in hiring (`HalfPipelinePoints` 25 / `MaxPipelinePoints` 50) and in the pipeline tool's model
  (HC 0.6 / OC 0.2 / DC 0.2 with tenure ramp `min(1, SeasonsWithTeam/ramp)`). `[pipe §2,§6][schema §7]`
- Tunables: `InfluenceRequiredPerPipelineLevel[]`, `RecruitInitialInfluencePerPipelineLevel[]`,
  `ChanceToSwayBoostFromPipelineInfluenceLevel[]`. `[schema §7]`
- Writes to SchoolPipelineInfluence rows are game-honored (pipeline tool's 980-row write survived a season
  and showed in-game effects). `[pipe intro][diff §4]`

## 11. The transfer portal

- **There is no `TransferPortal` table.** The portal is: `Franchise.Transfers: Recruit[]` (master list) +
  Recruit rows with `Class = Transfer_*` + SeasonInfo period flags + events. `[schema §3][web §6]`
- Portal entrants are seeded by star rating via `TransferStartingRecruitData {MinCommitScore, MaxCommitScore,
  RecruitStage, StarRating}` and `RecruitingTunables.TransferDefaultCommitScore = 1000`. `[schema §3]`
- Events: `AddToTransferPortalEvent{Recruit}`, `EncourageTransfersStart/EndEvent`,
  `SendPlayerToTransferPortalAction`, `ForceTransferAction`; reaction
  `RecruitingEvalTransferAddedToPortalReaction`. `[schema §3][web §6]`
- Team counters: `Team.LastSeasonTransfersLost` (0–30) / `LastSeasonTransfersSigned` (0–35). `[schema §6]`
- **Observed**: at portal-stage week 1 (QQ2: `IsTransferPortalNewlyAvailable` + `IsTransferSignPeriodActive`
  both true) there were **zero Transfer_\* Recruit rows** — UNVERIFIED where/when portal players materialize
  (possibly deeper into stage 2; needs a later-week save). `[dump §2][diff §3]`
- Retention levers on the roster side: `Player.AbsoluteTransferChance` (UNVERIFIED),
  `CoachTalentEffects.PlayersLeaving_LessTransferChance[8]` / `PlayersLeaving_InterestBoost_Transfers[8]` /
  `PlayersLeaving_ThresholdDecrease_Dealbreakers[8]`, `ProgramPoints_DecreaseRiskOfTransfer`;
  `LeagueSetting.CPUPlayerTransferChance` / `UserPlayerTransferChance` sliders. `[web §6][dump §12]`

## 12. The force-commit mutation recipe

The only **proven** commit write-set for CFB27 (from `force-commit-recruits`, run by real users). Verbatim
core (`instantCommit`): `[fc §4]`

```js
// 1. Board side — hijack an existing RecruitTarget row on the school's board (never allocates rows)
W(targetEntry, 'Recruit', makeRef(recruitTableId, recruitRow));
W(targetEntry, 'CommittedWeekNumber', currentWeek);          // SeasonInfo.CurrentWeek
W(targetEntry, 'OriginalNILExpectation', 0);
W(targetEntry, 'CurrentNILOffer', 0);
W(targetEntry, 'NILExpectation', 0);
W(targetEntry, 'ScholarshipStatus', 'Offered');

// 2. Recruit side — stage machine
W(recruitRec, 'RecruitStage', 'HardCommitted');
W(recruitRec, 'RecruitStageAdvance', 'InstantCommit');

// 3. Player side — compatibility sweetener (UNVERIFIED intent: anti-decommit?)
W(playerEntry, 'RecruitingDealbreaker', schoolBestGradeDealbreaker);
W(playerEntry, 'IdealRecruitingPitch', DEALBREAKER_TO_PITCH[dealbreaker]);

// 4. Top-school side — the BINDING: slot 0 of TopSchoolsList
const firstCol = topSchoolsListT.offsetTable.map(o => o.name)[0];   // 'ProspectTargetSchool0'
topSchoolRec.TeamId = boardRow;            // school's TeamIndex
topSchoolRec.TeamInfluence = commitScore;  // = Recruit.CommitScore → satisfies the commit threshold
```

Notes: no rollback on partial failure (earlier writes stick); the tool runs at "week 4 of the transfer-portal
period" per its README; post-run, the game AI may still decommit some forced commits on advance. `[fc §4,§8,§11]`
Divergence worth testing: force-commit writes `ScholarshipStatus='Offered'` while the engine's own enum has a
`Committed` value that live data never shows — 'Offered' matches observed engine behavior. `[schema §10][dump §4]`

An alternative, community-tested commit route **[1SRC]**: make the target team the influence leader
(`RecruitTarget.ProspectInfluenceTotal`), then set `Recruit.CommitScore ≤ leader influence` → HardCommitted
on next weekly advance (rival influence zeroed by the engine). `[web §5a]`

## 13. Write-survival semantics

Community-verified (brooksg357, game-tested 2026-07-07) — all **[1SRC]**, crucial for durable writes: `[web §5a]`

- `RecruitTarget.ProspectInfluenceTotal` (team-side) is the **master store**; the weekly advance re-derives
  the recruit's TopSchoolsList entry from it. List entries with no backing target row persist untouched.
- **Sticky (one-shot) writes**: `Recruit.RecruitStage` (ratchet — never recomputed downward),
  `Recruit.CommitScore`, Player identity/ratings.
- **Transient (re-apply weekly)**: board membership, RecruitTarget rows, ProspectInfluenceTotal,
  ActivePitches, ScheduledVisit, ScholarshipStatus. **The CPU re-curates its board every weekly advance** and
  drops injected 0-influence targets ⇒ durable orchestration needs per-week re-application by an external
  tool (exactly our tool's job).
- `ActiveRecruitingPitch[]` pool can be exhausted (4,830/4,830 observed in a year-2028 save) — recycle
  pitch arrays instead of allocating.

## 14. Gotchas checklist

1. Skip Recruit row 0 (Invalid sentinel); gate every row on `record.isEmpty`. `[dump §2]`
2. RecruitStage has NO uncommitted literal — Top10 is base; `Signed` shares raw 6 with alias `Last_`. `[dump §2]`
3. `RecruitStageAdvance` rests at `Invalid`; it is a one-shot command field. `[dump §2]`
4. Commit truth = Recruit.RecruitStage + TopSchools slot 0. Board entries, `CommittedWeekNumber`,
   `ScholarshipStatus`, `TotalScholarshipOffers` are all unreliable indicators. `[dump §4]`
5. TopSchools slots must stay influence-desc sorted after edits **[1SRC]**. `[web §5a]`
6. `TeamId`/`TeamIndex` ≠ Team-table row; FCS pseudo-teams share TeamIndex 255. Build a TeamIndex→row map
   and expect a collision on 255. `[fc §11][diff §5]`
7. User boards point into `UserRecruitTarget`, not `RecruitTarget` — resolve each slot ref's actual table. `[dump §4]`
8. Boards are wiped between cycles; a brand-new dynasty's preseason has RecruitTarget.nextRecordToUse = 0 —
   a "hijack existing row" strategy has nothing to hijack there. `[diff §3]`
9. RecruitTarget rows are a scarce fixed pool (cap 4870, 4795 used mid-cycle — only ~75 spare). `[fc §11][dump §1]`
10. `Motivation1/2/3` read as raw `'0000'` — handle non-name enum reads everywhere. `[dump §9]`
11. Negative-capable fields: `CurrentScholarshipBonus` (−200..50), `ProspectInfluenceDelta` (−200..1023),
    `BaseNILValue` (−255..1023) — don't clamp at 0. `[schema §14]`
12. Player.readRecords() full-width is slow (16.5k × 282) — always use field projections. `[dump §15]`

## Sources

- `research/notes-force-commit.md` (proven commit recipe, board mechanics, savePicker)
- `research/notes-save-dump.md` + `research/dumps/QQ|QQ2|QQ2b/*.sample.json`, `research/dumps/*/scan.json`,
  `research/dumps/QQ2/commit-analysis.json` (live enums, distributions, commit-binding evidence)
- `research/notes-save-diff.md` + `research/dumps/save-diff.json` (cross-save recruit lifecycle)
- `research/notes-schema.md` + `research/schema-relevant-tables.json` (field defs, enums, tunables)
- `research/notes-web.md` (brooksg357 recruiting docs [1SRC], table-map.json schemas [CODE], RO27 artifacts)
- `research/notes-jersey-tool.md` (PositionE dump, offense/defense sets)
- `research/notes-pipeline-tool.md` (pipeline model, state→region map)
