@@ -1192,7 +1192,191 @@ async initializeTone(): Promise<void> {
11921192
11931193---
11941194
1195- ## 13. [ Template for Future Patterns]
1195+ ## 13. Mutation-Invariant Mismatch (Dual Truth Source)
1196+
1197+ ** Discovered** : 2026-01-01 (Codebase Audit)
1198+
1199+ ** Root Cause** : A mutation handler modifies state in a way that violates documented invariants, and the invariant validation is not called after that mutation. The invariant definition and the mutation handler have different expectations about what valid state looks like.
1200+
1201+ ### The Pattern
1202+
1203+ ``` typescript
1204+ // invariants.ts - Defines what valid state looks like
1205+ const MAX_STEPS = 128 ;
1206+
1207+ function checkTracksHaveValidArrays(tracks : SessionTrack []): string [] {
1208+ const violations: string [] = [];
1209+ for (const track of tracks ) {
1210+ // Invariant: arrays MUST be exactly MAX_STEPS length
1211+ if (track .steps .length !== MAX_STEPS ) {
1212+ violations .push (` Track ${track .id }: steps length ${track .steps .length } !== ${MAX_STEPS } ` );
1213+ }
1214+ }
1215+ return violations ;
1216+ }
1217+
1218+ // live-session.ts - Mutation handler with DIFFERENT expectations
1219+ private async handleSetTrackStepCount (ws , player , msg ): Promise < void > {
1220+ const track = this .state .tracks .find (t => t .id === msg .trackId );
1221+ track.stepCount = msg .stepCount ;
1222+
1223+ // BUG: Resizes arrays to match stepCount, violating the invariant!
1224+ if (msg.stepCount < oldStepCount ) {
1225+ track .steps = track .steps .slice (0 , msg .stepCount ); // Now length !== MAX_STEPS
1226+ }
1227+
1228+ // BUG: No validation called after mutation!
1229+ await this.persistToDoStorage();
1230+ this.broadcast({ type : ' track_step_count_set' , ... });
1231+ }
1232+
1233+ // Other handlers DO call validation
1234+ private async handleAddTrack (... ): Promise < void > {
1235+ // ... mutation ...
1236+ this.validateAndRepairState(' handleAddTrack' ); // ✓ Called
1237+ }
1238+ ` ` `
1239+
1240+ **What happens:**
1241+ 1. User sets stepCount to 64
1242+ 2. Arrays are truncated to 64 elements
1243+ 3. No validation runs (validateAndRepairState not called)
1244+ 4. State now violates invariant: ` steps.length (64) !== MAX_STEPS (128)`
1245+ 5. Later, user adds a track
1246+ 6. Validation runs, finds violation, auto-repairs by padding
1247+ 7. Cycle repeats
1248+
1249+ ### Why It's Dangerous
1250+
1251+ 1. **Silent invariant violations**: State is invalid but no error is logged/thrown
1252+ 2. **Inconsistent repair timing**: Violations persist until next validating mutation
1253+ 3. **Data loss**: Truncation destroys data; repair fills with defaults
1254+ 4. **Dual truth source**: Two parts of codebase disagree on valid state shape
1255+ 5. **Hard to reproduce**: Bug only manifests with specific mutation sequences
1256+ 6. **Hash/sync failures**: Invalid state may cause hash mismatches in multiplayer
1257+ 7. **False confidence**: Invariant checks exist but don't cover all mutations
1258+
1259+ ### Prevention Checklist
1260+
1261+ When adding or modifying mutation handlers:
1262+
1263+ - [ ] **Audit invariant expectations**: Read ` invariants .ts ` before writing mutation logic
1264+ - [ ] **Match mental models**: Ensure handler's assumptions match invariant definitions
1265+ - [ ] **Call validation after mutation**: Add ` validateAndRepairState(context)` after state changes
1266+ - [ ] **Check for validation gaps**: Grep for all mutation handlers, verify each calls validation
1267+ - [ ] **Write invariant tests**: Test that invariants pass after each type of mutation
1268+ - [ ] **Document expectations**: Comment which invariants the handler relies on
1269+ - [ ] **Prefer deletion over modification**: When fixing, remove violating code rather than changing invariants
1270+
1271+ ### Detection Script
1272+
1273+ ` ` ` bash
1274+ # Find mutation handlers that DON' T call validateAndRepairState
1275+ # 1. List all private async handle* methods
1276+ grep -n " private async handle" src/worker/live-session.ts | cut -d: - f1 ,2
1277+
1278+ # 2. Check which ones call validateAndRepairState
1279+ grep -n "validateAndRepairState" src/worker/live-session.ts
1280+
1281+ # 3. Compare the two lists - any handler missing validation is a potential bug
1282+
1283+ # Find array mutations that might violate length invariants
1284+ grep -rn "\.slice(0 ," src/worker/ --include=" *.ts"
1285+ grep -rn " \.push(" src/worker/ --include=" *.ts" | grep -v test
1286+ grep -rn " = \[\.\.\. " src/worker/ --include=" *.ts" | grep steps
1287+
1288+ # Find places where arrays are created with non-MAX_STEPS length
1289+ grep -rn " Array(" src/worker/ --include=" *.ts" | grep -v " MAX_STEPS\| 128"
1290+ ` ` `
1291+
1292+ ### Code Locations
1293+
1294+ **Invariant definitions:**
1295+ - ` src/worker/invariants.ts:17 ` - ` MAX_STEPS = 128 `
1296+ - ` src / worker / invariants .ts :227 - 244 ` - ` checkTracksHaveValidArrays ()`
1297+ - ` src / worker / invariants .ts :281 - 301 ` - ` validateStateInvariants ()`
1298+
1299+ **Violating mutation:**
1300+ - ` src / worker / live - session .ts :1034 - 1044 ` - Array resizing in ` handleSetTrackStepCount `
1301+ - ` src / worker / live - session .ts :600 - 603 ` - Dynamic array expansion in ` handleToggleStep `
1302+
1303+ **Validation calls (for comparison):**
1304+ - ` src / worker / live - session .ts :737 ` - ` handleAddTrack ` ✓
1305+ - ` src / worker / live - session .ts :778 ` - ` handleDeleteTrack ` ✓
1306+ - ` src / worker / live - session .ts :808 ` - ` handleClearTrack ` ✓
1307+
1308+ **Missing validation:**
1309+ - ` src / worker / live - session .ts :1014 - 1058 ` - ` handleSetTrackStepCount ` ✗
1310+
1311+ ### Example Fix
1312+
1313+ ` ` ` typescript
1314+ // BEFORE: Mutation violates invariant, no validation
1315+ private async handleSetTrackStepCount (... ): Promise < void > {
1316+ track.stepCount = msg .stepCount ;
1317+
1318+ // Resize arrays (VIOLATES invariant that arrays are always 128)
1319+ if (msg.stepCount > oldStepCount ) {
1320+ track .steps = [... track .steps , ... new Array (msg .stepCount - oldStepCount ).fill (false )];
1321+ } else if (msg.stepCount < oldStepCount ) {
1322+ track .steps = track .steps .slice (0 , msg .stepCount ); // Truncates!
1323+ }
1324+
1325+ await this.persistToDoStorage();
1326+ this.broadcast(... );
1327+ // No validateAndRepairState() call!
1328+ }
1329+
1330+ // AFTER: Remove violating code, arrays stay at MAX_STEPS
1331+ private async handleSetTrackStepCount (... ): Promise < void > {
1332+ track.stepCount = msg .stepCount ;
1333+ // Arrays stay at MAX_STEPS length - stepCount indicates active steps only
1334+ // Invariant preserved: track.steps.length === MAX_STEPS (128)
1335+
1336+ await this.persistToDoStorage();
1337+ this.broadcast(... );
1338+ }
1339+ ` ` `
1340+
1341+ ### Deeper Pattern: Dual Truth Source
1342+
1343+ This bug is an instance of a broader anti-pattern: **Dual Truth Source**.
1344+
1345+ When two parts of a codebase have different answers to "what is valid state?":
1346+ - Invariant validators define truth
1347+ - Mutation handlers define truth
1348+ - Repair logic defines truth
1349+ - Type definitions define truth
1350+
1351+ If these diverge, bugs emerge. The fix is always to **pick one source of truth and align everything else**.
1352+
1353+ In this case:
1354+ - **Source of truth**: ` invariants .ts ` (arrays are always 128)
1355+ - **Align handler**: Remove resizing code
1356+ - **Align repair**: Already correct (pads to 128)
1357+ - **Align types**: Already correct (arrays typed without length)
1358+
1359+ ### Known Instances
1360+
1361+ | Handler | Calls Validation? | Violates Invariant? | Status |
1362+ |---------|------------------|---------------------|--------|
1363+ | ` handleAddTrack ` | ✓ Yes | No | ✅ Safe |
1364+ | ` handleDeleteTrack ` | ✓ Yes | No | ✅ Safe |
1365+ | ` handleClearTrack ` | ✓ Yes | No | ✅ Safe |
1366+ | ` handleCopySequence ` | ✓ Yes | No | ✅ Safe |
1367+ | ` handleSetTrackStepCount ` | ✗ No | **Yes** | ⚠️ **BUG** |
1368+ | ` handleToggleStep ` | ✗ No | Yes (expands only) | ⚠️ Minor |
1369+ | ` handleSetParameterLock ` | ✗ No | No (doesn't resize) | ✅ Safe |
1370+
1371+ ### Related Documentation
1372+
1373+ - **Full analysis**: ` specs / research / STEP - ARRAY - INVARIANT - BUG .md `
1374+ - **Invariant system**: ` src / worker / invariants .ts `
1375+ - **Polyrhythm spec**: ` specs / POLYRHYTHM - SUPPORT .md ` (added odd step counts)
1376+
1377+ ---
1378+
1379+ ## 14. [Template for Future Patterns]
11961380
11971381**Discovered**: [Phase/Date]
11981382
0 commit comments