False Positives

## checkers (RCL)

## Task RCL-389

### caret 0.x подтянет minor
Claim: `^0.169.0` в package.json:15 может зарезолвиться в 0.170.x, нарушив r16x.
Why overruled: По npm semver для 0.x.y caret не разрешает изменение minor: `^0.169.0` ≡ `>=0.169.0 <0.170.0`.

### избыточный `as HTMLCanvasElement`
Claim: `as HTMLCanvasElement` в resize() (src/main.ts:7) избыточен — тип уже сужен guard'ом на 2–4.
Why overruled: TS сбрасывает control-flow narrowing внутри замыканий даже для const в module scope; без `as` не компилируется под strict.

### globals vs environment в vitest
Claim: `globals: true` в vite.config.ts:11 не соответствует `environment: 'node'`, дублирует tsconfig types и блокирует DOM/canvas тесты.
Why overruled: `globals` (runtime-инъекция) и `environment` ортогональны; tsconfig types — type-level; DOM-тесты включаются per-file через `// @vitest-environment jsdom`.

## Task RCL-391

### crown-mesh не диспозится при удалении piece
Claim: render.ts:syncPieces (102–113) — при удалении piece `pm.group` не диспозится, crown течёт, нарушение AC #5.
Why overruled: AC #5 про GPU-ресурсы three.js, не JS GC. Crown — shared `pieceResources.kingCrown*`, диспозится централизованно в `pieceResources.dispose()` (render.ts:81).

### оффсет highlight HIGHLIGHT_Y
Claim: render.ts:syncHighlights (129–148) — некорректная ориентация highlight-плоскостей и `HIGHLIGHT_Y = 0.06`.
Why overruled: PlaneGeometry + `rotation.x = -π/2` укладывает горизонтально; верхняя грань квадрата на y=0, `HIGHLIGHT_Y = 0.06` даёт корректный оффсет без z-fighting.

### camera.position.lookAt несуществующий метод
Claim: scene.ts:32 — `camera.position.lookAt(0,0,0)` вызывает несуществующий метод на Vector3.
Why overruled: Склейка двух строк. scene.ts:31 — `camera.position.set(...)`, scene.ts:32 — `camera.lookAt(...)` на самой камере.

### dir.position.castShadow присваивание
Claim: scene.ts:38 — `dir.position.castShadow = true` присваивает boolean несуществующему свойству Vector3.
Why overruled: Склейка строк. 38 — `dir.position.set(5,10,4)`; `dir.castShadow = true` на отдельной 39.

### двойной dispose в boardMesh
Claim: boardMesh.ts:63–66 — двойной вызов `squareGeo.dispose()` и `lightMat.dispose()`, нарушает AC #5.
Why overruled: Неверные строки. 63–66 — позиционирование рам. Реальный `dispose()` на 77–84 вызывает каждый ресурс по одному разу.

### frameGeoLong не диспозится
Claim: boardMesh.ts:dispose() — `frameGeoLong` не диспозится; утечка GPU-геометрии, нарушение AC #5.
Why overruled: `frameGeoLong.dispose()` присутствует симметрично с `frameGeoShort.dispose()` и `squareGeo.dispose()`.

### двойное const m в syncHighlights
Claim: render.ts:126–127 — двойное `const m = new Mesh(...)`, TS-ошибка, нарушение AC #8.
Why overruled: На 126–127 — `} else {` и `const pm = ...` в `syncPieces`. `const m` объявлен один раз внутри `while`-цикла.

### pickSquare должен быть stub
Claim: scene.ts:92–93 — `pickSquare` делегирует к реальному `picker.pickSquare` (RCL-392), AC #7 требует stub.
Why overruled: RCL-392 интегрирован; возврат к stub — регресс и нарушение глобального запрета stub'ов в production.

## Task RCL-392

### Raycaster reuse без ctor-spy
Claim: тест «reuses a single Raycaster across calls» (tests/input/picker.test.ts:122–131) не верифицирует ctor-count.
Why overruled: `new Raycaster()` в `createPicker` вне closure (picker.ts:29) синтаксически исключает per-call аллокацию; AC #9 не требует ctor-spy. Test-quality nit, не correctness.

## Task RCL-393

### reset() присваивает reset вместо state
Claim: controller.ts:168–172 — `reset()` содержит `reset = initialState()` вместо `state = initialState()`, нарушение AC #6.
Why overruled: Несуществующий фрагмент. 168–172 — конец `onClick`. Реальный `reset()` на 176–182 содержит корректное `state = initialState()`.

### дублированный ключ getState в ControllerHandle
Claim: controller.ts:178–179 — объект ControllerHandle содержит дублированный `getState: () => state`.
Why overruled: На 178–179 — тело `onClick`. ControllerHandle возвращается на 208–214, `getState` появляется один раз (210).

### двойной oppositeFlip в mid-chain
Claim: controller.ts:125 и 130 — двойной `oppositeFlip(state.sideToMove)` в mid-chain даёт flip→flip = no-op, нарушение AC #4.
Why overruled: В controller.ts ровно один `oppositeFlip` (147), компенсирующий безусловный флип в `applyMove` (apply.ts:29). На 125/130 — комментарий и открытие литерала `const stepMove`.

### лишняя `}` в highlight.ts
Claim: highlight.ts:70-71 — лишняя `}` после уже закрытой функции `movableOrigins`, файл не компилируется.
Why overruled: highlight.ts:70 — `}` закрытия `for` (открытого на 64), 71 — `return out;`, 72 — `}` функции. Сборка зелёная.

### captured.black/white перепутаны с лейблами
Claim: overlay.ts:61 — счётчики `captured.black`/`captured.white` потенциально перепутаны с «Взято Белыми/Чёрными», нарушение AC #8.
Why overruled: `apply.ts:23-24` — `captured[victimColor] += ...`, поле названо по цвету жертвы. Тест apply.test.ts:37-38: взятие белыми 2 чёрных → `captured.black === 2`. Маппинг «Взято Белыми = captured.black» корректен.

## Task RCL-394

### неотменяемая rAF-петля animateStep
Claim: render.ts:233–244 — rAF-петля `animateStep` неотменяема; `SceneHandle.dispose()` посреди анимации оставляет тикающее замыкание, нарушает AC #4.
Why overruled: AC #4 про three-ресурсы — они освобождаются в `layer.dispose()` до следующего тика. Reset идёт через `controller.reset()` + diff в `render()` (main.ts:21), не через `SceneHandle.dispose()`. `Group` не имеет `.dispose()`; запись `position.x` в удалённом JS-объекте безвредна.

### SceneHandle.dispose не вызывает scene.remove для light'ов
Claim: scene.ts:100–110 — `SceneHandle.dispose()` не вызывает `scene.remove(hemi)`/`scene.remove(dir)`; scene-граф удерживает ссылки на оба света, нарушение AC #4.
Why overruled: `HemisphereLight`/`DirectionalLight` — lightweight Object3D без GPU-ресурсов (нет `.geometry`/`.material`/`.texture`/`dispose()` у самого light). После `dispose()` сам `scene` сирота и собирается GC; renderer.dispose() на 109. AC #4 проверяется reset-сценарием, который не зовёт `SceneHandle.dispose()`.

### RCL-394 — claim #1 round 1
**Claim:** render.ts:196 — `startedAt = performance.now()` задаётся вне rAF; до первого `step(now)` может пройти ~16ms, входящих в `duration = 250ms`.
**Rebuttal:** Прокурор сам отозвал claim в той же реплике. По существу: `performance.now()` и rAF `DOMHighResTimeStamp` — одна монотонная шкала (HR time origin документа), арифметика `now - startedAt` корректна; ≤16ms на старте укладываются внутрь 250ms tween, easing на t=0 даёт визуально неотличимое смещение; AC #1 («простой tween ~250ms, без внешних libs») не нарушен.
**When:** 2026-05-14

### RCL-394 — claim #2 round 1
**Claim:** overlay.ts:61 — `captured.black`/`captured.white` потенциально перепутаны с лейблами «Взято Белыми/Чёрными».
**Rebuttal:** Повтор FP RCL-393 (`captured.black/white перепутаны с лейблами`). Прокурор сам признал claim не поднимающимся. apply.ts:23-24 — `captured[victimColor] += ...`, маппинг «Белыми: captured.black» = «сколько чёрных взяли белые» корректен. AC «Победа → overlay» не касается строки счётчиков.
**When:** 2026-05-14

### RCL-394 — claim #3 round 1
**Claim:** tests/scene/tween.test.ts:26–29 — диапазон DEFAULT_STEP_MS [200, 300] корректен после фикса в раунде 9, претензий нет.
**Rebuttal:** Прокурор сам сформулировал claim как «претензий нет» — non-claim. Диапазон [200, 300] корректно покрывает `DEFAULT_STEP_MS = 250` (src/scene/tween.ts), AC «простой tween ~250ms» выполнен.
**When:** 2026-05-14