# T005 — Wired career-progression curve (post-fix)

Goal: `baolife-economy` · Task T005 · Approach B (Judge-approved): wire the real
tier-promotion `job_manager.handleJob` into BOTH weekly ticks (online +
offline), before finances, mirrored.

This is the re-measurement that proves the fix. Before T005, recurring salary was
frozen at the entry tier for an entire career because `job_manager.handleJob`
(the promotion/raise/firing function) was dead code — never called from any live
game loop. See `T003-engaged-curve.md` for the pre-fix flat curves (engaged ==
passive, byte-for-byte).

All figures: start age 18, 160 in-game-year horizon, seed 1 (salary-tier curves
are seed-stable; seed only shifts death age). Captured from
`npx vitest run tests/e2e/economy-curve.test.ts`.

---

## Engaged Accountant (Work Hard) — mid-tier ladder

finalAge=73 · deathCause=natural · errors=0 · invariants=0

| age | salary | money   | netWorth | occupation |
|-----|--------|---------|----------|------------|
| 18  | 0      | 0       | 0        | -          |
| 23  | 2000   | 4793    | 4793     | work       |
| 28  | 3000   | 12038   | 12038    | work       |
| 33  | 3000   | 31940   | 31940    | work       |
| 38  | 3000   | 52090   | 52090    | work       |
| 43  | 3000   | 72093   | 72093    | work       |
| 48  | 3000   | 92295   | 92295    | work       |
| 53  | 3000   | 112498  | 112498   | work       |
| 58  | 3000   | 132648  | 132648   | work       |
| 63  | 3000   | 155350  | 155350   | work       |
| 68  | 0      | 158260  | 158260   | retired    |
| 73  | 0      | 148960  | 148960   | retired    |

- **salaryTierProgression (yearly samples): 1200 -> 2000 -> 3000** (intermediate
  $1600 / $2400 rungs are crossed between annual samples; peak salary $3000 =
  Chief Financial Officer, the top of the Accountant ladder).
- entrySalary=1200 · peakSalary=3000 (first reached age 24).
- **Promotion ages**: reaches the $3000 CFO ceiling by ~age 24-28 (rapid early
  climb with Work Hard +2/wk; thereafter holds at the ladder top).
- **Peak net worth = $163,410 (age 65)** — within the healthy target $150k-$300k.

## Engaged Software Engineer (Work Hard) — highest-ceiling career

finalAge=73 · deathCause=natural · errors=0 · invariants=0

| age | salary | money   | netWorth | occupation |
|-----|--------|---------|----------|------------|
| 18  | 0      | 0       | 0        | -          |
| 23  | 4000   | 5973    | 5973     | work       |
| 28  | 6000   | 21333   | 21333    | work       |
| 33  | 6000   | 54938   | 54938    | work       |
| 38  | 6000   | 88738   | 88738    | work       |
| 43  | 6000   | 122443  | 122443   | work       |
| 48  | 6000   | 156348  | 156348   | work       |
| 53  | 6000   | 190253  | 190253   | work       |
| 58  | 6000   | 224053  | 224053   | work       |
| 63  | 6000   | 260458  | 260458   | work       |
| 68  | 0      | 268828  | 268828   | retired    |
| 73  | 0      | 259528  | 259528   | retired    |

- **salaryTierProgression (yearly samples): 2000 -> 4000 -> 6000** (entry Junior
  $2000 -> Senior $4000 -> Software Engineering Manager $6000).
- entrySalary=2000 · peakSalary=6000 (first reached age 27).
- **Promotion ages**: climbs through the ungated rungs by ~age 23-27.
- **Stalls at $6000 (Manager)** — the CTO rung ($10000) is prestige-gated
  (minPrestige 300) AND intelligence-floored (75). The sim never builds prestige,
  so the worker correctly holds just below the elite ceiling (gate working as
  designed). With prestige/intelligence cleared this would climb toward $10000.
- **Peak net worth = $273,978 (age 65)** — within the healthy target $250k-$600k
  (the lower end, since CTO is gated off in this seed).

## Passive Accountant (no focus) — control

finalAge=73 · deathCause=natural · errors=0 · invariants=0

| age | salary | money   | netWorth | occupation |
|-----|--------|---------|----------|------------|
| 18  | 0      | 0       | 0        | -          |
| 23  | 1200   | 4492    | 4492     | work       |
| 33  | 1200   | 18836   | 18836    | work       |
| 43  | 1200   | 42577   | 42577    | work       |
| 53  | 1200   | 66468   | 66468    | work       |
| 63  | 1200   | 92909   | 92909    | work       |
| 68  | 1600   | 102605  | 102605   | retired    |
| 73  | 1600   | 113585  | 113585   | retired    |

- **salaryTierProgression: 1200** (creeps to $1600 only very late via the neutral
  random-walk — one occasional promotion, no ladder).
- entrySalary=1200 · peakSalary=1200 (working-life peak).
- **Peak net worth = $97,693 (age 65)** — low, NOT exploding (only a slow creep,
  exactly as required for the no-focus control).

---

## Verdict

**Late-game money is now MEANINGFUL and BOUNDED.**

- Meaningful: an engaged worker climbs a real salary ladder
  (Accountant 1200->3000, SWE 2000->6000) and accumulates **$163k-$274k** net
  worth — comfortably into mid-tier shop territory, versus the passive control's
  flat ~$98k. Engaged net worth is **1.67x-2.8x** the passive peak.
- Bounded: highest observed peak is **$273,978**, far under the **$5,000,000**
  hard ceiling. The passive control still only creeps (~$98k peak), and elite top
  rungs (CTO $10000) remain prestige/intelligence gated, so the curve cannot run
  away.

## What was wired (and the one tiny job_manager tweak)

- **Online** — `PlayerSession.processWeekTick`: call `handleJob(player, player.c)`
  BEFORE `applyWeeklyFinances(player.c)`. Promotion/firing messages flow through
  the existing `player.messageQueue` -> `messageEvent` drain in
  `processHourTick` (no new channel invented).
- **Offline** — `GameEngine.handleWeeklyUpdates`: import the real promotion fn
  under an alias `processJobProgression` (= `job_manager.handleJob`) to avoid
  colliding with GameEngine's private happiness-only `handleJob`, and call it for
  the player character BEFORE `handleFinances`. The legacy happiness nudge is
  kept (harmless). Messages accumulate on `player.messageQueue`, delivered on
  reconnect via the offline loop drain — same pattern.
- **Mirror parity**: identical function (`job_manager.handleJob`), identical
  ordering (career progression BEFORE finances), identical message surfacing
  (`player.messageQueue`).
- **Tiny job_manager.ts fix (required)**: post-promotion performance reset
  changed `0 -> 50` (the fresh-hire baseline `setJob` uses). Resetting to 0
  instantly satisfied the `< 10` termination check on the next weekly tick,
  firing the worker right after every promotion — a promote-then-fire sawtooth
  that prevented any ladder. With baseline 50 the worker climbs the next rung
  instead of being fired off. No salary tiers, finance math, or stats_manager
  constants were touched.
