feat(ptt): code-side initial split — transmit_active / capability badge / sanitizer

Implements the gen2 v0.9.3 doc baseline's first slice of code work:

  * SRS-201: split the audio engine's `ptt` AtomicBool into the
    authoritative `transmit_active` flag. The legacy `set_ptt` /
    `ptt` accessors are retained as `#[doc(hidden)]` thin wrappers
    so the existing bridge command and the existing Flutter
    hold-to-talk UI keep compiling.
  * SAD-075 / SDD-089 acknowledged at the type level: only
    `AudioEngine::set_transmit_active` (or its legacy alias)
    mutates the flag; the encoder feed reads it once per outbound
    frame and never writes.
  * SDD-082: new `chanora_audio::ptt` module ships the
    `PttCapabilityLevel` enum (`L0Focused`, `L1GlobalShortcut`,
    `L2GlobalHoldToTalk`, `L3GlobalWithMouseButtons`,
    `L4DeviceAware` reserved) with a stable `as_str` mapping and
    an `is_global` classifier.
  * SDD-087: `PttBackendDescriptor::focused()` constant value for
    the universal Focused-PTT fallback. The struct shape carries
    only privacy-safe fields (`level`, `backend_id`,
    `bound_input_class`) — a key code cannot fit through this
    surface by construction (DEC-027).
  * SAD-077 / SDD-090: `RedactingLogLayer` now hosts the
    `PttBanCheckVisitor` and the `PTT_BANNED_FIELDS` constant
    (`key_code`, `scan_code`, `virtual_key`, `vk`, `keysym`,
    `keysym_string`, `key_sequence`, `key_press_history`,
    `key_timing`). Any record whose field set names a banned key
    is dropped before reaching the in-memory log sink or the
    user-initiated diagnostic export. The check is structural and
    runs ahead of formatting / redaction.
  * `SessionEvent::PttCapability` carries the diagnostics-safe
    descriptor through the broadcast event stream;
    `chanora_core::ChanoraSession::start_audio` publishes the
    Focused-PTT descriptor when the audio engine starts (SRS-196
    / SDD-091).
  * `BridgeEvent::PttCapability` mirrors the event across the
    FFI boundary. flutter_rust_bridge codegen regenerated.
  * Flutter `_AudioControls` renders a capability badge above the
    PTT button: a globe icon for Global levels, a focus-frame
    icon for `L0Focused`, plus a Tooltip exposing the bound input
    class. New ARB key `pttCapabilityBadge(level, backend)` in
    `app_en.arb` and `app_zh.arb`.

Per-platform global PTT backends (`WindowsRawInputBackend`,
`MacOSEventTapBackend`, `LinuxGnomeWaylandBackend`) and the
`MissedKeyUpWatchdog` task land in a separate follow-up commit;
this milestone ships only PTT-L0 universally so the application's
runtime capability reporting is honest from day one.

Tests
-----

* `chanora_audio` rises from 1 to 4 unit tests covering
  `PttCapabilityLevel::as_str`, `is_global`, and the
  `PttBackendDescriptor::focused()` shape contract.
* `chanora_diagnostics` rises from 9 to 11 unit tests covering
  the new `PttBanCheckVisitor` over every banned field name and
  the `PTT_BANNED_FIELDS` stability assertion.
* Workspace total: 53 unit + integration tests, all green with
  `CHANORA_DISABLE_KEYRING=1` (was 49 at v1.0.0-rc.2).
* `flutter analyze`: clean.
* `cargo deny check`: advisories ok, bans ok, licenses ok,
  sources ok.
* `cargo about generate`: zero warnings (license inventory
  regenerated).
* `tools/dump_flutter_licenses.sh`: 94 packages, zero without
  LICENSE.
* Linux x86_64 release bundle builds clean.

No Android live verification in this commit per the user's note
that the test device was removed. Android arm64-v8a continues to
build via the same `cargo ndk` path; runtime reporting on Android
is `L0Focused` for the foreseeable future.
This commit is contained in:
EdisonJwa
2026-05-15 15:02:03 +08:00
parent 02ffadfa52
commit 7b21916049
16 changed files with 642 additions and 31 deletions
+161
View File
@@ -0,0 +1,161 @@
//! Desktop Push-to-Talk capability model (SRS-195 / SRS-196 / SAD-071 /
//! SDD-081 / SDD-082).
//!
//! This module defines the typed `PttCapabilityLevel` enum and a
//! lightweight `PttBackendDescriptor` value the audio engine
//! publishes to upstream consumers so the UI can render the live
//! capability badge (SDD-091) and the release verification record
//! can carry per-platform evidence (SysDes-148).
//!
//! Concrete platform backends (`WindowsRawInputBackend`,
//! `MacOSEventTapBackend`, `LinuxGnomeWaylandBackend`) are deferred
//! to a follow-up code milestone; this commit lands the trait shape
//! and the universal `FocusedPttBackend` constant value so the
//! current Flutter-side hold-to-talk widget reports its capability
//! honestly through the bridge.
use core::fmt;
/// Detected runtime capability of the active desktop PTT backend.
///
/// The reported value shall match runtime behaviour — a backend
/// that *could* deliver `L2GlobalHoldToTalk` but lacks the
/// user-granted permission or the required compositor support
/// reports `L0Focused` (SRS-196 / SysRS-298).
///
/// `L4DeviceAware` is reserved per the gen2 v0.9.3 baseline
/// (SDD-082) and is **not** produced by any MVP implementation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PttCapabilityLevel {
/// Focused PTT. Press / release works only while the
/// application window has input focus. Mandatory baseline on
/// every desktop platform per SysRS-296.
L0Focused,
/// Global shortcut activation. The OS recognises a global
/// accelerator and notifies the application, but hold-to-talk
/// semantics may be approximated rather than guaranteed.
L1GlobalShortcut,
/// Global hold-to-talk. Press and release events are delivered
/// while the application is not focused.
L2GlobalHoldToTalk,
/// Global hold-to-talk plus mouse side buttons (typically
/// Mouse4 / Mouse5, sometimes labelled "back" / "forward").
L3GlobalWithMouseButtons,
/// Device-aware PTT. Reserved; no MVP implementation produces
/// this value (SDD-082).
L4DeviceAware,
}
impl PttCapabilityLevel {
/// Short identifier used by the diagnostics sanitizer and by
/// the release verification record. Stable across releases —
/// release notes and platform-test traces compare against these
/// strings.
pub fn as_str(self) -> &'static str {
match self {
Self::L0Focused => "L0Focused",
Self::L1GlobalShortcut => "L1GlobalShortcut",
Self::L2GlobalHoldToTalk => "L2GlobalHoldToTalk",
Self::L3GlobalWithMouseButtons => "L3GlobalWithMouseButtons",
Self::L4DeviceAware => "L4DeviceAware",
}
}
/// True when the level represents a global (non-focused)
/// behaviour. UI consumers use this to gate the "global PTT
/// available" affordance.
pub fn is_global(self) -> bool {
matches!(
self,
Self::L1GlobalShortcut
| Self::L2GlobalHoldToTalk
| Self::L3GlobalWithMouseButtons
| Self::L4DeviceAware
)
}
}
impl fmt::Display for PttCapabilityLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// Diagnostics-safe descriptor of the active PTT backend. Carries
/// only the fields the privacy policy permits in the user-initiated
/// diagnostic export (SRS-202 / DEC-027): the detected capability
/// level, a fixed `backend_id` string per implementation, and an
/// optional bound-input class (`"keyboard"`, `"mouse-side-button"`,
/// …). The raw key code, scan code, virtual-key value, or keysym
/// of any user binding is **never** part of this structure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PttBackendDescriptor {
/// Detected runtime capability.
pub level: PttCapabilityLevel,
/// Stable identifier per implementation. Examples:
/// `"focused"`, `"raw-input"`, `"low-level-hook"`,
/// `"event-tap"`, `"gnome-wayland-portal"`.
pub backend_id: &'static str,
/// Coarse description of the bound input. `None` when no
/// binding is active. The value is a stable category string,
/// never a key code.
pub bound_input_class: Option<&'static str>,
}
impl PttBackendDescriptor {
/// The universal Focused-PTT fallback (SDD-087). Every desktop
/// platform reports this value until a platform-specific
/// global backend lands in a follow-up code milestone.
pub const fn focused() -> Self {
Self {
level: PttCapabilityLevel::L0Focused,
backend_id: "focused",
bound_input_class: Some("keyboard"),
}
}
}
impl Default for PttBackendDescriptor {
fn default() -> Self {
Self::focused()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn level_as_str_is_stable() {
assert_eq!(PttCapabilityLevel::L0Focused.as_str(), "L0Focused");
assert_eq!(
PttCapabilityLevel::L2GlobalHoldToTalk.as_str(),
"L2GlobalHoldToTalk"
);
assert_eq!(
PttCapabilityLevel::L3GlobalWithMouseButtons.as_str(),
"L3GlobalWithMouseButtons"
);
}
#[test]
fn level_is_global_classification() {
assert!(!PttCapabilityLevel::L0Focused.is_global());
assert!(PttCapabilityLevel::L1GlobalShortcut.is_global());
assert!(PttCapabilityLevel::L2GlobalHoldToTalk.is_global());
assert!(PttCapabilityLevel::L3GlobalWithMouseButtons.is_global());
assert!(PttCapabilityLevel::L4DeviceAware.is_global());
}
#[test]
fn focused_descriptor_carries_only_safe_fields() {
let d = PttBackendDescriptor::focused();
assert_eq!(d.level, PttCapabilityLevel::L0Focused);
assert_eq!(d.backend_id, "focused");
assert_eq!(d.bound_input_class, Some("keyboard"));
// Compile-time check: the struct shape itself excludes
// anything that could carry a key code (DEC-027).
let _: &str = d.backend_id;
let _: Option<&str> = d.bound_input_class;
}
}