fix(sim-control): narrow SimControlParseResult via type predicates

3fe0ea5 left `nuxt typecheck` failing on main:

  useLiveAtcSession.ts: Property 'reason' does not exist on type
  'SimControlParseResult'.

Cause: the union is discriminated by a boolean, and the project builds
with `strict: false` (nuxt.config.ts). With strictNullChecks off,
TypeScript does not treat `true`/`false` literal types as discriminants,
so `if (r.matched) … else r.reason` never narrows. Nothing about the
sim-control types themselves is wrong — the same three lines with any
boolean-discriminated union fail identically.

Adds isSimControlMatch/isSimControlRejection type predicates, which narrow
regardless of strictNullChecks, and routes the one caller through them.
Turning on strictNullChecks would fix it at the root but is a repo-wide
change, not a bug fix.

The pre-push hook was correctly refusing to push this; origin/main is
clean, so the breakage never escaped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-07-16 17:51:09 +02:00
parent 7d49f18cc6
commit 672ac18ac7
2 changed files with 27 additions and 5 deletions

View File

@@ -8,6 +8,8 @@ import { useApi } from '~/composables/useApi'
import type { useRadioSpeech } from '~/composables/useRadioSpeech'
import useCommunicationsEngine from '../../shared/utils/communicationsEngine'
import {
isSimControlMatch,
isSimControlRejection,
parseSimControl,
simControlRejectionSpeech,
simControlResultSpeech,
@@ -331,11 +333,14 @@ export function useLiveAtcSession(
// anchors already keep it from ever matching real ICAO phraseology either way.
if (bridgeConnected.value) {
const simResult = parseSimControl(transcript)
if (simResult.matched) {
if (isSimControlMatch(simResult)) {
void sendSimControlCommand(simResult.command)
return
}
if (simResult.reason !== 'no_intent') {
// A rejection with a reason means the pilot clearly addressed their sim but
// got something wrong — tell them. 'no_intent' just means this was ordinary
// radio traffic, so it falls through to the flow untouched.
if (isSimControlRejection(simResult) && simResult.reason !== 'no_intent') {
const reply = simControlRejectionSpeech(simResult.reason)
scheduleControllerSpeech(reply)
appendLogEntry('atc', reply, currentState.value?.id ?? '', { frequency: frequencies.value.active })

View File

@@ -38,9 +38,26 @@ export type SimControlNoMatchReason =
| 'missing_runway'
| 'missing_airport'
export type SimControlParseResult =
| { matched: true; command: SimControlCommand; text: string }
| { matched: false; reason: SimControlNoMatchReason; text: string }
export type SimControlMatch = { matched: true; command: SimControlCommand; text: string }
export type SimControlRejection = { matched: false; reason: SimControlNoMatchReason; text: string }
export type SimControlParseResult = SimControlMatch | SimControlRejection
/**
* Narrowing helpers. This union is discriminated by a boolean, and the project
* builds with `strict: false` (nuxt.config.ts) — so `strictNullChecks` is off,
* and without it TypeScript does not treat `true`/`false` literal types as
* discriminants. `if (result.matched) … else result.reason` therefore does NOT
* narrow and fails to compile. An explicit type predicate narrows regardless,
* which is why callers must go through these rather than testing `.matched`.
*/
export function isSimControlMatch(result: SimControlParseResult): result is SimControlMatch {
return result.matched
}
export function isSimControlRejection(result: SimControlParseResult): result is SimControlRejection {
return !result.matched
}
/** Hard value ranges; anything outside is a refusal, never a clamp. */
export const SIM_CONTROL_LIMITS = {