# Events & Life Progression Audit

**Date:** 2026-02-07
**Auditor:** events-auditor (automated static analysis)
**Scope:** Backend event system (server/src/events/, handlers/, game/engine/), iOS event handling, life progression mechanics

---

## Executive Summary

The event system has **critical architectural issues** that prevent most events from working correctly at runtime. The biggest problems are:

1. The active game loop (`PlayerSession`) is missing core progression mechanics (age, death, intraday planning)
2. Five entire event categories are not wired into `allEvents` and can never fire
3. Question answer arguments are passed in the wrong order, causing most answer handlers to silently do nothing
4. Non-event helper functions leak into `allEvents` and can produce invalid payloads

**Total issues found: 29** (P0: 6, P1: 12, P2: 7, P3: 4)

---

## P0 - Broken (Must Fix)

### P0-1: Question answers passed in wrong argument slot

**Impact:** Most question answer handlers silently do nothing -- stats/relationship updates are skipped.

- `handleQuestionEvent` calls: `eventFn(player, 'answer', undefined, response)` at `server/src/handlers/events.ts:183`
- `handleGenericEvent` calls: `eventFn(player, 'answer', key, message)` at `server/src/handlers/events.ts:315`
- Most event functions expect `response` as the **3rd argument**, not 4th. Example: `firstCrush` at `server/src/events/adolescence/social.ts:29` checks 3rd arg at `:64`
- The `undefined` in position 3 of `handleQuestionEvent` means the response data is passed as the 4th arg where no event reads it

### P0-2: allEvents includes non-event helper functions

**Impact:** Event loop can emit invalid payloads (arrays, booleans, strings) instead of event objects.

- `randomRelationshipEvents` is spread into `allEvents` at `server/src/events/index.ts:86`
- This includes helper functions that return non-event values:
  - `processWeeklyRelationshipEvents` returns an array (`server/src/events/relationships/randomEvents.ts:225`)
  - `hasActiveRomance` returns boolean (`server/src/events/relationships/randomEvents.ts:249`)
  - Other helpers return string/null (`server/src/events/relationships/randomEvents.ts:258`)
- `checkEvents` accepts any truthy return (`server/src/stats/stats_manager.ts:165`)
- `PlayerSession` then sends it as if it were a valid event (`server/src/game/PlayerSession.ts:140-144`)

### P0-3: Active game loop missing age progression

**Impact:** Characters never age during live gameplay. All age-gated events, birthday logic, tutorial timing, and death-by-age are broken.

- Active loop is `PlayerSession` (`server/src/game/PlayerSession.ts:24, :90, :149`) -- it never calls the age updater
- Age update logic exists at `server/src/stats/stats_manager.ts:505` but is only used in `LoopManager` (unused code path)
- `ageHours`, `ageDays`, `ageYears` never advance during live play

### P0-4: School-year transition system is disabled

**Impact:** Elementary/middle/high/college transitions via date-based progression never fire.

- `allEvents` does not include `school_year` events (`server/src/events/index.ts:76`)
- The school_year package exists and is well-implemented (`server/src/events/school_year/transitions.ts:51`)
- `checkEvents` (`server/src/stats/stats_manager.ts:156`) only iterates `allEvents`, so school_year is unreachable

### P0-5: Five event categories entirely missing from allEvents

**Impact:** Entire categories of events can never trigger from the normal event scanning loop.

Missing from `allEvents` at `server/src/events/index.ts:76`:
| Category | Files | Estimated Events |
|----------|-------|-----------------|
| `activities/` | 9 files | ~70 events |
| `social/` | 2 files | ~4 events |
| `school_year/` | 2 files | ~25 events |
| `dilemmas/` | 2 files | ~12 events |
| `conversations/` | 9 files | N/A (different architecture) |

### P0-6: dayOfYear never wraps in PlayerSession

**Impact:** After 365 ticks of daily progression, all date-dependent checks (SAT timing, school start/end, holidays) become permanently broken.

- `dayOfYear` increments unbounded at `server/src/game/PlayerSession.ts:69`
- Intended wrapping logic exists in `GameEngine.ts:375` but is not used by PlayerSession
- Breaks: SAT trigger (`server/src/events/education/tests.ts:56`), school counters (`server/src/game/PlayerSession.ts:197`), holiday matching

---

## P1 - Significant Bugs

### P1-1: Dilemma resolution path depends on allEvents, but dilemmas are missing

- Resolver lookup at `server/src/stats/stats_manager.ts:118` searches `allEvents`
- Dilemmas are not in `allEvents` (`server/src/events/index.ts:76`)
- Automated resolver also calls with `response = undefined` (`server/src/stats/stats_manager.ts:121`), but handlers require `response?.option` (`server/src/events/dilemmas/moral_choices.ts:118, :192`)

### P1-2: Family answer handlers exist but are not wired into dispatch

- Answer handler mapping exists at `server/src/events/family/index.ts:107`
- Dispatcher never uses that mapping; generic handler just calls same event ID (`server/src/handlers/events.ts:311, :315`)
- Example: question function at `server/src/events/family/activities.ts:139`, separate answer function at `:175` -- the answer function is never called

### P1-3: Occupation state mismatch blocks many career events

- Job manager sets `occupation = occupation.title` (the actual job title) at `server/src/services/jobs/job_manager.ts:539`
- Many career events require `occupation === 'work'`:
  - `server/src/events/negative/career.ts:28`
  - `server/src/events/adulthood/lifeEvents.ts:41`
- If jobs come from the normal job manager flow, these events are filtered out

### P1-4: EventRegistry design bug -- non-age conditions drop events

- Events with only `requiresJob`/`requiresRelationship`/`dayOfYear` (no `ageRange`) are never indexed
- Age index only built on `ageRange` (`server/src/game/engine/EventRegistry.ts:71`)
- "No conditions" bucket excludes events with any condition (`EventRegistry.ts:79`)
- Query only scans `_byAge` and `_noConditions` (`EventRegistry.ts:103, :123`)
- Also: no `registerEvent()` call sites exist outside the definition (`EventRegistry.ts:165`), so the registry is effectively unused

### P1-5: Education state model inconsistency

- `setEducation` sets college occupation to `'school'` (`server/src/services/education/education_manager.ts:750`)
- School-year/education events expect `'student'` (`server/src/events/school_year/transitions.ts:248`, `server/src/events/education/schoolLife.ts:12`)
- Age 17 maps to `'high_school'` in education manager (`:610`) but transitions/tests use grade strings like `'12th'` (`:217`, `tests.ts:11`)

### P1-6: Education progression can jump unrealistically

- `setEducation` for adults may jump directly to `associate_degree`, `college yr 3`, `bachelors_degree`, or `doctorate_degree` based on age/randomness
- Refs: `server/src/services/education/education_manager.ts:755-761`

### P1-7: Intraday location updates not executed on active path

- Many school events gated by `location.includes('school')` (`server/src/events/education/schoolLife.ts:29`, `tests.ts:16`)
- Default location is `'home'` (`server/src/models/Person.ts:383`)
- Intraday planner only called in `LoopManager` (`server/src/game/engine/LoopManager.ts:230, :332`), not in `PlayerSession`

### P1-8: Tutorial timing is broken for most new-player paths

- Tutorial mode depends on `ageHours < 24` (`server/src/services/retention/tutorial.ts:191`)
- `ageHours` is not advanced in active loop (see P0-3)
- Default setup age is 15 (`server/src/services/character/character_manager.ts:838`), making `ageHours` derive from `ageDays` (`server/src/models/Person.ts:360`) -- this effectively disables tutorial mode immediately

### P1-9: Death flow incomplete on active runtime path

- Death handlers exist (`server/src/services/health/health_manager.ts:202`)
- Engine loop handles dead state (`server/src/game/engine/LoopManager.ts:202`)
- PlayerSession does not check `player.c.status === 'dead'` and does not run daily death calculations (`server/src/game/PlayerSession.ts:44, :149`)

### P1-10: Message-event claim flow is placeholder (no server-side cost/reward application)

- `handleClaimEvent` explicitly acknowledges it is placeholder (`server/src/handlers/retention.ts:301, :304`)
- Negative-event consequences that rely on event cost metadata are not enforced server-side
- Client does optimistic deduction but server never validates

### P1-11: iOS question queue uses removeLast() which may not match the displayed question

- `EventModalView.handleAnswerSelection` does `questionQueue.removeLast()` (`ios/.../EventModalView.swift:370`)
- Questions are appended, so the last item is the newest, but the modal displays `currentQuestion` which may be set from a different position
- If questions arrive rapidly, the wrong question could be removed

### P1-12: iOS sendAnswer sends answer as entire AnswerOption object, not just the selection

- `sendAnswer` encodes the full `AnswerOption` struct as JSON (`ios/.../WebSocketService.swift:275-278`)
- Server expects `response.option` for answer text matching, but receives the full object with `option`, `id`, `data`, `energyCost`, `moneyCost`, `diamondCost`
- Combined with P0-1 (wrong argument slot), answer processing is doubly broken

---

## P2 - Minor Issues

### P2-1: Duplicate event IDs silently overridden by spread order

- `agingParent` exists in both `adulthood` (`server/src/events/adulthood/lifeEvents.ts:160`) and `family` (`server/src/events/family/lifeTransitions.ts:31`)
- `allEvents` spreads adulthood before family (`server/src/events/index.ts:81, :85`), so family wins silently

### P2-2: Probability outlier makes extracurricular event effectively dead

- `checkProbability(1_000_000)` at `server/src/events/education/activities.ts:14`
- In age window 13-17, this event has negligible chance of ever firing
- `checkProbability` has no guard for invalid divisors (`server/src/events/base.ts:336`)

### P2-3: Events use player.relationships snapshot vs authoritative player.r

- Snapshot initialized at `server/src/models/Player.ts:280`
- Common writes go to `player.r` (`server/src/services/character/character_manager.ts:569`)
- Events read `player.relationships` (`server/src/events/adolescence/social.ts:47`)
- This can cause stale/missing relationship context in event logic

### P2-4: Loop timing mismatch in alternate loop path

- Config tick is 1ms (`server/src/config.ts:45`)
- `LoopManager.startGameLoop` defaults to 1000ms (`server/src/game/engine/LoopManager.ts:575`)
- If that path is ever used, pacing diverges 1000x from main loop assumptions

### P2-5: Age-band boundaries overlap heavily (not strict lifecycle partitions)

- Childhood: 0-12 (`server/src/events/childhood/index.ts:3`)
- Adolescence: 10-18 (`server/src/events/adolescence/index.ts:3`)
- Adulthood: 18+ (`server/src/events/adulthood/index.ts:3`)
- Event-level overlap examples: `newFriend` age 4-21 (`server/src/events/adolescence/social.ts:272`), `firstJob` age 15-18 (`server/src/events/adulthood/career.ts:76`)
- No authoritative central life-stage transition logic exists

### P2-6: Holiday date matching uses hardcoded dates for movable holidays

- Thanksgiving fixed at 11-23 (`server/src/events/holidays/annual.ts:33`)
- Black Friday fixed at 11-24 (`:44`)
- Easter fixed at 04-10 (`:211`)
- These dates change every year in reality

### P2-7: Some education events unreachable due to value mismatch

- `collegeAllNighter` requires `education === 'college'` (`server/src/events/education/quickWins.ts:235`)
- Progression uses values like `'college yr 1'..'college yr 4'` (`server/src/events/school_year/transitions.ts:258`)
- These never match exactly

---

## P3 - Improvements

### P3-1: handleQuestionEvent parses compound key but never uses it

- Parse at `server/src/handlers/events.ts:147`; key not used in call at `:183`

### P3-2: EventRegistry infrastructure is dead code

- No `registerEvent()` calls found outside the class definition (`server/src/game/engine/EventRegistry.ts:165`)
- `checkEvents` asks registry first (`server/src/stats/stats_manager.ts:141`) but it always returns empty
- Consider either populating it from `allEvents` or removing it

### P3-3: Ad-hoc transient fields on player lack type safety

- `_temp_sibling_id`, `_temp_parent_id`, `_temp_relative_id` in family events (`server/src/events/family/activities.ts:338`, `lifeTransitions.ts:156`)
- `minor`, `driversTestAttempts`, `greekLife` via type casts in school-year transitions (`server/src/events/school_year/transitions.ts:400, :511, :359`)
- These should be formalized in the Player or Person model

### P3-4: Relationship event quality varies -- some use shallow precondition checks

- Dedicated relationship random-event system properly checks active status and partner alive state (`server/src/events/relationships/randomEvents.ts:154, :235`)
- Adult romance/family events only require partner existence, missing affinity checks (`server/src/events/adulthood/romance.ts:13, :20`, `family.ts:14, :21`)

---

## iOS-Specific Findings

### iOS-1: Answer response format mismatch (P1)
- iOS sends `{"type": questionID, "message": answerJson}` where answerJson is the full `AnswerOption` struct (`ios/.../WebSocketService.swift:281`)
- Server dispatcher treats `type` as command name and routes to fallback generic handler
- Combined with backend P0-1, the response data never reaches the correct argument position

### iOS-2: No error handling for malformed event payloads (P2)
- `questionEvent` parsing at `WebSocketService.swift:582-618` does not validate required fields
- Missing `id` defaults to empty string, missing `message` defaults to empty string
- An event with no answers creates an `EventModalView` with empty answer grid

### iOS-3: Question model lacks Codable conformance (P3)
- `Question` struct (`ios/.../Question.swift:11`) conforms to `Identifiable, Equatable` but not `Codable`
- Parsing is manual in `WebSocketService` -- consider adding Codable for cleaner parsing

### iOS-4: MessageEvent image events shown as modal, non-image as timeline (P2)
- Events with images go to `currentMessageEvent` modal (`WebSocketService.swift:507`)
- Events without images go to `lifeEvents` timeline (`WebSocketService.swift:510`)
- This creates inconsistent UX -- some events block gameplay, others don't

---

## Answers to Audit Questions

| # | Question | Answer |
|---|----------|--------|
| 1 | Do events fire at correct ages? | **Partially.** Age checks exist per-event but age never advances in live gameplay (P0-3), making most age-gated events dead at runtime |
| 2 | Does event deduplication work? | **Partially.** fname/events Set tracking exists but answer handler mismatch (P0-1) prevents many askedQuestions updates from executing |
| 3 | Do question responses affect game state? | **No, for most events.** Arguments passed in wrong order (P0-1) + wrong argument slot in handleQuestionEvent |
| 4 | Do conversation events flow correctly? | **Mostly yes.** Conversation init/response flow is separate from the broken question path |
| 5 | Are life stage transitions properly triggered? | **No.** No central transition logic; age never advances in live play (P0-3); boundaries overlap (P2-5) |
| 6 | Does education progression work? | **No.** School-year transitions disabled (P0-4); education state model inconsistent (P1-5); progression can jump unrealistically (P1-6) |
| 7 | Do career events fire with jobs? | **Rarely.** Occupation state mismatch (P1-3) blocks most career events |
| 8 | Are there iOS model mismatches? | **Yes.** Answer format mismatch (iOS-1); some backend fields not modeled in iOS (characters, image on some events) |
| 9 | Do holiday events trigger on correct dates? | **Partially.** Fixed holidays work; movable holidays (Thanksgiving, Easter) are hardcoded to wrong dates (P2-6); dayOfYear wrapping bug (P0-6) breaks all date matching long-term |
| 10 | Are there dead events? | **Yes, extensively.** 5 entire categories missing (P0-5); probability outliers (P2-2); education value mismatches (P2-7); occupation state mismatches (P1-3) |
| 11 | Does tutorial sequence work? | **No.** ageHours not advancing (P0-3) and default age of 15 (P1-8) bypass tutorial mode entirely |
| 12 | Do negative events have lasting consequences? | **Partially.** Some events apply direct state changes; many rely on claim flow which is placeholder (P1-10) |

---

## Recommended Fix Priority

1. **Fix PlayerSession to include age progression, death checks, and intraday planning** (unblocks P0-3, P1-7, P1-9)
2. **Add missing event categories to allEvents** (unblocks P0-4, P0-5, P1-1)
3. **Fix question answer argument ordering** (unblocks P0-1, P1-2)
4. **Clean non-event helpers out of randomRelationshipEvents export** (fixes P0-2)
5. **Add dayOfYear wrapping in PlayerSession** (fixes P0-6)
6. **Normalize occupation state values** (fixes P1-3, P1-5)
7. **Implement claim event server-side validation** (fixes P1-10)
8. **Fix tutorial ageHours dependency** (fixes P1-8)
