ASELSANMicrokernel
S553 · SOURCE-BOUND GATE EVIDENCE

S553 · R1 ekran: kilit/durum/ayar ekranları system UI akış modeli

tam S553 implementation modülü → Operations --test hedefi ile bağlı tam focused test → ayrı Operations kaydı Bu sayfa yalnız S553 kapısına aittir; komşu kapıların kaynakları bu kabulün içine katılmaz.

S553Focused kod testiOperations id exactsource SHA exacttest target exact

operation: g8l-s553-r1-system-ui-lock-status-settings-scene-flow-model

uygulama/model · focused test · Operations · 3 exact excerpt

sequence-bound=true · implementation-bound=true
01 · Yürütme / doğrulama kodu

Kapının gerçek repository sözleşmesi

tam dosyaL1–L721
kernel/src/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s553_r1_system_ui_lock_status_settings_scene_flow_model.rs::S553 r1 system ui lock status settings scene flow model implementation
//! S553 models the controlled system-UI flow of the R1 phone target on top of
//! the retained scene abstraction (`crate::ui::scene`): a Lock scene, a Home
//! (status) scene, a Settings scene and a placeholder App scene, driven by an
//! explicit transition table over typed input events.
//!
//! The model covers: a constant-time 4-digit PIN compare with a bounded
//! attempt counter and a tick-based lockout, an unlock gesture that is only
//! honoured while the `PIN_REQUIRED` flag is clear, a bounded scene stack
//! (depth <= 4), a bounded settings store of `u8` flags, a status bar (battery
//! percent, signal bars 0..=5, HH:MM clock derived from a monotonic tick
//! count) and a `SceneReceipt` for every accepted event.  Every invalid
//! transition, out-of-range value, tick regression, arithmetic overflow,
//! stack overflow/underflow, out-of-order event id or divergent replay fails
//! closed and leaves the state untouched.  An exact replay of the last
//! accepted event is retained with the same receipt.
//!
//! This gate is a source/host model only.  It does not claim a panel, a touch
//! controller, a framebuffer, a modem, a board, a UART or any physical
//! observation, and it is not wired into any boot, IRQ, scheduler or driver
//! path.  The S540 and S543 physical verdicts stay immutable RED.
//! Predecessor: S552 (input focus and touch event routing model).  Next gate:
//! S554 (modem AT command transport framing model).

pub const S553_SEQUENCE: usize = 553;
pub const S553_EXPECTED_PREDECESSOR: usize = 552;
pub const S553_R1_STAGE: u8 = 2;
pub const S553_R1_RANGE_FIRST: usize = 536;
pub const S553_R1_RANGE_LAST: usize = 568;
pub const S553_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS: usize = 0;
pub const S553_PHYSICAL_OBSERVATIONS: usize = 0;
pub const S553_PHYSICAL_OR_DEVICE_OPERATIONS: usize = 0;
pub const S553_SD_WRITES: usize = 0;
pub const S553_UART_OPENS: usize = 0;
pub const S553_POWER_TRANSITIONS: usize = 0;
pub const S553_NEW_IMMUTABLE_RAW_CAPTURES: usize = 0;
pub const S553_S540_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S553_S543_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S553_AUTOMATIC_PROMOTION: bool = false;
pub const S553_BOOT_TO_UI_PHYSICALLY_OBSERVED: bool = false;
pub const S553_HARDWARE_PRESENT: bool = false;
pub const S553_R1_ACCEPTANCE_COMPLETE: bool = false;
pub const RUNBOOK_EXECUTED_IN_S553: bool = false;

/// Number of PIN digits; every digit must be in `0..=9`.
pub const S553_PIN_DIGITS: usize = 4;
/// Failed PIN attempts tolerated before a lockout; the sixth failure locks.
pub const S553_MAX_PIN_ATTEMPTS: u8 = 5;
/// Maximum scene stack depth (inclusive).
pub const S553_SCENE_STACK_MAX_DEPTH: usize = 4;
/// Number of `u8` flag slots in the settings store.
pub const S553_SETTINGS_CAPACITY: usize = 8;
/// Settings key of the `PIN_REQUIRED` flag (set by default).
pub const S553_SETTING_PIN_REQUIRED: u8 = 0;
/// Highest permitted signal bar count.
pub const S553_SIGNAL_BARS_MAX: u8 = 5;
/// Highest permitted battery percent.
pub const S553_BATTERY_PERCENT_MAX: u8 = 100;
/// Model tick rate: ticks per second of the monotonic tick counter.
pub const S553_TICK_HZ: u64 = 100;
/// Ticks per clock minute.
pub const S553_TICKS_PER_MINUTE: u64 = S553_TICK_HZ * 60;
/// Minutes per wall-clock day; the clock wraps at this boundary.
pub const S553_MINUTES_PER_DAY: u64 = 24 * 60;
/// Lockout duration after `S553_MAX_PIN_ATTEMPTS` is exceeded (30 s).
pub const S553_LOCKOUT_TICKS: u64 = 30 * S553_TICK_HZ;
/// Model default PIN used by `State::new()`; it is a fixture, not a secret.
pub const S553_DEFAULT_PIN: [u8; S553_PIN_DIGITS] = [1, 2, 3, 4];
/// Default settings flags: only `PIN_REQUIRED` set.
pub const S553_DEFAULT_SETTINGS: [u8; S553_SETTINGS_CAPACITY] = [1, 0, 0, 0, 0, 0, 0, 0];

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS553Scene {
    Lock,
    Home,
    Settings,
    App,
}

impl G8lS553Scene {
    pub const fn index(self) -> u8 {
        match self {
            Self::Lock => 0,
            Self::Home => 1,
            Self::Settings => 2,
            Self::App => 3,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS553EventKind {
    UnlockGesture,
    PinEntry,
    Back,
    OpenSettings,
    OpenApp,
    ToggleSetting,
    Tick,
    BatteryReport,
    SignalReport,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS553Input {
    UnlockGesture,
    PinEntry([u8; S553_PIN_DIGITS]),
    Back,
    OpenSettings,
    OpenApp,
    ToggleSetting(u8),
    Tick(u64),
    BatteryReport(u8),
    SignalReport(u8),
}

impl G8lS553Input {
    pub const fn kind(self) -> G8lS553EventKind {
        match self {
            Self::UnlockGesture => G8lS553EventKind::UnlockGesture,
            Self::PinEntry(_) => G8lS553EventKind::PinEntry,
            Self::Back => G8lS553EventKind::Back,
            Self::OpenSettings => G8lS553EventKind::OpenSettings,
            Self::OpenApp => G8lS553EventKind::OpenApp,
            Self::ToggleSetting(_) => G8lS553EventKind::ToggleSetting,
            Self::Tick(_) => G8lS553EventKind::Tick,
            Self::BatteryReport(_) => G8lS553EventKind::BatteryReport,
            Self::SignalReport(_) => G8lS553EventKind::SignalReport,
        }
    }

    const fn is_status_event(self) -> bool {
        matches!(
            self,
            Self::Tick(_) | Self::BatteryReport(_) | Self::SignalReport(_)
        )
    }
}

/// An input event with a contiguous, caller-assigned id (first id is 1).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS553InputEvent {
    pub id: u64,
    pub input: G8lS553Input,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS553StatusBar {
    pub battery_percent: u8,
    pub signal_bars: u8,
    pub clock_hour: u8,
    pub clock_minute: u8,
    pub clock_text: [u8; 5],
    pub tick_count: u64,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS553SceneReceipt {
    pub sequence: usize,
    pub predecessor_sequence: usize,
    pub transition_sequence: u64,
    pub event_id: u64,
    pub event_kind: G8lS553EventKind,
    pub from_scene: G8lS553Scene,
    pub to_scene: G8lS553Scene,
    pub stack: [G8lS553Scene; S553_SCENE_STACK_MAX_DEPTH],
    pub stack_depth: u8,
    pub status_bar: G8lS553StatusBar,
    pub settings: [u8; S553_SETTINGS_CAPACITY],
    pub pin_failed_attempts: u8,
    pub lockout_count: u8,
    pub locked_out: bool,
    pub lockout_release_tick: u64,
    pub hardware_present: bool,
    pub physical_observations: usize,
    pub runbook_executed: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS553SceneFlowOutcome {
    Transitioned(G8lS553SceneReceipt),
    StatusUpdated(G8lS553SceneReceipt),
    SettingToggled(G8lS553SceneReceipt),
    UnlockRejected(G8lS553SceneReceipt),
    LockedOut(G8lS553SceneReceipt),
    Retained(G8lS553SceneReceipt),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS553SceneFlowError {
    EventOrder,
    PublishedEventDivergence,
    InvalidTransition,
    SettingsFromLockForbidden,
    SceneStackOverflow,
    SceneStackUnderflow,
    PinRequired,
    PinDigitOutOfRange,
    LockedOut,
    SettingKeyOutOfRange,
    SettingValueNotFlag,
    BatteryOutOfRange,
    SignalOutOfRange,
    TickRegression,
    TickOverflow,
    TransitionCounterOverflow,
    LockoutCounterOverflow,
}

impl G8lS553SceneFlowError {
    pub const fn diagnostic_code(self) -> u64 {
        match self {
            Self::EventOrder => 1,
            Self::PublishedEventDivergence => 2,
            Self::InvalidTransition => 3,
            Self::SettingsFromLockForbidden => 4,
            Self::SceneStackOverflow => 5,
            Self::SceneStackUnderflow => 6,
            Self::PinRequired => 7,
            Self::PinDigitOutOfRange => 8,
            Self::LockedOut => 9,
            Self::SettingKeyOutOfRange => 10,
            Self::SettingValueNotFlag => 11,
            Self::BatteryOutOfRange => 12,
            Self::SignalOutOfRange => 13,
            Self::TickRegression => 14,
            Self::TickOverflow => 15,
            Self::TransitionCounterOverflow => 16,
            Self::LockoutCounterOverflow => 17,
        }
    }
}

/// What an accepted scene event does to the scene stack.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS553TransitionAction {
    /// Lock -> Home; resets the stack to `[Home]`.
    Unlock,
    /// Push the given scene (bounded by `S553_SCENE_STACK_MAX_DEPTH`).
    Push(G8lS553Scene),
    /// Pop the top scene; the stack must keep at least one scene.
    Pop,
    /// Home -> Lock; resets the stack to `[Lock]`.
    Lock,
    /// Scene unchanged (settings toggle).
    Stay,
    /// Explicit guard rejection.
    Reject(G8lS553SceneFlowError),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS553TransitionRule {
    pub from: G8lS553Scene,
    pub kind: G8lS553EventKind,
    pub action: G8lS553TransitionAction,
}

const fn rule(
    from: G8lS553Scene,
    kind: G8lS553EventKind,
    action: G8lS553TransitionAction,
) -> G8lS553TransitionRule {
    G8lS553TransitionRule { from, kind, action }
}

/// Explicit scene transition table.  Status events (tick/battery/signal) are
/// accepted in every scene and are not part of this table.  Any
/// (scene, kind) pair absent from the table is an invalid transition.
pub const S553_TRANSITION_TABLE: [G8lS553TransitionRule; 11] = [
    rule(
        G8lS553Scene::Lock,
        G8lS553EventKind::UnlockGesture,
        G8lS553TransitionAction::Unlock,
    ),
    rule(
        G8lS553Scene::Lock,
        G8lS553EventKind::PinEntry,
        G8lS553TransitionAction::Unlock,
    ),
    rule(
        G8lS553Scene::Lock,
        G8lS553EventKind::OpenSettings,
        G8lS553TransitionAction::Reject(G8lS553SceneFlowError::SettingsFromLockForbidden),
    ),
    rule(
        G8lS553Scene::Home,
        G8lS553EventKind::OpenSettings,
        G8lS553TransitionAction::Push(G8lS553Scene::Settings),
    ),
    rule(
        G8lS553Scene::Home,
        G8lS553EventKind::OpenApp,
        G8lS553TransitionAction::Push(G8lS553Scene::App),
    ),
    rule(
        G8lS553Scene::Home,
        G8lS553EventKind::Back,
        G8lS553TransitionAction::Lock,
    ),
    rule(
        G8lS553Scene::Settings,
        G8lS553EventKind::ToggleSetting,
        G8lS553TransitionAction::Stay,
    ),
    rule(
        G8lS553Scene::Settings,
        G8lS553EventKind::Back,
        G8lS553TransitionAction::Pop,
    ),
    rule(
        G8lS553Scene::Settings,
        G8lS553EventKind::OpenApp,
        G8lS553TransitionAction::Push(G8lS553Scene::App),
    ),
    rule(
        G8lS553Scene::App,
        G8lS553EventKind::Back,
        G8lS553TransitionAction::Pop,
    ),
    rule(
        G8lS553Scene::App,
        G8lS553EventKind::OpenSettings,
        G8lS553TransitionAction::Push(G8lS553Scene::Settings),
    ),
];

pub fn s553_lookup_transition(
    from: G8lS553Scene,
    kind: G8lS553EventKind,
) -> Option<G8lS553TransitionAction> {
    S553_TRANSITION_TABLE
        .iter()
        .find(|entry| entry.from == from && entry.kind == kind)
        .map(|entry| entry.action)
}

/// Constant-time PIN compare: every digit is examined and OR-accumulated so
/// the result does not depend on the position of the first mismatch.
pub fn s553_pin_matches_constant_time(
    entered: [u8; S553_PIN_DIGITS],
    expected: [u8; S553_PIN_DIGITS],
) -> bool {
    let mut difference: u8 = 0;
    let mut index = 0;
    while index < S553_PIN_DIGITS {
        difference |= entered[index] ^ expected[index];
        index += 1;
    }
    difference == 0
}

pub fn s553_pin_digits_are_valid(pin: [u8; S553_PIN_DIGITS]) -> bool {
    pin.iter().all(|digit| *digit <= 9)
}

/// Derives the HH:MM wall clock from a monotonic tick count; the clock wraps
/// every `S553_MINUTES_PER_DAY` minutes.
pub const fn s553_clock_from_ticks(tick_count: u64) -> (u8, u8) {
    let minute_of_day = (tick_count / S553_TICKS_PER_MINUTE) % S553_MINUTES_PER_DAY;
    ((minute_of_day / 60) as u8, (minute_of_day % 60) as u8)
}

/// Encodes HH:MM as five ASCII bytes.
pub const fn s553_clock_text(hour: u8, minute: u8) -> [u8; 5] {
    [
        b'0' + hour / 10,
        b'0' + hour % 10,
        b':',
        b'0' + minute / 10,
        b'0' + minute % 10,
    ]
}

const fn status_bar(battery_percent: u8, signal_bars: u8, tick_count: u64) -> G8lS553StatusBar {
    let (clock_hour, clock_minute) = s553_clock_from_ticks(tick_count);
    G8lS553StatusBar {
        battery_percent,
        signal_bars,
        clock_hour,
        clock_minute,
        clock_text: s553_clock_text(clock_hour, clock_minute),
        tick_count,
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Snapshot {
    stack: [G8lS553Scene; S553_SCENE_STACK_MAX_DEPTH],
    depth: u8,
    status: G8lS553StatusBar,
    settings: [u8; S553_SETTINGS_CAPACITY],
    pin_failed_attempts: u8,
    lockout_count: u8,
    lockout_release_tick: Option<u64>,
    transitions: u64,
}

impl Snapshot {
    const fn current_scene(&self) -> G8lS553Scene {
        self.stack[(self.depth as usize).saturating_sub(1)]
    }

    const fn locked_out(&self) -> bool {
        self.lockout_release_tick.is_some()
    }
}

#[derive(Debug)]
pub struct G8lS553SystemUiState {
    pin: [u8; S553_PIN_DIGITS],
    snapshot: Snapshot,
    last: Option<(G8lS553InputEvent, G8lS553SceneReceipt)>,
}

impl G8lS553SystemUiState {
    pub const fn new() -> Self {
        Self {
            pin: S553_DEFAULT_PIN,
            snapshot: Snapshot {
                stack: [G8lS553Scene::Lock; S553_SCENE_STACK_MAX_DEPTH],
                depth: 1,
                status: status_bar(0, 0, 0),
                settings: S553_DEFAULT_SETTINGS,
                pin_failed_attempts: 0,
                lockout_count: 0,
                lockout_release_tick: None,
                transitions: 0,
            },
            last: None,
        }
    }

    /// Builds a state with a caller-provided PIN; digits must be `0..=9`.
    pub fn with_pin(pin: [u8; S553_PIN_DIGITS]) -> Result<Self, G8lS553SceneFlowError> {
        if !s553_pin_digits_are_valid(pin) {
            return Err(G8lS553SceneFlowError::PinDigitOutOfRange);
        }
        let mut state = Self::new();
        state.pin = pin;
        Ok(state)
    }

    pub const fn current_scene(&self) -> G8lS553Scene {
        self.snapshot.current_scene()
    }

    pub const fn stack_depth(&self) -> usize {
        self.snapshot.depth as usize
    }

    pub const fn status_bar(&self) -> G8lS553StatusBar {
        self.snapshot.status
    }

    pub const fn settings(&self) -> [u8; S553_SETTINGS_CAPACITY] {
        self.snapshot.settings
    }

    pub const fn pin_failed_attempts(&self) -> u8 {
        self.snapshot.pin_failed_attempts
    }

    pub const fn lockout_count(&self) -> u8 {
        self.snapshot.lockout_count
    }

    pub const fn locked_out(&self) -> bool {
        self.snapshot.locked_out()
    }

    pub const fn accepted_events(&self) -> u64 {
        self.snapshot.transitions
    }

    /// Id the next new event must carry.
    pub const fn expected_event_id(&self) -> u64 {
        self.snapshot.transitions + 1
    }

    pub fn last_receipt(&self) -> Option<G8lS553SceneReceipt> {
        self.last.map(|(_, receipt)| receipt)
    }
}

impl Default for G8lS553SystemUiState {
    fn default() -> Self {
        Self::new()
    }
}

fn receipt_for(
    event: G8lS553InputEvent,
    from_scene: G8lS553Scene,
    next: &Snapshot,
) -> G8lS553SceneReceipt {
    G8lS553SceneReceipt {
        sequence: S553_SEQUENCE,
        predecessor_sequence: S553_EXPECTED_PREDECESSOR,
        transition_sequence: next.transitions,
        event_id: event.id,
        event_kind: event.input.kind(),
        from_scene,
        to_scene: next.current_scene(),
        stack: next.stack,
        stack_depth: next.depth,
        status_bar: next.status,
        settings: next.settings,
        pin_failed_attempts: next.pin_failed_attempts,
        lockout_count: next.lockout_count,
        locked_out: next.locked_out(),
        lockout_release_tick: next.lockout_release_tick.unwrap_or(0),
        hardware_present: S553_HARDWARE_PRESENT,
        physical_observations: S553_PHYSICAL_OBSERVATIONS,
        runbook_executed: RUNBOOK_EXECUTED_IN_S553,
    }
}

fn apply_status_event(
    next: &mut Snapshot,
    input: G8lS553Input,
) -> Result<(), G8lS553SceneFlowError> {
    match input {
        G8lS553Input::Tick(tick_count) => {
            if tick_count < next.status.tick_count {
                return Err(G8lS553SceneFlowError::TickRegression);
            }
            next.status = status_bar(
                next.status.battery_percent,
                next.status.signal_bars,
                tick_count,
            );
            if let Some(release) = next.lockout_release_tick {
                if tick_count >= release {
                    next.lockout_release_tick = None;
                }
            }
        }
        G8lS553Input::BatteryReport(percent) => {
            if percent > S553_BATTERY_PERCENT_MAX {
                return Err(G8lS553SceneFlowError::BatteryOutOfRange);
            }
            next.status = status_bar(percent, next.status.signal_bars, next.status.tick_count);
        }
        G8lS553Input::SignalReport(bars) => {
            if bars > S553_SIGNAL_BARS_MAX {
                return Err(G8lS553SceneFlowError::SignalOutOfRange);
            }
            next.status = status_bar(next.status.battery_percent, bars, next.status.tick_count);
        }
        _ => return Err(G8lS553SceneFlowError::InvalidTransition),
    }
    Ok(())
}

fn push_scene(next: &mut Snapshot, scene: G8lS553Scene) -> Result<(), G8lS553SceneFlowError> {
    let depth = next.depth as usize;
    if depth >= S553_SCENE_STACK_MAX_DEPTH {
        return Err(G8lS553SceneFlowError::SceneStackOverflow);
    }
    next.stack[depth] = scene;
    next.depth = next
        .depth
        .checked_add(1)
        .ok_or(G8lS553SceneFlowError::SceneStackOverflow)?;
    Ok(())
}

fn pop_scene(next: &mut Snapshot) -> Result<(), G8lS553SceneFlowError> {
    if next.depth <= 1 {
        return Err(G8lS553SceneFlowError::SceneStackUnderflow);
    }
    next.depth -= 1;
    next.stack[next.depth as usize] = G8lS553Scene::Lock;
    Ok(())
}

fn reset_stack(next: &mut Snapshot, base: G8lS553Scene) {
    next.stack = [G8lS553Scene::Lock; S553_SCENE_STACK_MAX_DEPTH];
    next.stack[0] = base;
    next.depth = 1;
}

/// Result of an unlock attempt on the scene stack.
enum Unlock {
    Granted,
    Rejected,
    LockedOut,
}

fn apply_unlock(
    next: &mut Snapshot,
    pin: [u8; S553_PIN_DIGITS],
    input: G8lS553Input,
) -> Result<Unlock, G8lS553SceneFlowError> {
    if next.locked_out() {
        return Err(G8lS553SceneFlowError::LockedOut);
    }
    let entered = match input {
        G8lS553Input::UnlockGesture => {
            if next.settings[S553_SETTING_PIN_REQUIRED as usize] != 0 {
                return Err(G8lS553SceneFlowError::PinRequired);
            }
            next.pin_failed_attempts = 0;
            reset_stack(next, G8lS553Scene::Home);
            return Ok(Unlock::Granted);
        }
        G8lS553Input::PinEntry(entered) => entered,
        _ => return Err(G8lS553SceneFlowError::InvalidTransition),
    };
    if !s553_pin_digits_are_valid(entered) {
        return Err(G8lS553SceneFlowError::PinDigitOutOfRange);
    }
    if s553_pin_matches_constant_time(entered, pin) {
        next.pin_failed_attempts = 0;
        reset_stack(next, G8lS553Scene::Home);
        return Ok(Unlock::Granted);
    }
    let attempts = next
        .pin_failed_attempts
        .checked_add(1)
        .ok_or(G8lS553SceneFlowError::LockoutCounterOverflow)?;
    if attempts > S553_MAX_PIN_ATTEMPTS {
        let release = next
            .status
            .tick_count
            .checked_add(S553_LOCKOUT_TICKS)
            .ok_or(G8lS553SceneFlowError::TickOverflow)?;
        next.lockout_count = next
            .lockout_count
            .checked_add(1)
            .ok_or(G8lS553SceneFlowError::LockoutCounterOverflow)?;
        next.lockout_release_tick = Some(release);
        next.pin_failed_attempts = 0;
        return Ok(Unlock::LockedOut);
    }
    next.pin_failed_attempts = attempts;
    Ok(Unlock::Rejected)
}

fn apply_toggle(next: &mut Snapshot, key: u8) -> Result<(), G8lS553SceneFlowError> {
    if key as usize >= S553_SETTINGS_CAPACITY {
        return Err(G8lS553SceneFlowError::SettingKeyOutOfRange);
    }
    let value = next.settings[key as usize];
    if value > 1 {
        return Err(G8lS553SceneFlowError::SettingValueNotFlag);
    }
    next.settings[key as usize] = value ^ 1;
    Ok(())
}

/// Drives one input event through the scene flow.  Fail-closed: any `Err`
/// leaves the state untouched and does not consume the event id.  An exact
/// replay of the last accepted event is retained with the same receipt; a
/// different payload under the last accepted id is a divergence.
pub fn service_s553_model_scene_flow(
    state: &mut G8lS553SystemUiState,
    event: G8lS553InputEvent,
) -> Result<G8lS553SceneFlowOutcome, G8lS553SceneFlowError> {
    if let Some((published_event, published_receipt)) = state.last {
        if event.id == published_event.id {
            if event == published_event {
                return Ok(G8lS553SceneFlowOutcome::Retained(published_receipt));
            }
            return Err(G8lS553SceneFlowError::PublishedEventDivergence);
        }
    }
    if event.id != state.expected_event_id() {
        return Err(G8lS553SceneFlowError::EventOrder);
    }

    let mut next = state.snapshot;
    next.transitions = next
        .transitions
        .checked_add(1)
        .ok_or(G8lS553SceneFlowError::TransitionCounterOverflow)?;
    let from_scene = next.current_scene();

    let outcome: fn(G8lS553SceneReceipt) -> G8lS553SceneFlowOutcome =
        if event.input.is_status_event() {
            apply_status_event(&mut next, event.input)?;
            G8lS553SceneFlowOutcome::StatusUpdated
        } else {
            let action = s553_lookup_transition(from_scene, event.input.kind())
                .ok_or(G8lS553SceneFlowError::InvalidTransition)?;
            match action {
                G8lS553TransitionAction::Reject(error) => return Err(error),
                G8lS553TransitionAction::Unlock => {
                    match apply_unlock(&mut next, state.pin, event.input)? {
                        Unlock::Granted => G8lS553SceneFlowOutcome::Transitioned,
                        Unlock::Rejected => G8lS553SceneFlowOutcome::UnlockRejected,
                        Unlock::LockedOut => G8lS553SceneFlowOutcome::LockedOut,
                    }
                }
                G8lS553TransitionAction::Push(scene) => {
                    push_scene(&mut next, scene)?;
                    G8lS553SceneFlowOutcome::Transitioned
                }
                G8lS553TransitionAction::Pop => {
                    pop_scene(&mut next)?;
                    G8lS553SceneFlowOutcome::Transitioned
                }
                G8lS553TransitionAction::Lock => {
                    reset_stack(&mut next, G8lS553Scene::Lock);
                    G8lS553SceneFlowOutcome::Transitioned
                }
                G8lS553TransitionAction::Stay => match event.input {
                    G8lS553Input::ToggleSetting(key) => {
                        apply_toggle(&mut next, key)?;
                        G8lS553SceneFlowOutcome::SettingToggled
                    }
                    _ => return Err(G8lS553SceneFlowError::InvalidTransition),
                },
            }
        };

    let receipt = receipt_for(event, from_scene, &next);
    state.snapshot = next;
    state.last = Some((event, receipt));
    Ok(outcome(receipt))
}
snippet sha256: cedaf20d38f8file sha256: cedaf20d38f8
02 · Doğrulayan test kodu

Operations komutuna bağlı focused test

tam dosyaL1–L604
simulation/tests/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s553_r1_system_ui_lock_status_settings_scene_flow_model.rs::S553 r1 system ui lock status settings scene flow model focused tests
use aselsan_microkernel_simulation::g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s553_r1_system_ui_lock_status_settings_scene_flow_model::*;
use std::collections::BTreeSet;

const SOURCE: &str = include_str!(
    "../../kernel/src/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s553_r1_system_ui_lock_status_settings_scene_flow_model.rs"
);
const MAIN: &str = include_str!("../../kernel/src/main.rs");
const SIMULATION_LIB: &str = include_str!("../src/lib.rs");

type Outcome = G8lS553SceneFlowOutcome;
type Error = G8lS553SceneFlowError;
type Scene = G8lS553Scene;
type Input = G8lS553Input;

fn event(id: u64, input: Input) -> G8lS553InputEvent {
    G8lS553InputEvent { id, input }
}

/// Drives the next event using the state's expected id.
fn drive(state: &mut G8lS553SystemUiState, input: Input) -> Result<Outcome, Error> {
    let id = state.expected_event_id();
    service_s553_model_scene_flow(state, event(id, input))
}

fn receipt_of(outcome: Outcome) -> G8lS553SceneReceipt {
    match outcome {
        Outcome::Transitioned(receipt)
        | Outcome::StatusUpdated(receipt)
        | Outcome::SettingToggled(receipt)
        | Outcome::UnlockRejected(receipt)
        | Outcome::LockedOut(receipt)
        | Outcome::Retained(receipt) => receipt,
    }
}

fn unlocked_home() -> G8lS553SystemUiState {
    let mut state = G8lS553SystemUiState::new();
    let outcome = drive(&mut state, Input::PinEntry(S553_DEFAULT_PIN)).unwrap();
    assert!(matches!(outcome, Outcome::Transitioned(_)));
    assert_eq!(state.current_scene(), Scene::Home);
    state
}

#[test]
fn sequence_scope_and_nonpromotion_are_exact() {
    assert_eq!(S553_SEQUENCE, 553);
    assert_eq!(S553_EXPECTED_PREDECESSOR, 552);
    assert_eq!(S553_R1_STAGE, 2);
    assert_eq!(S553_R1_RANGE_FIRST, 536);
    assert_eq!(S553_R1_RANGE_LAST, 568);
    assert_eq!(S553_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS, 0);
    assert_eq!(S553_PHYSICAL_OBSERVATIONS, 0);
    assert_eq!(S553_PHYSICAL_OR_DEVICE_OPERATIONS, 0);
    assert_eq!(S553_SD_WRITES, 0);
    assert_eq!(S553_UART_OPENS, 0);
    assert_eq!(S553_POWER_TRANSITIONS, 0);
    assert_eq!(S553_NEW_IMMUTABLE_RAW_CAPTURES, 0);
    assert!(S553_S540_PHYSICAL_VERDICT_RETAINED_RED);
    assert!(S553_S543_PHYSICAL_VERDICT_RETAINED_RED);
    assert!(!S553_AUTOMATIC_PROMOTION);
    assert!(!S553_BOOT_TO_UI_PHYSICALLY_OBSERVED);
    assert!(!S553_HARDWARE_PRESENT);
    assert!(!S553_R1_ACCEPTANCE_COMPLETE);
    assert!(!RUNBOOK_EXECUTED_IN_S553);
    assert_eq!(S553_PIN_DIGITS, 4);
    assert_eq!(S553_MAX_PIN_ATTEMPTS, 5);
    assert_eq!(S553_SCENE_STACK_MAX_DEPTH, 4);
    assert_eq!(S553_SETTINGS_CAPACITY, 8);
    assert_eq!(S553_SIGNAL_BARS_MAX, 5);
    assert_eq!(S553_BATTERY_PERCENT_MAX, 100);
    assert_eq!(S553_TICKS_PER_MINUTE, 6_000);
    assert_eq!(S553_LOCKOUT_TICKS, 3_000);
}

#[test]
fn module_is_registered_in_kernel_and_simulation() {
    let module = "g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s553_r1_system_ui_lock_status_settings_scene_flow_model";
    assert!(MAIN.contains(&format!("mod {module};")));
    assert!(SIMULATION_LIB.contains(&format!("pub mod {module};")));
}

#[test]
fn source_has_no_device_execution_or_uart_emission_surface() {
    for forbidden in [
        "unsafe",
        "asm!",
        "write_volatile",
        "crate::uart",
        "crate::arch",
        "#[no_mangle]",
        "spin::",
        "std::",
        "kprintln!",
    ] {
        assert!(!SOURCE.contains(forbidden), "forbidden token: {forbidden}");
    }
    assert!(SOURCE.contains("source/host model only"));
    assert!(SOURCE.contains("S553_HARDWARE_PRESENT: bool = false"));
    assert!(SOURCE.contains("S553_PHYSICAL_OBSERVATIONS: usize = 0"));
    assert!(SOURCE.contains("S553_R1_ACCEPTANCE_COMPLETE: bool = false"));
    assert!(SOURCE.contains("RUNBOOK_EXECUTED_IN_S553: bool = false"));
}

#[test]
fn diagnostic_codes_are_nonzero_and_unique() {
    let errors = [
        Error::EventOrder,
        Error::PublishedEventDivergence,
        Error::InvalidTransition,
        Error::SettingsFromLockForbidden,
        Error::SceneStackOverflow,
        Error::SceneStackUnderflow,
        Error::PinRequired,
        Error::PinDigitOutOfRange,
        Error::LockedOut,
        Error::SettingKeyOutOfRange,
        Error::SettingValueNotFlag,
        Error::BatteryOutOfRange,
        Error::SignalOutOfRange,
        Error::TickRegression,
        Error::TickOverflow,
        Error::TransitionCounterOverflow,
        Error::LockoutCounterOverflow,
    ];
    let codes: BTreeSet<_> = errors.into_iter().map(Error::diagnostic_code).collect();
    assert_eq!(codes.len(), errors.len());
    assert!(!codes.contains(&0));
}

#[test]
fn exact_replay_retains_the_same_receipt() {
    let mut state = G8lS553SystemUiState::new();
    let first = event(1, Input::PinEntry(S553_DEFAULT_PIN));
    let Outcome::Transitioned(receipt) = service_s553_model_scene_flow(&mut state, first).unwrap()
    else {
        panic!("first unlock must transition")
    };
    assert_eq!(
        service_s553_model_scene_flow(&mut state, first),
        Ok(Outcome::Retained(receipt))
    );
    assert_eq!(state.accepted_events(), 1);
    assert_eq!(state.expected_event_id(), 2);
    assert_eq!(state.last_receipt(), Some(receipt));
}

#[test]
fn divergent_input_after_publication_fails_closed() {
    let mut state = G8lS553SystemUiState::new();
    service_s553_model_scene_flow(&mut state, event(1, Input::PinEntry(S553_DEFAULT_PIN))).unwrap();
    let before = state.last_receipt();
    assert_eq!(
        service_s553_model_scene_flow(&mut state, event(1, Input::UnlockGesture)),
        Err(Error::PublishedEventDivergence)
    );
    assert_eq!(
        service_s553_model_scene_flow(&mut state, event(1, Input::PinEntry([1, 2, 3, 5]))),
        Err(Error::PublishedEventDivergence)
    );
    assert_eq!(state.last_receipt(), before);
    assert_eq!(state.current_scene(), Scene::Home);
}

#[test]
fn default_state_starts_locked_with_pin_required() {
    let state = G8lS553SystemUiState::new();
    assert_eq!(state.current_scene(), Scene::Lock);
    assert_eq!(state.stack_depth(), 1);
    assert_eq!(state.settings(), S553_DEFAULT_SETTINGS);
    assert_eq!(state.settings()[S553_SETTING_PIN_REQUIRED as usize], 1);
    assert_eq!(state.pin_failed_attempts(), 0);
    assert_eq!(state.lockout_count(), 0);
    assert!(!state.locked_out());
    assert_eq!(state.accepted_events(), 0);
    assert_eq!(state.expected_event_id(), 1);
    assert_eq!(state.last_receipt(), None);
    let status = state.status_bar();
    assert_eq!(status.battery_percent, 0);
    assert_eq!(status.signal_bars, 0);
    assert_eq!(status.clock_text, *b"00:00");
    assert_eq!(status.tick_count, 0);
    let default_state = G8lS553SystemUiState::default();
    assert_eq!(default_state.current_scene(), Scene::Lock);
    assert_eq!(default_state.settings(), S553_DEFAULT_SETTINGS);
}

#[test]
fn correct_pin_unlocks_to_home_with_exact_receipt() {
    let mut state = G8lS553SystemUiState::new();
    drive(&mut state, Input::PinEntry([1, 2, 3, 5])).unwrap();
    assert_eq!(state.pin_failed_attempts(), 1);
    let Outcome::Transitioned(receipt) = drive(&mut state, Input::PinEntry(S553_DEFAULT_PIN)).unwrap()
    else {
        panic!("correct PIN must unlock")
    };
    assert_eq!(receipt.sequence, 553);
    assert_eq!(receipt.predecessor_sequence, 552);
    assert_eq!(receipt.transition_sequence, 2);
    assert_eq!(receipt.event_id, 2);
    assert_eq!(receipt.event_kind, G8lS553EventKind::PinEntry);
    assert_eq!(receipt.from_scene, Scene::Lock);
    assert_eq!(receipt.to_scene, Scene::Home);
    assert_eq!(receipt.stack_depth, 1);
    assert_eq!(receipt.stack[0], Scene::Home);
    assert_eq!(receipt.pin_failed_attempts, 0);
    assert_eq!(receipt.lockout_count, 0);
    assert!(!receipt.locked_out);
    assert_eq!(receipt.lockout_release_tick, 0);
    assert!(!receipt.hardware_present);
    assert_eq!(receipt.physical_observations, 0);
    assert!(!receipt.runbook_executed);
    assert_eq!(state.current_scene(), Scene::Home);
}

#[test]
fn unlock_gesture_requires_pin_flag_cleared() {
    let mut state = G8lS553SystemUiState::new();
    assert_eq!(drive(&mut state, Input::UnlockGesture), Err(Error::PinRequired));
    assert_eq!(state.current_scene(), Scene::Lock);
    assert_eq!(state.accepted_events(), 0);

    drive(&mut state, Input::PinEntry(S553_DEFAULT_PIN)).unwrap();
    drive(&mut state, Input::OpenSettings).unwrap();
    let Outcome::SettingToggled(receipt) =
        drive(&mut state, Input::ToggleSetting(S553_SETTING_PIN_REQUIRED)).unwrap()
    else {
        panic!("toggle must be accepted in Settings")
    };
    assert_eq!(receipt.settings[0], 0);
    assert_eq!(receipt.to_scene, Scene::Settings);
    drive(&mut state, Input::Back).unwrap();
    assert_eq!(state.current_scene(), Scene::Home);
    drive(&mut state, Input::Back).unwrap();
    assert_eq!(state.current_scene(), Scene::Lock);
    let Outcome::Transitioned(receipt) = drive(&mut state, Input::UnlockGesture).unwrap() else {
        panic!("gesture must unlock once PIN_REQUIRED is clear")
    };
    assert_eq!(receipt.event_kind, G8lS553EventKind::UnlockGesture);
    assert_eq!(receipt.to_scene, Scene::Home);
    assert_eq!(receipt.transition_sequence, 6);
}

#[test]
fn sixth_pin_failure_locks_out_and_increments_lockout_counter() {
    let mut state = G8lS553SystemUiState::new();
    for attempt in 1..=S553_MAX_PIN_ATTEMPTS {
        let Outcome::UnlockRejected(receipt) = drive(&mut state, Input::PinEntry([9, 9, 9, 9])).unwrap()
        else {
            panic!("wrong PIN must be rejected without lockout")
        };
        assert_eq!(receipt.pin_failed_attempts, attempt);
        assert_eq!(receipt.to_scene, Scene::Lock);
        assert!(!receipt.locked_out);
    }
    let Outcome::LockedOut(receipt) = drive(&mut state, Input::PinEntry([9, 9, 9, 9])).unwrap() else {
        panic!("sixth failure must lock out")
    };
    assert!(receipt.locked_out);
    assert_eq!(receipt.lockout_count, 1);
    assert_eq!(receipt.pin_failed_attempts, 0);
    assert_eq!(receipt.lockout_release_tick, S553_LOCKOUT_TICKS);
    assert!(state.locked_out());
    assert_eq!(state.lockout_count(), 1);
}

#[test]
fn lockout_rejects_pins_until_release_tick() {
    let mut state = G8lS553SystemUiState::new();
    drive(&mut state, Input::Tick(1_000)).unwrap();
    for _ in 0..=S553_MAX_PIN_ATTEMPTS {
        drive(&mut state, Input::PinEntry([0, 0, 0, 0])).unwrap();
    }
    assert!(state.locked_out());
    assert_eq!(state.last_receipt().unwrap().lockout_release_tick, 1_000 + S553_LOCKOUT_TICKS);
    assert_eq!(drive(&mut state, Input::PinEntry(S553_DEFAULT_PIN)), Err(Error::LockedOut));
    assert_eq!(drive(&mut state, Input::UnlockGesture), Err(Error::LockedOut));
    drive(&mut state, Input::Tick(1_000 + S553_LOCKOUT_TICKS - 1)).unwrap();
    assert!(state.locked_out());
    assert_eq!(drive(&mut state, Input::PinEntry(S553_DEFAULT_PIN)), Err(Error::LockedOut));
    let Outcome::StatusUpdated(receipt) = drive(&mut state, Input::Tick(1_000 + S553_LOCKOUT_TICKS)).unwrap()
    else {
        panic!("tick must be a status update")
    };
    assert!(!receipt.locked_out);
    assert_eq!(receipt.lockout_release_tick, 0);
    assert!(matches!(
        drive(&mut state, Input::PinEntry(S553_DEFAULT_PIN)),
        Ok(Outcome::Transitioned(_))
    ));
    assert_eq!(state.current_scene(), Scene::Home);
}

#[test]
fn settings_are_unreachable_from_lock() {
    let mut state = G8lS553SystemUiState::new();
    assert_eq!(drive(&mut state, Input::OpenSettings), Err(Error::SettingsFromLockForbidden));
    assert_eq!(drive(&mut state, Input::OpenApp), Err(Error::InvalidTransition));
    assert_eq!(drive(&mut state, Input::Back), Err(Error::InvalidTransition));
    assert_eq!(drive(&mut state, Input::ToggleSetting(1)), Err(Error::InvalidTransition));
    assert_eq!(state.current_scene(), Scene::Lock);
    assert_eq!(state.accepted_events(), 0);
    assert_eq!(
        s553_lookup_transition(Scene::Lock, G8lS553EventKind::OpenSettings),
        Some(G8lS553TransitionAction::Reject(Error::SettingsFromLockForbidden))
    );
}

#[test]
fn scene_stack_depth_is_bounded_at_four() {
    let mut state = unlocked_home();
    drive(&mut state, Input::OpenSettings).unwrap();
    drive(&mut state, Input::OpenApp).unwrap();
    let Outcome::Transitioned(receipt) = drive(&mut state, Input::OpenSettings).unwrap() else {
        panic!("fourth push must be accepted")
    };
    assert_eq!(receipt.stack_depth, 4);
    assert_eq!(
        receipt.stack,
        [Scene::Home, Scene::Settings, Scene::App, Scene::Settings]
    );
    assert_eq!(drive(&mut state, Input::OpenApp), Err(Error::SceneStackOverflow));
    assert_eq!(state.stack_depth(), 4);
    assert_eq!(state.current_scene(), Scene::Settings);
    assert_eq!(state.accepted_events(), 4);
}

#[test]
fn back_pops_scenes_and_home_back_locks_the_device() {
    let mut state = unlocked_home();
    drive(&mut state, Input::OpenApp).unwrap();
    drive(&mut state, Input::OpenSettings).unwrap();
    let Outcome::Transitioned(receipt) = drive(&mut state, Input::Back).unwrap() else {
        panic!("back must pop")
    };
    assert_eq!(receipt.from_scene, Scene::Settings);
    assert_eq!(receipt.to_scene, Scene::App);
    assert_eq!(receipt.stack_depth, 2);
    assert_eq!(receipt.stack[2], Scene::Lock);
    drive(&mut state, Input::Back).unwrap();
    assert_eq!(state.current_scene(), Scene::Home);
    let Outcome::Transitioned(receipt) = drive(&mut state, Input::Back).unwrap() else {
        panic!("home back must lock")
    };
    assert_eq!(receipt.from_scene, Scene::Home);
    assert_eq!(receipt.to_scene, Scene::Lock);
    assert_eq!(receipt.stack_depth, 1);
    assert_eq!(receipt.stack[0], Scene::Lock);
    assert_eq!(drive(&mut state, Input::Back), Err(Error::InvalidTransition));
}

#[test]
fn toggle_setting_flips_flags_and_rejects_out_of_range_keys() {
    let mut state = unlocked_home();
    assert_eq!(drive(&mut state, Input::ToggleSetting(2)), Err(Error::InvalidTransition));
    drive(&mut state, Input::OpenSettings).unwrap();
    let Outcome::SettingToggled(receipt) = drive(&mut state, Input::ToggleSetting(7)).unwrap() else {
        panic!("last key must be toggleable")
    };
    assert_eq!(receipt.settings, [1, 0, 0, 0, 0, 0, 0, 1]);
    drive(&mut state, Input::ToggleSetting(7)).unwrap();
    assert_eq!(state.settings(), S553_DEFAULT_SETTINGS);
    assert_eq!(
        drive(&mut state, Input::ToggleSetting(S553_SETTINGS_CAPACITY as u8)),
        Err(Error::SettingKeyOutOfRange)
    );
    assert_eq!(drive(&mut state, Input::ToggleSetting(u8::MAX)), Err(Error::SettingKeyOutOfRange));
    assert_eq!(state.settings(), S553_DEFAULT_SETTINGS);
    assert!(state.settings().iter().all(|flag| *flag <= 1));
}

#[test]
fn status_bar_rejects_out_of_range_battery_and_signal() {
    let mut state = G8lS553SystemUiState::new();
    let Outcome::StatusUpdated(receipt) = drive(&mut state, Input::BatteryReport(100)).unwrap() else {
        panic!("battery 100 must be accepted")
    };
    assert_eq!(receipt.status_bar.battery_percent, 100);
    assert_eq!(receipt.to_scene, Scene::Lock);
    assert_eq!(drive(&mut state, Input::BatteryReport(101)), Err(Error::BatteryOutOfRange));
    let Outcome::StatusUpdated(receipt) = drive(&mut state, Input::SignalReport(5)).unwrap() else {
        panic!("signal 5 must be accepted")
    };
    assert_eq!(receipt.status_bar.signal_bars, 5);
    assert_eq!(receipt.status_bar.battery_percent, 100);
    assert_eq!(drive(&mut state, Input::SignalReport(6)), Err(Error::SignalOutOfRange));
    assert_eq!(drive(&mut state, Input::SignalReport(u8::MAX)), Err(Error::SignalOutOfRange));
    assert_eq!(state.status_bar().signal_bars, 5);
    assert_eq!(state.status_bar().battery_percent, 100);
    assert_eq!(state.accepted_events(), 2);
}

#[test]
fn clock_derives_hh_mm_from_ticks_and_wraps_at_midnight() {
    assert_eq!(s553_clock_from_ticks(0), (0, 0));
    assert_eq!(s553_clock_from_ticks(S553_TICKS_PER_MINUTE - 1), (0, 0));
    assert_eq!(s553_clock_from_ticks(S553_TICKS_PER_MINUTE), (0, 1));
    assert_eq!(s553_clock_from_ticks(S553_TICKS_PER_MINUTE * 61), (1, 1));
    assert_eq!(s553_clock_from_ticks(S553_TICKS_PER_MINUTE * 1_439), (23, 59));
    assert_eq!(s553_clock_from_ticks(S553_TICKS_PER_MINUTE * 1_440), (0, 0));
    assert_eq!(s553_clock_from_ticks(u64::MAX), (0, 18));
    assert_eq!(s553_clock_text(23, 59), *b"23:59");
    assert_eq!(s553_clock_text(7, 5), *b"07:05");

    let mut state = G8lS553SystemUiState::new();
    let Outcome::StatusUpdated(receipt) =
        drive(&mut state, Input::Tick(S553_TICKS_PER_MINUTE * (13 * 60 + 37))).unwrap()
    else {
        panic!("tick must update status")
    };
    assert_eq!(receipt.status_bar.clock_hour, 13);
    assert_eq!(receipt.status_bar.clock_minute, 37);
    assert_eq!(receipt.status_bar.clock_text, *b"13:37");
    assert_eq!(receipt.status_bar.tick_count, S553_TICKS_PER_MINUTE * 817);
    let Outcome::StatusUpdated(receipt) = drive(&mut state, Input::Tick(u64::MAX)).unwrap() else {
        panic!("max tick must update status")
    };
    assert_eq!(receipt.status_bar.clock_text, *b"00:18");
}

#[test]
fn tick_regression_and_lockout_arithmetic_overflow_fail_closed() {
    let mut state = G8lS553SystemUiState::new();
    drive(&mut state, Input::Tick(500)).unwrap();
    assert_eq!(drive(&mut state, Input::Tick(499)), Err(Error::TickRegression));
    assert!(matches!(drive(&mut state, Input::Tick(500)), Ok(Outcome::StatusUpdated(_))));
    drive(&mut state, Input::Tick(u64::MAX - 1)).unwrap();
    for _ in 0..S553_MAX_PIN_ATTEMPTS {
        drive(&mut state, Input::PinEntry([0, 0, 0, 0])).unwrap();
    }
    assert_eq!(state.pin_failed_attempts(), S553_MAX_PIN_ATTEMPTS);
    assert_eq!(drive(&mut state, Input::PinEntry([0, 0, 0, 0])), Err(Error::TickOverflow));
    assert_eq!(state.pin_failed_attempts(), S553_MAX_PIN_ATTEMPTS);
    assert_eq!(state.lockout_count(), 0);
    assert!(!state.locked_out());
    assert_eq!(state.accepted_events(), 3 + u64::from(S553_MAX_PIN_ATTEMPTS));
}

#[test]
fn lockout_counter_saturation_fails_closed() {
    let mut state = G8lS553SystemUiState::new();
    let mut tick = 0u64;
    for round in 1..=u8::MAX {
        for _ in 0..=S553_MAX_PIN_ATTEMPTS {
            drive(&mut state, Input::PinEntry([5, 5, 5, 5])).unwrap();
        }
        assert_eq!(state.lockout_count(), round);
        tick += S553_LOCKOUT_TICKS;
        drive(&mut state, Input::Tick(tick)).unwrap();
        assert!(!state.locked_out());
    }
    for _ in 0..S553_MAX_PIN_ATTEMPTS {
        drive(&mut state, Input::PinEntry([5, 5, 5, 5])).unwrap();
    }
    assert_eq!(
        drive(&mut state, Input::PinEntry([5, 5, 5, 5])),
        Err(Error::LockoutCounterOverflow)
    );
    assert_eq!(state.lockout_count(), u8::MAX);
    assert!(!state.locked_out());
    assert!(matches!(
        drive(&mut state, Input::PinEntry(S553_DEFAULT_PIN)),
        Ok(Outcome::Transitioned(_))
    ));
}

#[test]
fn event_ids_must_be_contiguous() {
    let mut state = G8lS553SystemUiState::new();
    assert_eq!(
        service_s553_model_scene_flow(&mut state, event(0, Input::Tick(1))),
        Err(Error::EventOrder)
    );
    assert_eq!(
        service_s553_model_scene_flow(&mut state, event(2, Input::Tick(1))),
        Err(Error::EventOrder)
    );
    service_s553_model_scene_flow(&mut state, event(1, Input::Tick(1))).unwrap();
    service_s553_model_scene_flow(&mut state, event(2, Input::Tick(2))).unwrap();
    assert_eq!(
        service_s553_model_scene_flow(&mut state, event(1, Input::Tick(1))),
        Err(Error::EventOrder)
    );
    assert_eq!(
        service_s553_model_scene_flow(&mut state, event(4, Input::Tick(4))),
        Err(Error::EventOrder)
    );
    assert_eq!(
        service_s553_model_scene_flow(&mut state, event(u64::MAX, Input::Tick(4))),
        Err(Error::EventOrder)
    );
    assert_eq!(state.accepted_events(), 2);
    assert_eq!(state.status_bar().tick_count, 2);
}

#[test]
fn pin_digits_out_of_range_are_rejected() {
    let mut state = G8lS553SystemUiState::new();
    for pin in [[10, 0, 0, 0], [0, 0, 0, 10], [1, 2, 3, 255]] {
        assert_eq!(drive(&mut state, Input::PinEntry(pin)), Err(Error::PinDigitOutOfRange));
    }
    assert_eq!(state.pin_failed_attempts(), 0);
    assert_eq!(state.accepted_events(), 0);
    assert_eq!(
        G8lS553SystemUiState::with_pin([0, 0, 1, 10]).err(),
        Some(Error::PinDigitOutOfRange)
    );
    let mut custom = G8lS553SystemUiState::with_pin([0, 9, 0, 9]).unwrap();
    assert!(matches!(
        drive(&mut custom, Input::PinEntry(S553_DEFAULT_PIN)),
        Ok(Outcome::UnlockRejected(_))
    ));
    assert!(matches!(
        drive(&mut custom, Input::PinEntry([0, 9, 0, 9])),
        Ok(Outcome::Transitioned(_))
    ));
}

#[test]
fn constant_time_pin_compare_examines_every_digit() {
    assert!(s553_pin_matches_constant_time([1, 2, 3, 4], [1, 2, 3, 4]));
    assert!(s553_pin_matches_constant_time([0, 0, 0, 0], [0, 0, 0, 0]));
    for position in 0..S553_PIN_DIGITS {
        let mut entered = S553_DEFAULT_PIN;
        entered[position] ^= 0b1000;
        assert!(!s553_pin_matches_constant_time(entered, S553_DEFAULT_PIN));
    }
    assert!(!s553_pin_matches_constant_time([4, 3, 2, 1], S553_DEFAULT_PIN));
    assert!(s553_pin_digits_are_valid([9, 9, 9, 9]));
    assert!(!s553_pin_digits_are_valid([9, 9, 9, 10]));
    let compare = SOURCE
        .split("pub fn s553_pin_matches_constant_time")
        .nth(1)
        .unwrap()
        .split("pub fn s553_pin_digits_are_valid")
        .next()
        .unwrap();
    assert!(compare.contains("difference |= entered[index] ^ expected[index]"));
    assert!(!compare.contains("return false"));
    assert!(!compare.contains("break"));
}

#[test]
fn transition_table_is_explicit_and_unambiguous() {
    assert_eq!(S553_TRANSITION_TABLE.len(), 11);
    let mut pairs = BTreeSet::new();
    for entry in S553_TRANSITION_TABLE {
        assert!(pairs.insert((entry.from.index(), entry.kind as u8)));
        assert!(!matches!(
            entry.kind,
            G8lS553EventKind::Tick | G8lS553EventKind::BatteryReport | G8lS553EventKind::SignalReport
        ));
        assert_eq!(s553_lookup_transition(entry.from, entry.kind), Some(entry.action));
    }
    assert!(!S553_TRANSITION_TABLE
        .iter()
        .any(|entry| entry.from == Scene::Lock
            && entry.action == G8lS553TransitionAction::Push(Scene::Settings)));
    assert_eq!(
        s553_lookup_transition(Scene::Home, G8lS553EventKind::PinEntry),
        None
    );
    assert_eq!(
        s553_lookup_transition(Scene::App, G8lS553EventKind::ToggleSetting),
        None
    );
    assert_eq!(
        s553_lookup_transition(Scene::Home, G8lS553EventKind::Back),
        Some(G8lS553TransitionAction::Lock)
    );
    assert_eq!(Scene::Lock.index(), 0);
    assert_eq!(Scene::App.index(), 3);
}

#[test]
fn invalid_transition_leaves_state_untouched() {
    let mut state = unlocked_home();
    drive(&mut state, Input::BatteryReport(42)).unwrap();
    let receipt = receipt_of(drive(&mut state, Input::OpenSettings).unwrap());
    assert_eq!(state.last_receipt(), Some(receipt));
    let scene = state.current_scene();
    let depth = state.stack_depth();
    let settings = state.settings();
    let status = state.status_bar();
    let accepted = state.accepted_events();
    for input in [
        Input::UnlockGesture,
        Input::PinEntry(S553_DEFAULT_PIN),
        Input::OpenSettings,
        Input::ToggleSetting(8),
        Input::BatteryReport(200),
        Input::SignalReport(9),
    ] {
        assert!(drive(&mut state, input).is_err());
    }
    assert_eq!(state.current_scene(), scene);
    assert_eq!(state.stack_depth(), depth);
    assert_eq!(state.settings(), settings);
    assert_eq!(state.status_bar(), status);
    assert_eq!(state.last_receipt(), Some(receipt));
    assert_eq!(state.accepted_events(), accepted);
    assert_eq!(scene, Scene::Settings);
    assert_eq!(status.battery_percent, 42);
}
snippet sha256: 60edc05d1b32file sha256: 60edc05d1b32
03 · Kapı kimlik kaydı

Operations sıra, kimlik ve başlık bağı

tam Operations kaydıL2721–L2780
website/src/lib/operations.ts::g8l-s553-r1-system-ui-lock-status-settings-scene-flow-model
  {
    id: "g8l-s553-r1-system-ui-lock-status-settings-scene-flow-model",
    date: "2026-08-30",
    sequence: 553,
    status: "passed",
    umbrella_status: "partial",
    title: "S553 · R1 ekran: kilit/durum/ayar ekranları system UI akış modeli",
    summary:
      "S553 kaynak/host model kapısı PASS'tir: kernel/src/ui/scene.rs retained scene soyutlamasının üzerinde Lock, Home (durum), Settings ve App (placeholder) sahnelerinden oluşan kontrollü system-UI akışı, 11 satırlık açık geçiş tablosu, sabit-zamanlı 4 haneli PIN karşılaştırması, 5 deneme sonrası kilitlenme sayacı, derinliği en fazla 4 olan sahne yığını, 8 slotluk u8 bayrak ayar deposu ve pil yüzdesi / sinyal çubuğu (0..5) / tick sayısından türetilen HH:MM saat içeren durum çubuğu olarak modellendi. Her kabul edilen olay sıra numaralı bir SceneReceipt üretir; geçersiz geçişler, aralık dışı değerler, tick gerilemesi, aritmetik taşma, yığın taşması, sıra dışı olay id'si ve yayın sonrası sapma fail-closed reddedilir ve durumu değiştirmez. Focused 24/24 PASS'tir. S540 ve S543 fiziksel RED değişmez kalır; hardware=none, physical observation=0, RUNBOOK_EXECUTED_IN_S553=NO, Boot-to-UI=false ve R1 acceptance=false'dur. S554 host-only modem AT komut taşıma çerçeveleme modeli kapısıdır.",
    evidence: [
      "S553, kernel/src/ui/scene.rs retained Scene/Layer soyutlamasının üzerine oturacak kontrollü system-UI akışını Lock, Home (durum), Settings ve App (placeholder) sahneleriyle salt kaynak/host modeli olarak tanımlar; hiçbir panel, dokunmatik denetleyici, framebuffer grant, modem, board veya UART bu kapıda mevcut değildir.",
      "Geçiş tablosu S553_TRANSITION_TABLE 11 açık satırdan oluşur: Lock→Home (gesture yalnız PIN_REQUIRED=0 iken, PinEntry sabit-zamanlı eşleşmede), Home→Settings/App push, Home Back→Lock, Settings ToggleSetting stay, Settings/App Back pop, Settings→App ve App→Settings push; tabloda olmayan her (sahne, olay) çifti InvalidTransition ile reddedilir.",
      "Lock sahnesinden OpenSettings açık bir guard satırıyla SettingsFromLockForbidden döner; Lock'tan OpenApp, Back ve ToggleSetting InvalidTransition'dır.",
      "PIN karşılaştırması s553_pin_matches_constant_time ile tüm 4 haneyi XOR-OR biriktirerek erken dönüşsüz yapılır; 9'dan büyük hane PinDigitOutOfRange ile reddedilir.",
      "S553_MAX_PIN_ATTEMPTS=5'tir; altıncı ardışık hatalı PIN LockedOut sonucu üretir, lockout_count checked u8 ile artar, deneme sayacı sıfırlanır ve lockout_release_tick=tick+3000 (30 s, 100 Hz) checked u64 ile hesaplanır; kilit süresince her unlock olayı Err(LockedOut) döner ve serbest bırakma tick'ine ulaşan Tick kilidi kaldırır.",
      "Sahne yığını S553_SCENE_STACK_MAX_DEPTH=4 ile sınırlıdır; dördüncü push kabul edilir, beşinci push SceneStackOverflow ile reddedilir ve yığın değişmez.",
      "Ayar deposu 8 slotluk u8 bayraktır (anahtar 0 = PIN_REQUIRED, varsayılan 1); ToggleSetting yalnız Settings sahnesinde, anahtar<8 ve değer<=1 iken kabul edilir, 8 ve 255 anahtarları SettingKeyOutOfRange döner.",
      "Durum çubuğu pil 0..=100, sinyal 0..=5 ve tick sayısından türetilen HH:MM saati (minute_of_day = tick/6000 % 1440) beş baytlık ASCII clock_text ile taşır; 101 pil, 6/255 sinyal ve tick gerilemesi fail-closed reddedilir; u64::MAX tick 00:18 olarak sarar.",
      "Her kabul edilen olay sequence=553, predecessor=552, artan transition_sequence, olay id/kind, from/to sahne, tam yığın, durum çubuğu, ayar anlık görüntüsü, deneme/lockout sayaçları, hardware_present=false, physical_observations=0 ve runbook_executed=false alanlı bir G8lS553SceneReceipt üretir.",
      "service_s553_model_scene_flow sonraki anlık görüntüyü yerel olarak hesaplar ve yalnız başarıda commit eder; olay id'leri bitişik olmak zorundadır (0, atlanmış, bayat ve u64::MAX id EventOrder), son kabul edilen olayın birebir tekrarı Retained ile aynı receipt'i döndürür, aynı id altında farklı payload PublishedEventDivergence'tır.",
      "On yedi hata kodu 1..=17 aralığında benzersiz ve sıfırdan farklıdır; TickOverflow (u64::MAX-1 tick'te altıncı hatalı PIN) ve LockoutCounterOverflow (255 lockout sonrası 256.) focused testte doğrulanmıştır.",
      "Focused target 1 grup / 24 passed / 0 failed / 0 ignored / 0 filtered verdi; 6 sözleşme testi ve 18 alan testi (mutlu yol, sınır değerleri, bozuk girdi, taşma, sıralama) içerir.",
      "Implementation 23913 B / cedaf20d38f88ff1914ff08e858b0240e57c279495ea0a9e86b40aef55cc1745; focused test 24302 B / 60edc05d1b322d449748d9761fd6a635e2edab6484672b8bbc5fe5b4f8f09f29 SHA-256'dır.",
      "Proof 6202 B'dır.",
      "Modül hiçbir boot, IRQ, scheduler veya sürücü yoluna bağlanmamıştır; unsafe, asm!, write_volatile, crate::uart, crate::arch, #[no_mangle] ve spin:: yüzeyi içermez.",
      "S540 immutable raw 20525 B ve S543 immutable raw 20509 B fiziksel RED kararlarıyla byte-exact korunur; automatic promotion=false ve rerun=false'dur.",
      "S553 sırasında candidate freeze, SD write/read-back/eject, UART open/capture, power transition, fiziksel koşu veya yeni immutable raw üretimi yapılmadı.",
      "RUNBOOK_EXECUTED_IN_S553=NO; supported-profile runtime observations=0, physical observations=0, hardware present=false, Boot-to-UI physically observed=false ve R1 acceptance=false'dur.",
      "S554 yalnız host üzerinde modem AT komut taşıma çerçeveleme modelini tanımlayacaktır; aygıt, modem veya fiziksel koşu yetkisi değildir.",
    ],
    commands: [
      "CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s553_r1_system_ui_lock_status_settings_scene_flow_model -- --test-threads=1",
    ],
    terminalSessions: [
      {
        id: "s553-focused",
        title: "S553 system UI scene flow model focused acceptance",
        commandLines: [
          "CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s553_r1_system_ui_lock_status_settings_scene_flow_model -- --test-threads=1",
        ],
        outputLines: [
          "test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s",
          "S553 focused=1 group / 24 passed / 0 failed",
          "hardware=none physical=0 runbook=NO",
        ],
        exitCode: 0,
        outputMode: "complete",
      },
    ],
    terminalSessionsNote:
      "S553 kaynak/host model PASS'tir; supported-profile runtime veya fiziksel PASS değildir. S540 ve S543 RED raw ve kararları değişmez.",
    limitations: [
      "S553 yalnız kaynak/host system-UI akış modelidir; hiçbir donanım/panel/modem/board gözlemi yoktur ve modül hiçbir üretim çağrı noktasına bağlanmamıştır.",
      "Sahne akışı kernel/src/ui/scene.rs Layer/Scene compositing'ine henüz bağlanmamıştır; gerçek framebuffer grant, dokunma girişi veya vsync olayı bu kapıda yoktur.",
      "Varsayılan PIN bir model fikstürüdür; gerçek kimlik doğrulama, güvenli depolama veya kalıcı ayar saklama modellenmemiştir.",
      "S540 ve S543 fiziksel RED immutable kalır; otomatik yükseltme veya yeniden koşu yapılmaz.",
      "Boot-to-UI fiziksel olarak gözlenmedi; R1 acceptance false kalır ve RUNBOOK_EXECUTED_IN_S553=NO'dur.",
      "S554 host-only modem AT komut taşıma çerçeveleme modeli tamamlanmadan R1 3. aşama (modem/ses) için kaynak modeli yoktur; yeni SD/UART/power koşusu ayrı kapı, fresh target revalidation, açık operatör yetkisi ve yeni immutable raw ister.",
    ],
  },
snippet sha256: 9af99df80a90file sha256: 9726dbf00f84
Focused test komutu
CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s553_r1_system_ui_lock_status_settings_scene_flow_model -- --test-threads=1
proof: docs/M8.1-RPi5-G8l-S553-R1-System-UI-Lock-Status-Settings-Scene-Flow-Model-Proof.md
Registry schema v5 · generator website/scripts/generate-code-gates.mjs · Tam SHA-256: 91d38c7b6222f0b4c117be786454853543da55a160e543d9b951057cc20dcc06