feat(live-atc): add simulated AI background traffic on the tuned frequency

Implements the ai-traffic roadmap item per
docs/plans/2026-07-14-ai-traffic-architecture-design.md.

Simulated other aircraft on the user's frequency — callsigns, ATC
instructions, readbacks in their own stable voice, handovers — as pure
scenery. It never touches radioBackend: the Python backend keeps owning
the dialogue *with* the user, useAiTraffic owns the radio *around* the
user. The two share only the speech queue (arbitration) and the log.

Rules live as pure, seeded, framework-free modules under
shared/utils/aiTraffic/ so they run in tsx --test without a browser:
callsign collision rules, wake/in-trail separation, runway slots, the
speed ladder, direct validation, the §3 decision table, and the gating
chain. app/composables/useAiTraffic.ts wires them to Vue (1 Hz tick,
spawner, scheduler).

Gating is evaluated twice — before enqueue and again at playback, since
seconds pass in between. Traffic never keys up while the user holds PTT,
while their transmission is out at the backend, or inside the fresh
readback window. Off by default; the toggle surfaces the feature's v1
limitations rather than burying them in a doc.

Zero LLM calls: variance comes from seeded RNG over template variants.

Deviations from the design, both documented in the design doc:
- Adds SimAircraft.quietUntilSec. The design's rule table says "first
  matching row per tick" but never says an instruction must be allowed to
  take effect before the next one. Without it the planner re-derives the
  same unresolved condition every second and nags one aircraft with the
  same vector: 624 calls/30min measured, vs 90 with the cooldown.
- Airline pool limited to the 14 designators DEFAULT_AIRLINE_TELEPHONY
  already knows; UAE/AUA/WZZ from the design would be spelled out letter
  by letter instead of spoken as airline names.

Verified: 406 tests pass (176 new), no new typecheck errors, /live-atc
compiles and serves. The manual in-session walkthrough (audible traffic,
toggle mid-session) is NOT verified — it needs a login and the Python
backend. The 30-minute deterministic integration run stands in for it and
caught two of the three bugs found during development.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-07-16 17:46:52 +02:00
parent 3fe0ea5f8a
commit 7d49f18cc6
25 changed files with 3968 additions and 3 deletions

View File

@@ -19,6 +19,20 @@ const learningMode = defineModel<boolean>('learningMode', { required: true })
const debugMode = defineModel<boolean>('debugMode', { required: true })
const prerecEnabled = defineModel<boolean>('prerecEnabled', { required: true })
const prerecSeconds = defineModel<number>('prerecSeconds', { required: true })
const aiTrafficEnabled = defineModel<boolean>('aiTrafficEnabled', { required: true })
/**
* Shown while AI traffic is on. These are deliberate v1 boundaries from the
* architecture design, not bugs — stating them up front is cheaper than having
* someone discover them mid-session and file them as defects.
*/
const AI_TRAFFIC_LIMITS = [
'Background scenery only — it never affects how your own radio work is scored.',
'Audible on the tuned frequency only. Other frequencies stay busy, just silent.',
'Never transmits while you hold PTT, while ATC is answering you, or in the ~12s after an instruction.',
'Fixed phraseology, no AI text generation. Invented waypoint names, not real SIDs/STARs.',
'Traffic never conflicts with you: it always yields, and you get no extra instructions from it.',
]
</script>
<template>
@@ -116,6 +130,34 @@ const prerecSeconds = defineModel<number>('prerecSeconds', { required: true })
/>
</div>
<div class="pt-2 border-t border-white/10 space-y-3">
<div>
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Frequency</p>
<p class="text-[11px] text-white/50 mt-1">
Simulated other aircraft on your frequency callsigns, readbacks, handovers so the
band sounds alive while you fly.
</p>
</div>
<v-switch
v-model="aiTrafficEnabled"
color="cyan"
inset
label="AI traffic (background chatter)"
hide-details
/>
<!-- Surfaced rather than buried in a doc: every one of these is a
deliberate v1 boundary, and knowing them up front is what stops
them from reading as broken behaviour. -->
<v-expand-transition>
<ul v-if="aiTrafficEnabled" class="pm-ai-limits">
<li v-for="limit in AI_TRAFFIC_LIMITS" :key="limit">
<v-icon size="13" class="pm-ai-limits__icon">mdi-information-outline</v-icon>
<span>{{ limit }}</span>
</li>
</ul>
</v-expand-transition>
</div>
<div class="pt-2 border-t border-white/10 space-y-3">
<div>
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Voice input</p>
@@ -155,6 +197,33 @@ const prerecSeconds = defineModel<number>('prerecSeconds', { required: true })
</template>
<style scoped>
/* The AI-traffic caveats. Quiet by design — they inform, they don't warn. */
.pm-ai-limits {
display: flex;
flex-direction: column;
gap: 6px;
margin: 0;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid color-mix(in srgb, #22d3ee 22%, transparent);
background: color-mix(in srgb, #22d3ee 7%, transparent);
list-style: none;
}
.pm-ai-limits li {
display: flex;
align-items: flex-start;
gap: 7px;
font-size: 11px;
line-height: 1.45;
color: var(--t3);
}
.pm-ai-limits__icon {
flex: none;
margin-top: 1px;
color: #22d3ee;
opacity: 0.75;
}
/* Mirrors the page's segmented control so the sheet doesn't depend on
live-atc.vue's scoped styles reaching a teleported dialog. */
.pm-seg {