Recorded offline Ultra call.
Quotes and source locations below are copies from the checked snapshot. Symbol matching is textual and may match comments or strings; the cited line exists but need not contain the symbol. Severity and the contradiction explanation are model proposals.
Second-model check: gemini-3.8-flash-high via agy, read-only, run 2026-10-29. This is one second opinion, not ground truth.
Confirmed by a second model
Candidate 1: decideRivalAction
Second-model verdict: REAL_DRIFT
Second-model reason: The code at game/src/ai.js:35 advances at `dist > 1.62` and line 60 branches kicks at `dist > 1.28`, clearly contradicting the distance bands defined in docs/design.md:55 (`far > 2.2`, `mid 1.3-2.2`, `close < 1.3`).
Proposed severity: high
The AI decision logic uses a distance threshold of 1.62 to decide between advancing and close-range combat, and 1.25 for grapple range, which does not match the design document's defined distance bands of far (>2.2), mid (1.3-2.2), and close (<1.3).
Document: docs/design.md (verbatim copy)
dist: far(>2.2) | mid(1.3-2.2) | close(<1.3).
Code location: game/src/ai.js:35 · symbol: decideRivalAction
if(dist>1.62) return {type:'ADVANCE'};
Candidate 2: decideRivalAction
Second-model verdict: REAL_DRIFT
Second-model reason: The rival state check in game/src/ai.js:70 includes `'HIT_STUN'`, `'KNOCKDOWN'`, `'BEING_THROWN'`, `'GRABBED'`, and `'GRAB'`, which clearly goes beyond the six rival phases enumerated in docs/design.md:55.
Proposed severity: high
The code checks for rival states 'STARTUP', 'ACTIVE', 'RECOVERY', 'HIT_STUN', 'KNOCKDOWN', 'BEING_THROWN', 'GRABBED', 'GRAB', which includes states like 'HIT_STUN', 'BEING_THROWN', 'GRABBED', 'GRAB' not listed in the design document's rival phase enumeration, and omits 'idle' and 'down'.
Document: docs/design.md (verbatim copy)
rival phase: idle|startup|active|recovery|down|stunned.
Code location: game/src/ai.js:70 · symbol: decideRivalAction
if(rival.ground || rival.mount || player.mount || rival.grapple || player.grapple || ['HIT_STUN','KNOCKDOWN','BEING_THROWN','GRABBED','GRAB'].includes(rival.state))return;
Candidate 3: decideRivalAction
Second-model verdict: REAL_DRIFT
Second-model reason: The action selection in game/src/ai.js:48 and game/src/ai.js:60-61 produces specific strike types including `'JAB'`, `'CROSS'`, `'HOOK'`, `'ROUNDHOUSE'`, `'THROW_PERSON'`, and `'FEINT'`, which clearly goes beyond the generic action tokens listed in docs/design.md:56.
Proposed severity: high
The AI decision function returns action types like 'FEINT', 'THROW_PERSON', 'RETREAT', 'ADVANCE', 'SWEEP', 'ROUNDHOUSE', 'CROSS', 'JAB', 'BLOCK', 'PARRY', 'GRAB', 'THROW_PROP', 'KICK_PROP', which include several actions not in the design document's list (FEINT, THROW_PERSON, ROUNDHOUSE, CROSS, JAB, THROW_PROP, KICK_PROP) and uses different naming (e.g., 'BLOCK' vs 'block', 'ADVANCE' vs 'advance').
Document: docs/design.md (verbatim copy)
actions: advance retreat block punch kick low_punch sweep parry grab jump prop wait
Code location: game/src/ai.js:48 · symbol: decideRivalAction
return weighted(options.filter(([type])=>type!==last),rng);
Second model disagrees (4)
Candidate 4: decideRivalAction
Second-model verdict: NOT_DRIFT
Second-model reason: The claim is wrong because CPU feints are fully functional in game/src/ai.js:114-116 (playing `IDLE` with a `ROUNDHOUSE` follow-up executed at game/src/ai.js:52), while player input handling is irrelevant to the rival CPU tactics described in game/README.md:86.
Proposed severity: high
The neutral move selection options array includes 'FEINT' with weight 1, but the FEINT action type is not handled in the player input processing or animation system, making it a non-functional action that contradicts the claim of working feints.
Document: game/README.md (verbatim copy)
The CPU uses distance to select punches versus kicks, punch and kick combinations, including hooks, uppercuts and high/back kicks, recovery punishment, guard, parry cooldowns, feints, and guarded backsteps.
Code location: game/src/ai.js:60 · symbol: decideRivalAction
let options=dist>1.28?[['ROUNDHOUSE',6],['SWEEP',3],['ADVANCE',2]]:[['JAB',4],['CROSS',3],['HOOK',2],['UPPERCUT',1],['BODY_JAB',2],['ROUNDHOUSE',4],['HIGH_KICK',2],['BACK_KICK',1],['SWEEP',2],['FEINT',1]];
Candidate 5: createAIMemory
Second-model verdict: NOT_DRIFT
Second-model reason: The claim is wrong because game/src/ai.js:81 explicitly caps `observedMoves` to the last 8 player attacks (confusing it with the 10-entry CPU history in game/src/ai.js:19), and game/src/ai.js:54-58 adapts tactics against repeated punches and sweeps as documented in game/README.md:86.
Proposed severity: med
The AI memory initializes observedMoves as an empty array and the code pushes to it, but the memory object stores 'lastSerial' and 'attacksSinceKick' while the design claims tracking of 'last eight observed player attacks'; the code limits history to 10 entries but the logic for detecting repeated punches uses a filter on the last 4 moves, not 8.
Document: game/README.md (verbatim copy)
It tracks the last eight observed player attacks and changes tactics against repeated punches or sweeps.
Code location: game/src/ai.js:13 · symbol: createAIMemory
return { time:0, nextDecision:0, history:[], observedMoves:[], lastSerial:-1, guardTime:0, attacksSinceKick:0, nextParry:0, nextGrab:0, followUp:null, lastAction:'IDLE', previousPlayerPhase:null, previousPlayerTime:0 };
Candidate 6: decideRivalAction
Second-model verdict: NOT_DRIFT
Second-model reason: The claim is wrong because game/src/ai.js:62 filters out the last action to prevent immediate repetition while game/src/ai.js:53 forces a kick whenever `attacksSinceKick >= 2`, directly preventing punch chains beyond two attacks as documented in game/README.md:86.
Proposed severity: med
The code filters out the last action type from options to avoid immediate repetition, but the kick scheduling logic checks 'attacksSinceKick >= 2' to force a kick, which only triggers after two non-kick attacks, not preventing longer punch chains as implied.
Document: game/README.md (verbatim copy)
Neutral move selection avoids immediately repeating the same attack; kick scheduling prevents endless punch chains.
Code location: game/src/ai.js:62 · symbol: decideRivalAction
options=options.filter(([type])=>type!==last);
Candidate 8: FIGHTERS
Second-model verdict: NOT_DRIFT
Second-model reason: The claim is speculative because fighter style labels in game/src/character.js:11-12 are cosmetic metadata, whereas game/README.md:86 and game/README.md:91-92 explicitly state that both fighters use punches and kicks and share balanced combat frame data.
Proposed severity: low
The FIGHTERS object defines 'kai' and 'rook' profiles with different styles ('KICKBOXING' vs 'STREET BOXING') but the AI decision logic in ai.js does not differentiate between fighter profiles; both use the same decideRivalAction function with identical move options.
Document: game/README.md (verbatim copy)
Kai and Rook both use punches and kicks.
Code location: game/src/character.js:11 · symbol: FIGHTERS
kai: { name: 'KAI', title: 'THE NIGHTHAWK', style: 'KICKBOXING', color: 0xe85437, skin: 0xc38b65, hair: 0x151b22, pants: 0x1d2730 },