smix-sdk 0.2.0

smix-sdk — user-facing public surface for the smix Rust library. App + ergonomic selector helpers + matchers. Wraps SimctlDriver + SimctlClient + HttpRunnerClient. v3.1 c11.
Documentation
//! v5.1 c3 — Capsule 软胶囊对账纯函数(G angle 主线)。
//!
//! 调用方:[`crate::App::stop_capsule_recording_and_reconcile`] 在 swift
//! `EventRecorder` 端 `/record/stop` 拿到 `Vec<RecordedEvent>` 后,跟 SDK
//! 端 [`crate::IssuedLedger`] 的 issued-action 账本对账,产出
//! [`CapsuleReconciliation`]:
//!
//! - **focus_change** = events 中 `raw_code == FOCUS_CHANGE_RAW_CODE` (1018)
//!   的子集,即 `kAXFirstResponderChangedNotification` user 真鼠标触发的
//!   焦点变化 ground truth。
//! - **attributed** = 每条 focus_change 在 issued ledger 中存在 `(event.ts -
//!   issued.ts).abs() <= window_ms` 的命中。
//! - **unattributed** = 剩下的 — 即外来 user 干预(SDK 没发对应 act 但
//!   焦点变了)。
//!
//! `target_hint` 不参与匹配(focus 变化的 element ID 经 swift KVC 转 hash
//! 后跟 SDK selector 不一一对应,只用时间窗判同源)。
//!
//! 默认 [`DEFAULT_RECONCILE_WINDOW_MS`] = 500 ms,依据 c2 capstone attachment
//! 实测 3 个 1018 events 跨 ~3.3 秒、SDK fill 到 EventRecorder 通常 < 300 ms。

use smix_runner_client::RecordedEvent;

use crate::issued_ledger::IssuedAction;

/// `kAXFirstResponderChangedNotification` 的 raw notification code。
pub const FOCUS_CHANGE_RAW_CODE: i32 = 1018;

/// 默认对账时间窗(ms)。
///
/// 实测依据(2026-06-14 real-sim diag):SDK `app.fill(input-email, ...)` 在
/// `sim-smix-02` 上耗 ~2.8 s(daemon-proxy keyboard typing + UI settle);
/// 1018 焦点变化在 fill 跨度内某点触发。default 3000 ms 覆盖典型 SDK fill /
/// tap / scroll 跨 sim 网络往返的真实延迟,留 200 ms 余量。
pub const DEFAULT_RECONCILE_WINDOW_MS: u64 = 3000;

/// 对账结果。`unattributed_events` 是外来 user 干预的全量明细(`Clone` 自
/// events 入参),上层判断时可看 element ID / timestamp。
#[derive(Clone, Debug, PartialEq)]
pub struct CapsuleReconciliation {
    pub issued_count: usize,
    pub focus_change_count: usize,
    pub attributed_count: usize,
    pub unattributed_count: usize,
    pub unattributed_events: Vec<RecordedEvent>,
}

/// 对账纯函数。无 IO,无 Mutex,可安全在任意线程跑。
pub fn reconcile(
    issued: &[IssuedAction],
    events: &[RecordedEvent],
    window_ms: u64,
) -> CapsuleReconciliation {
    let window = window_ms as f64;
    let focus_changes: Vec<&RecordedEvent> = events
        .iter()
        .filter(|e| e.raw_code == FOCUS_CHANGE_RAW_CODE)
        .collect();
    let mut attributed_count = 0usize;
    let mut unattributed_events: Vec<RecordedEvent> = Vec::new();
    for event in &focus_changes {
        let hit = issued
            .iter()
            .any(|act| (event.timestamp_ms - act.timestamp_ms).abs() <= window);
        if hit {
            attributed_count += 1;
        } else {
            unattributed_events.push((*event).clone());
        }
    }
    CapsuleReconciliation {
        issued_count: issued.len(),
        focus_change_count: focus_changes.len(),
        attributed_count,
        unattributed_count: unattributed_events.len(),
        unattributed_events,
    }
}