Mode · Spec Drift

Where did the game drift from its design?

Compare the game’s design documents with its source. An offline CLI proposes candidates and checks their references before this static page is built.

Ultra proposed, code verified existence of the quote and symbol; whether the code really contradicts the doc is for a human to judge

8 of 8 claims verified by code; 0 dropped

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.

3 of 8 confirmed, 1 vague, 4 not drift

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);

Doc wording is vague

Candidate 7: decideRivalAction

Second-model verdict: DOC_VAGUE

Second-model reason: There is no contradiction because game/src/ai.js:44-48 invites sweeps or throws against guarding opponents, and game/README.md:86 is just using general wording ("throws have a cooldown") for the grapple cooldown enforced by `rival.grappleCooldown` in game/src/ai.js:32 and game/src/grapple.js:4.

Proposed severity: low

When the player is guarding, the AI considers SWEEP and ROUNDHOUSE, and conditionally adds THROW_PERSON if grabReady and dist<=1.25, but the grapple cooldown is checked via 'grabReady' which uses 'rival.grappleCooldown', while the design document mentions 'throws have a cooldown' without specifying grapple cooldown.

Document: game/README.md (verbatim copy)

Guarding can invite a sweep or clinch, and throws have a cooldown.

Code location: game/src/ai.js:44 · symbol: decideRivalAction

  if(player.isGuarding||player.state==='BLOCK') {
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 },