From 33d7aec1ddfb15a379a199b41801cbbc9d9fb165 Mon Sep 17 00:00:00 2001 From: itsrubberduck Date: Fri, 10 Jul 2026 15:37:20 +0200 Subject: [PATCH] refactor(live-atc): extract debug simulation and trace inspectors to useDebugSimulation Moves debugMode, the scripted end-to-end simulation (simulationPilotSteps, simulationDecisions, runFullSimulation and its helpers), the decision-trace computeds the debug drawer renders, and debugState/debugNextStates out of live-atc.vue. The page keeps only the destructured bindings its debug template still reads; once DebugPanel.vue exists it constructs this composable itself and the page stops touching it entirely. Co-Authored-By: Claude Opus 4.8 --- app/composables/useDebugSimulation.ts | 438 ++++++++++++++++++++++++++ app/pages/live-atc.vue | 407 ++---------------------- 2 files changed, 457 insertions(+), 388 deletions(-) create mode 100644 app/composables/useDebugSimulation.ts diff --git a/app/composables/useDebugSimulation.ts b/app/composables/useDebugSimulation.ts new file mode 100644 index 0000000..62635b3 --- /dev/null +++ b/app/composables/useDebugSimulation.ts @@ -0,0 +1,438 @@ +import { computed, nextTick, ref } from 'vue' +import useCommunicationsEngine from '../../shared/utils/communicationsEngine' + +export interface DebugSimulationDeps { + setLastTransmission: (text: string) => void + clearLog: () => void + startDemoFlight: () => void +} + +/** + * Debug drawer state: the scripted end-to-end flow simulation, the decision + * trace the panel renders, and the current/next-state inspectors. + * + * Only the debug panel constructs this — nothing on the normal cockpit path + * depends on it. + */ +export function useDebugSimulation( + engine: ReturnType, + deps: DebugSimulationDeps, +) { + const { + currentState, nextCandidates, variables: vars, flags, lastDecisionTrace, + getStateDetails, moveTo: forceMove, normalizeATCText, renderATCMessage, + processPilotTransmission, buildLLMContext, applyLLMDecision, + } = engine + const { setLastTransmission, clearLog, startDemoFlight } = deps + + const debugMode = ref(true) + + const simulationRunning = ref(false) + const simulationTrace = ref([]) + const simulationError = ref('') + + type SimulationDecisionTemplate = { + next: string + controllerSayState?: string + controllerSayTpl?: string + updates?: Record + note?: string + } + + type SimulationTraceEntry = { + kind: 'info' | 'pilot' | 'atc' | 'llm-input' | 'llm-output' + id: string + label: string + payload?: any + } + + const completedPilotSteps = computed(() => simulationTrace.value.filter(entry => entry.kind === 'pilot').length) + + const simulationPilotSteps = [ + 'CD_CHECK_ATIS', + 'CD_VERIFY_READBACK', + 'GRD_READY_FOR_PUSH', + 'GRD_TAXI_REQUEST', + 'GRD_TAXI_READBACK', + 'TWR_LINEUP_REQ', + 'TWR_TAKEOFF_READBACK', + 'DEP_IDENT', + 'DEP_CLIMB_READBACK', + 'DES_READBACK', + 'APP_ESTABLISHED', + 'TWR_LAND_READBACK', + 'GRD_TAXI_IN_REQ', + 'GRD_TAXI_IN_READBACK' + ] as const + + type SimulationPilotState = typeof simulationPilotSteps[number] + + const simulationDecisions: Record = { + CD_CHECK_ATIS: { + next: 'CD_ISSUE_CLR', + controllerSayState: 'CD_ISSUE_CLR', + updates: { + push_available: true, + runway_occupied: false, + pilot_able: true, + runway_available: true, + push_delay_min: 0, + surface_wind: '220/05', + speed_restriction: '210 knots', + emergency_heading: '180' + } + }, + CD_VERIFY_READBACK: { + next: 'CD_READBACK_CHECK' + }, + GRD_READY_FOR_PUSH: { + next: 'GRD_PUSH_APPROVE', + controllerSayState: 'GRD_PUSH_APPROVE' + }, + GRD_TAXI_REQUEST: { + next: 'GRD_TAXI_INSTR', + controllerSayState: 'GRD_TAXI_INSTR' + }, + GRD_TAXI_READBACK: { + next: 'TWR_CONTACT', + controllerSayState: 'TWR_CONTACT' + }, + TWR_LINEUP_REQ: { + next: 'TWR_TAKEOFF_CLR', + controllerSayState: 'TWR_TAKEOFF_CLR' + }, + TWR_TAKEOFF_READBACK: { + next: 'DEP_CONTACT', + controllerSayState: 'DEP_CONTACT' + }, + DEP_IDENT: { + next: 'DEP_CLIMB_INSTR', + controllerSayState: 'DEP_CLIMB_INSTR' + }, + DEP_CLIMB_READBACK: { + next: 'ENR_HANDOFF', + controllerSayState: 'ENR_HANDOFF' + }, + DES_READBACK: { + next: 'APP_HANDOFF', + controllerSayState: 'APP_HANDOFF', + updates: { + speed_restriction: '180 knots' + } + }, + APP_ESTABLISHED: { + next: 'TWR_LAND_CONTACT', + controllerSayState: 'TWR_LAND_CONTACT', + updates: { + runway_available: true, + surface_wind: '210/06' + } + }, + TWR_LAND_READBACK: { + next: 'TWR_VACATE', + controllerSayState: 'TWR_VACATE' + }, + GRD_TAXI_IN_REQ: { + next: 'GRD_TAXI_INSTR_IN', + controllerSayState: 'GRD_TAXI_INSTR_IN' + }, + GRD_TAXI_IN_READBACK: { + next: 'FLOW_COMPLETE' + } + } + + const recordedAtcStates = new Set() + const simulationStepCount = simulationPilotSteps.length + + const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + + const decisionTrace = computed(() => lastDecisionTrace.value) + const timelineSteps = computed(() => decisionTrace.value?.candidateTimeline?.steps ?? []) + const timelineUsedFallback = computed(() => Boolean(decisionTrace.value?.candidateTimeline?.fallbackUsed)) + const traceAutoSelection = computed(() => decisionTrace.value?.autoSelection ?? null) + const traceFallback = computed(() => decisionTrace.value?.fallback ?? null) + + const cloneForTrace = (value: T): T => { + if (value === undefined || value === null) { + return value + } + try { + return JSON.parse(JSON.stringify(value)) + } catch (err) { + console.warn('Failed to clone trace payload, returning original value.', err) + return value + } + } + + function describeElimination(entry: any): string { + if (!entry || typeof entry !== 'object') { + return '' + } + if (entry.kind === 'regex' && entry.context?.patterns?.length) { + const patterns = entry.context.patterns + .map((pattern: any) => pattern?.pattern) + .filter((value: string | undefined) => Boolean(value)) + .join(', ') + return patterns ? `Patterns: ${patterns}` : entry.reason + } + if (entry.kind === 'condition' && entry.context?.condition) { + const condition = entry.context.condition + if (condition.type === 'regex' || condition.type === 'regex_not') { + const flag = condition.pattern ? `/${condition.pattern}/${condition.patternFlags || 'i'}` : '' + return flag ? `Condition: ${condition.type} ${flag}` : entry.reason + } + const variable = condition.variable || 'value' + const operator = condition.operator || '==' + const expected = entry.context?.expectedValue ?? condition.value ?? '—' + const actual = entry.context?.actualValue ?? '—' + return `${variable} ${operator} ${expected} (actual: ${actual})` + } + return entry.reason + } + + const debugState = computed(() => { + if (!currentState.value) return null + const sayTpl = currentState.value.say_tpl + return { + id: currentState.value.id, + phase: currentState.value.phase, + role: currentState.value.role, + frequencyName: currentState.value.frequencyName, + sayPlain: sayTpl ? renderATCMessage(sayTpl) : '', + sayNormalized: sayTpl ? normalizeATCText(sayTpl, { ...vars.value, ...flags.value }) : '' + } + }) + + const debugNextStates = computed(() => { + return nextCandidates.value.map(id => { + const state = getStateDetails(id) + if (!state) { + return { id, role: '', phase: '', frequencyName: undefined, sayPlain: '', sayNormalized: '' } + } + const sayPlain = state.say_tpl ? renderATCMessage(state.say_tpl) : '' + const sayNormalized = state.say_tpl ? normalizeATCText(state.say_tpl, { ...vars.value, ...flags.value }) : '' + return { + id, + role: state.role, + phase: state.phase, + frequencyName: state.frequencyName, + sayPlain, + sayNormalized + } + }) + }) + + const formatTracePayload = (payload: any): string => { + if (payload === null || payload === undefined) return '' + if (typeof payload === 'string') return payload + try { + return JSON.stringify(payload, null, 2) + } catch (err) { + return String(payload) + } + } + + const recordCurrentAtcMessage = () => { + const state = currentState.value + if (!state?.id || recordedAtcStates.has(state.id) || !state.say_tpl) { + return + } + + const plain = renderATCMessage(state.say_tpl) + const normalized = normalizeATCText(state.say_tpl, { ...vars.value, ...flags.value }) + + simulationTrace.value.push({ + kind: 'atc', + id: state.id, + label: `ATC • ${state.phase}`, + payload: { text: plain, normalized } + }) + + setLastTransmission(`ATC: ${plain}`) + recordedAtcStates.add(state.id) + } + + const pickNextStateId = (state: ReturnType | null): string | null => { + if (!state) return null + + const chains: Array> = [] + if (state.ok_next?.length) chains.push(state.ok_next.map(({ to }) => ({ to }))) + if (state.next?.length) chains.push(state.next.map(({ to }) => ({ to }))) + if (state.bad_next?.length) chains.push(state.bad_next.map(({ to }) => ({ to }))) + if (state.timer_next?.length) chains.push(state.timer_next.map(({ to }) => ({ to }))) + + for (const list of chains) { + for (const entry of list) { + if (entry?.to) return entry.to + } + } + + return null + } + + const advanceAutomaticStates = async () => { + let guard = 0 + + while (guard++ < 50) { + const state = currentState.value + if (!state?.id) break + if (state.auto === 'end') break + + const autoMode = Boolean(state.auto && state.auto !== 'end') + if (!autoMode && state.role === 'pilot') break + + const nextId = pickNextStateId(state) + if (!nextId || nextId === state.id) break + + forceMove(nextId) + await nextTick() + recordCurrentAtcMessage() + } + } + + const runFullSimulation = async () => { + if (simulationRunning.value) return + + simulationRunning.value = true + simulationTrace.value = [] + simulationError.value = '' + recordedAtcStates.clear() + debugMode.value = true + + try { + simulationTrace.value.push({ + kind: 'info', + id: 'init', + label: 'Simulation Start', + payload: { timestamp: new Date().toISOString(), steps: simulationStepCount } + }) + + startDemoFlight() + await nextTick() + clearLog() + recordedAtcStates.clear() + + if (currentState.value?.id !== 'CD_CHECK_ATIS') { + forceMove('CD_CHECK_ATIS') + await nextTick() + } + + for (const stepId of simulationPilotSteps) { + const state = getStateDetails(stepId) + if (!state?.utterance_tpl) { + throw new Error(`Missing pilot utterance for ${stepId}`) + } + + if (currentState.value?.id !== stepId) { + forceMove(stepId) + await nextTick() + } + + await wait(120) + + const pilotText = renderATCMessage(state.utterance_tpl) + const pilotNormalized = normalizeATCText(state.utterance_tpl, { ...vars.value, ...flags.value }) + + simulationTrace.value.push({ + kind: 'pilot', + id: stepId, + label: `Pilot • ${state.phase}`, + payload: { text: pilotText, normalized: pilotNormalized } + }) + + setLastTransmission(`Pilot: ${pilotText}`) + + const quickResponse = processPilotTransmission(pilotText) + if (quickResponse) { + simulationTrace.value.push({ + kind: 'info', + id: `${stepId}-quick`, + label: 'Quick Response', + payload: { text: quickResponse } + }) + await nextTick() + } + + const ctx = buildLLMContext(pilotText) + simulationTrace.value.push({ + kind: 'llm-input', + id: stepId, + label: 'LLM Request', + payload: cloneForTrace(ctx) + }) + + const template = simulationDecisions[stepId] + if (!template) { + throw new Error(`Missing simulation decision for ${stepId}`) + } + + const decision: any = { next_state: template.next } + if (template.controllerSayTpl) { + decision.controller_say_tpl = template.controllerSayTpl + } else if (template.controllerSayState) { + const sayState = getStateDetails(template.controllerSayState) + if (sayState?.say_tpl) { + decision.controller_say_tpl = sayState.say_tpl + } + } + if (template.updates) { + decision.updates = template.updates + } + + simulationTrace.value.push({ + kind: 'llm-output', + id: stepId, + label: `LLM Response → ${decision.next_state}`, + payload: cloneForTrace(decision) + }) + + applyLLMDecision(decision) + await nextTick() + recordCurrentAtcMessage() + await advanceAutomaticStates() + await wait(200) + } + + simulationTrace.value.push({ + kind: 'info', + id: 'complete', + label: 'Simulation Complete', + payload: { + timestamp: new Date().toISOString(), + finalState: currentState.value?.id, + pilotSteps: completedPilotSteps.value + } + }) + } catch (err: any) { + const message = err?.message || String(err) + simulationError.value = message + simulationTrace.value.push({ + kind: 'info', + id: 'error', + label: 'Simulation aborted', + payload: { error: message } + }) + } finally { + simulationRunning.value = false + } + } + + return { + debugMode, + simulationRunning, + simulationTrace, + simulationError, + simulationStepCount, + completedPilotSteps, + decisionTrace, + timelineSteps, + timelineUsedFallback, + traceAutoSelection, + traceFallback, + debugState, + debugNextStates, + describeElimination, + formatTracePayload, + runFullSimulation, + } +} diff --git a/app/pages/live-atc.vue b/app/pages/live-atc.vue index 872c8b6..32e089c 100644 --- a/app/pages/live-atc.vue +++ b/app/pages/live-atc.vue @@ -1529,6 +1529,7 @@ import { import { useAtisPlayback } from '~/composables/useAtisPlayback' import { usePttRecording } from '~/composables/usePttRecording' import { useBugReport } from '~/composables/useBugReport' +import { useDebugSimulation } from '~/composables/useDebugSimulation' import { useSimBridgeSync } from '~/composables/useSimBridgeSync' import { useSpeechInterrupt } from '~/composables/useSpeechInterrupt' import { useRadioSpeech } from '~/composables/useRadioSpeech' @@ -1739,51 +1740,8 @@ const clearLog = () => { clearLastTransmission() } -const decisionTrace = computed(() => lastDecisionTrace.value) -const timelineSteps = computed(() => decisionTrace.value?.candidateTimeline?.steps ?? []) -const timelineUsedFallback = computed(() => Boolean(decisionTrace.value?.candidateTimeline?.fallbackUsed)) -const traceAutoSelection = computed(() => decisionTrace.value?.autoSelection ?? null) -const traceFallback = computed(() => decisionTrace.value?.fallback ?? null) const sessionLabel = computed(() => engineSessionId.value || flags.value.session_id || '-') -const cloneForTrace = (value: T): T => { - if (value === undefined || value === null) { - return value - } - try { - return JSON.parse(JSON.stringify(value)) - } catch (err) { - console.warn('Failed to clone trace payload, returning original value.', err) - return value - } -} - -function describeElimination(entry: any): string { - if (!entry || typeof entry !== 'object') { - return '' - } - if (entry.kind === 'regex' && entry.context?.patterns?.length) { - const patterns = entry.context.patterns - .map((pattern: any) => pattern?.pattern) - .filter((value: string | undefined) => Boolean(value)) - .join(', ') - return patterns ? `Patterns: ${patterns}` : entry.reason - } - if (entry.kind === 'condition' && entry.context?.condition) { - const condition = entry.context.condition - if (condition.type === 'regex' || condition.type === 'regex_not') { - const flag = condition.pattern ? `/${condition.pattern}/${condition.patternFlags || 'i'}` : '' - return flag ? `Condition: ${condition.type} ${flag}` : entry.reason - } - const variable = condition.variable || 'value' - const operator = condition.operator || '==' - const expected = entry.context?.expectedValue ?? condition.value ?? '—' - const actual = entry.context?.actualValue ?? '—' - return `${variable} ${operator} ${expected} (actual: ${actual})` - } - return entry.reason -} - // --------------------------------------------------------------------------- // Scenario definitions // --------------------------------------------------------------------------- @@ -2037,7 +1995,6 @@ const speechSpeed = ref(0.95) const radioCheckLoading = ref(false) const radioEffectsEnabled = ref(true) const readbackEnabled = ref(false) -const debugMode = ref(true) // ── Bug Report ─────────────────────────────────────────────────────────────── const { @@ -2164,10 +2121,6 @@ const TABS: PmTab[] = [ ] const visibleTabs = computed(() => TABS) -const simulationRunning = ref(false) -const simulationTrace = ref([]) -const simulationError = ref('') - // Send unauthenticated visitors to login while preserving where they were // headed (e.g. /live-atc?token=… so the bridge link survives the round-trip), // instead of dropping them on the classroom fallback after sign-in. @@ -2270,120 +2223,6 @@ watch(vatsimId, (id) => { }) // Computed Properties -const completedPilotSteps = computed(() => simulationTrace.value.filter(entry => entry.kind === 'pilot').length) - -type SimulationDecisionTemplate = { - next: string - controllerSayState?: string - controllerSayTpl?: string - updates?: Record - note?: string -} - -type SimulationTraceEntry = { - kind: 'info' | 'pilot' | 'atc' | 'llm-input' | 'llm-output' - id: string - label: string - payload?: any -} - -const simulationPilotSteps = [ - 'CD_CHECK_ATIS', - 'CD_VERIFY_READBACK', - 'GRD_READY_FOR_PUSH', - 'GRD_TAXI_REQUEST', - 'GRD_TAXI_READBACK', - 'TWR_LINEUP_REQ', - 'TWR_TAKEOFF_READBACK', - 'DEP_IDENT', - 'DEP_CLIMB_READBACK', - 'DES_READBACK', - 'APP_ESTABLISHED', - 'TWR_LAND_READBACK', - 'GRD_TAXI_IN_REQ', - 'GRD_TAXI_IN_READBACK' -] as const - -type SimulationPilotState = typeof simulationPilotSteps[number] - -const simulationDecisions: Record = { - CD_CHECK_ATIS: { - next: 'CD_ISSUE_CLR', - controllerSayState: 'CD_ISSUE_CLR', - updates: { - push_available: true, - runway_occupied: false, - pilot_able: true, - runway_available: true, - push_delay_min: 0, - surface_wind: '220/05', - speed_restriction: '210 knots', - emergency_heading: '180' - } - }, - CD_VERIFY_READBACK: { - next: 'CD_READBACK_CHECK' - }, - GRD_READY_FOR_PUSH: { - next: 'GRD_PUSH_APPROVE', - controllerSayState: 'GRD_PUSH_APPROVE' - }, - GRD_TAXI_REQUEST: { - next: 'GRD_TAXI_INSTR', - controllerSayState: 'GRD_TAXI_INSTR' - }, - GRD_TAXI_READBACK: { - next: 'TWR_CONTACT', - controllerSayState: 'TWR_CONTACT' - }, - TWR_LINEUP_REQ: { - next: 'TWR_TAKEOFF_CLR', - controllerSayState: 'TWR_TAKEOFF_CLR' - }, - TWR_TAKEOFF_READBACK: { - next: 'DEP_CONTACT', - controllerSayState: 'DEP_CONTACT' - }, - DEP_IDENT: { - next: 'DEP_CLIMB_INSTR', - controllerSayState: 'DEP_CLIMB_INSTR' - }, - DEP_CLIMB_READBACK: { - next: 'ENR_HANDOFF', - controllerSayState: 'ENR_HANDOFF' - }, - DES_READBACK: { - next: 'APP_HANDOFF', - controllerSayState: 'APP_HANDOFF', - updates: { - speed_restriction: '180 knots' - } - }, - APP_ESTABLISHED: { - next: 'TWR_LAND_CONTACT', - controllerSayState: 'TWR_LAND_CONTACT', - updates: { - runway_available: true, - surface_wind: '210/06' - } - }, - TWR_LAND_READBACK: { - next: 'TWR_VACATE', - controllerSayState: 'TWR_VACATE' - }, - GRD_TAXI_IN_REQ: { - next: 'GRD_TAXI_INSTR_IN', - controllerSayState: 'GRD_TAXI_INSTR_IN' - }, - GRD_TAXI_IN_READBACK: { - next: 'FLOW_COMPLETE' - } -} - -const recordedAtcStates = new Set() -const simulationStepCount = simulationPilotSteps.length - -const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) const speechInterrupt = useSpeechInterrupt() const { stopCurrentSpeech } = speechInterrupt @@ -2427,38 +2266,6 @@ const { prefetchAtisAudio, } = useAtisPlayback(engine, freq, currentScreen, signalStrength, setLastTransmission) -const debugState = computed(() => { - if (!currentState.value) return null - const sayTpl = currentState.value.say_tpl - return { - id: currentState.value.id, - phase: currentState.value.phase, - role: currentState.value.role, - frequencyName: currentState.value.frequencyName, - sayPlain: sayTpl ? renderATCMessage(sayTpl) : '', - sayNormalized: sayTpl ? normalizeATCText(sayTpl, { ...vars.value, ...flags.value }) : '' - } -}) - -const debugNextStates = computed(() => { - return nextCandidates.value.map(id => { - const state = getStateDetails(id) - if (!state) { - return { id, role: '', phase: '', frequencyName: undefined, sayPlain: '', sayNormalized: '' } - } - const sayPlain = state.say_tpl ? renderATCMessage(state.say_tpl) : '' - const sayNormalized = state.say_tpl ? normalizeATCText(state.say_tpl, { ...vars.value, ...flags.value }) : '' - return { - id, - role: state.role, - phase: state.phase, - frequencyName: state.frequencyName, - sayPlain, - sayNormalized - } - }) -}) - // Methods const normalizeExpectedText = (text: string): string => { if (!flightContext.value) return text @@ -3072,200 +2879,24 @@ const formatTime = (date: Date): string => { }) } -const formatTracePayload = (payload: any): string => { - if (payload === null || payload === undefined) return '' - if (typeof payload === 'string') return payload - try { - return JSON.stringify(payload, null, 2) - } catch (err) { - return String(payload) - } -} - -const recordCurrentAtcMessage = () => { - const state = currentState.value - if (!state?.id || recordedAtcStates.has(state.id) || !state.say_tpl) { - return - } - - const plain = renderATCMessage(state.say_tpl) - const normalized = normalizeATCText(state.say_tpl, { ...vars.value, ...flags.value }) - - simulationTrace.value.push({ - kind: 'atc', - id: state.id, - label: `ATC • ${state.phase}`, - payload: { text: plain, normalized } - }) - - setLastTransmission(`ATC: ${plain}`) - recordedAtcStates.add(state.id) -} - -const pickNextStateId = (state: ReturnType | null): string | null => { - if (!state) return null - - const chains: Array> = [] - if (state.ok_next?.length) chains.push(state.ok_next.map(({ to }) => ({ to }))) - if (state.next?.length) chains.push(state.next.map(({ to }) => ({ to }))) - if (state.bad_next?.length) chains.push(state.bad_next.map(({ to }) => ({ to }))) - if (state.timer_next?.length) chains.push(state.timer_next.map(({ to }) => ({ to }))) - - for (const list of chains) { - for (const entry of list) { - if (entry?.to) return entry.to - } - } - - return null -} - -const advanceAutomaticStates = async () => { - let guard = 0 - - while (guard++ < 50) { - const state = currentState.value - if (!state?.id) break - if (state.auto === 'end') break - - const autoMode = Boolean(state.auto && state.auto !== 'end') - if (!autoMode && state.role === 'pilot') break - - const nextId = pickNextStateId(state) - if (!nextId || nextId === state.id) break - - forceMove(nextId) - await nextTick() - recordCurrentAtcMessage() - } -} - -const runFullSimulation = async () => { - if (simulationRunning.value) return - - simulationRunning.value = true - simulationTrace.value = [] - simulationError.value = '' - recordedAtcStates.clear() - debugMode.value = true - - try { - simulationTrace.value.push({ - kind: 'info', - id: 'init', - label: 'Simulation Start', - payload: { timestamp: new Date().toISOString(), steps: simulationStepCount } - }) - - startDemoFlight() - await nextTick() - clearLog() - recordedAtcStates.clear() - - if (currentState.value?.id !== 'CD_CHECK_ATIS') { - forceMove('CD_CHECK_ATIS') - await nextTick() - } - - for (const stepId of simulationPilotSteps) { - const state = getStateDetails(stepId) - if (!state?.utterance_tpl) { - throw new Error(`Missing pilot utterance for ${stepId}`) - } - - if (currentState.value?.id !== stepId) { - forceMove(stepId) - await nextTick() - } - - await wait(120) - - const pilotText = renderATCMessage(state.utterance_tpl) - const pilotNormalized = normalizeATCText(state.utterance_tpl, { ...vars.value, ...flags.value }) - - simulationTrace.value.push({ - kind: 'pilot', - id: stepId, - label: `Pilot • ${state.phase}`, - payload: { text: pilotText, normalized: pilotNormalized } - }) - - setLastTransmission(`Pilot: ${pilotText}`) - - const quickResponse = processPilotTransmission(pilotText) - if (quickResponse) { - simulationTrace.value.push({ - kind: 'info', - id: `${stepId}-quick`, - label: 'Quick Response', - payload: { text: quickResponse } - }) - await nextTick() - } - - const ctx = buildLLMContext(pilotText) - simulationTrace.value.push({ - kind: 'llm-input', - id: stepId, - label: 'LLM Request', - payload: cloneForTrace(ctx) - }) - - const template = simulationDecisions[stepId] - if (!template) { - throw new Error(`Missing simulation decision for ${stepId}`) - } - - const decision: any = { next_state: template.next } - if (template.controllerSayTpl) { - decision.controller_say_tpl = template.controllerSayTpl - } else if (template.controllerSayState) { - const sayState = getStateDetails(template.controllerSayState) - if (sayState?.say_tpl) { - decision.controller_say_tpl = sayState.say_tpl - } - } - if (template.updates) { - decision.updates = template.updates - } - - simulationTrace.value.push({ - kind: 'llm-output', - id: stepId, - label: `LLM Response → ${decision.next_state}`, - payload: cloneForTrace(decision) - }) - - applyLLMDecision(decision) - await nextTick() - recordCurrentAtcMessage() - await advanceAutomaticStates() - await wait(200) - } - - simulationTrace.value.push({ - kind: 'info', - id: 'complete', - label: 'Simulation Complete', - payload: { - timestamp: new Date().toISOString(), - finalState: currentState.value?.id, - pilotSteps: completedPilotSteps.value - } - }) - } catch (err: any) { - const message = err?.message || String(err) - simulationError.value = message - simulationTrace.value.push({ - kind: 'info', - id: 'error', - label: 'Simulation aborted', - payload: { error: message } - }) - } finally { - simulationRunning.value = false - } -} +// Debug drawer: scripted simulation + decision-trace inspectors. +const { + debugMode, + simulationRunning, + simulationTrace, + simulationError, + simulationStepCount, + completedPilotSteps, + timelineSteps, + timelineUsedFallback, + traceAutoSelection, + traceFallback, + debugState, + debugNextStates, + describeElimination, + formatTracePayload, + runFullSimulation, +} = useDebugSimulation(engine, { setLastTransmission, clearLog, startDemoFlight }) // Lifecycle onMounted(async () => {