ci / bun-nix (pull_request) Successful in 26s
ci / web (pull_request) Successful in 1m8s
ci / docs-site (pull_request) Successful in 1m9s
ci / rust-arm64 (pull_request) Successful in 1m34s
windows-drivers / driver-build (pull_request) Successful in 1m34s
android / android (pull_request) Successful in 3m30s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m15s
windows-drivers / probe-and-proto (pull_request) Successful in 18s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m6s
ci / rust (pull_request) Successful in 6m34s
apple / swift (pull_request) Failing after 11m43s
apple / screenshots (pull_request) Skipped
Follow-up to the G10 merge. The new sensor path was written against main, which does not carry this branch's G8 work, so it forwarded motion unconditionally — the one thing G8 exists to stop. `deviceMotion` checked `forwarding` and nothing else. A Bluetooth DualSense in a session that resolved to an X-Box backend would stream ~200 Hz of samples the host parses and discards, for the whole session, exactly as the USB capture path did before G8. Not a regression against shipped behaviour — the path is new — but it would have shipped the defect back into a client that had just been taught not to have it. `Slot` now carries `motionReaches`, asked once at open off the kind that pad DECLARED, in the same shape `ExternalPad` already used. Per pad, not per session: under Automatic the handshake carries the active pad's kind, so a couch with an X-Box pad on slot 0 and a DualSense on slot 1 must not have slot 1's working gyro suppressed by slot 0's answer. The notice moved to where the truth is known. `openSlot` knows only what kind a pad declared, not whether it physically has a gyro — that is discovered later, when `PadSensors` finds a gyroscope and calls `setDeviceHasSensorMotion`. Raising it there is the only placement that both tells a player whose gyro is being dropped and stays silent for the pads that never had one. Also unified the last duplicate scale in the module. G10 hoisted the wire units into `Gamepad` and pointed `DeviceGyro` at them, but `DsDevice` kept its own `20L` / `10000L` — and `Gamepad`'s new comment claims every sender goes through one place, which was not yet true. Two copies of a unit constant in one module is precisely the defect this program opened with (a DualShock 4 blob 40× hot because a second copy had drifted), so the claim and the code now agree. `val` rather than `const val` only because widening to Long is not a constant expression; Long is deliberate, since the calibration arithmetic overflows an Int before it divides. Proven non-vacuous rather than assumed: changing `Gamepad.MOTION_GYRO_LSB_PER_DEG_S` from 20 to 16 now fails four named cases across three classes — `DsDeviceTest.calibrationRescalesRawCountsOntoTheWireUnits`, `.theHostsOwnBlobIsAPassthrough`, `.parseStateAppliesTheCalibration` and `DeviceGyroTest.wireUnitConstants`. Before this change `DsDevice` would not have noticed. The gate itself has no test, for the reason the surrounding code already documents: `GamepadRouter` needs Android plus a live JNI handle, there is no Robolectric in this module, and a mock would test the mock. It is argued at the call sites instead. Gate: `:kit:compileDebugKotlin`, `:kit:testDebugUnitTest`, `:app:compileDebugKotlin`, `:app:testDebugUnitTest` — kit 75 / app 67, 0 failures, counts read out of the JUnit XML. The merge reconciles: 62 on this branch, plus 6 from main's DeviceGyroTest, plus G10's 7.