ASELSANMicrokernel
S563 · SOURCE-BOUND GATE EVIDENCE

S563 · R1 uygulama: sınırlı toparlanma ve hata sınırlama modeli

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

S563Focused kod testiOperations id exactsource SHA exacttest target exact

operation: g8l-s563-r1-bounded-recovery-fault-containment-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–L818
kernel/src/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s563_r1_bounded_recovery_fault_containment_model.rs::S563 r1 bounded recovery fault containment model implementation
#![allow(unexpected_cfgs)]

//! S563 models bounded recovery and fault containment for the R1 application
//! stage: six fault classes, three containment domains, a five-level
//! escalation ladder (`Contain -> RestartTask -> RestartGroup -> SafeMode ->
//! Halt`) with per-level budgets and cool-down ticks, a 64-entry fault
//! journal ring with a chained FNV-1a checksum and overflow accounting that
//! never loses counts, a safe-mode allowed-service allowlist, and a
//! deterministic decision function from `(fault, history)` to an action.
//!
//! This is a source/host model gate.  Nothing here is wired into a boot,
//! IRQ, scheduler, or driver path; no exception vector, watchdog, or task
//! teardown calls it.  It performs no device operation, emits no UART text,
//! and cannot claim a physical Boot-to-UI observation.  The S540 and S543
//! physical verdicts remain immutable RED.  Predecessor: S562 (service
//! kill/restart supervision model).  Next: S564 (update package manifest hash
//! chain model).

pub const S563_SEQUENCE: usize = 563;
pub const S563_EXPECTED_PREDECESSOR: usize = 562;
pub const S563_R1_STAGE: u8 = 4;
pub const S563_R1_RANGE_FIRST: usize = 536;
pub const S563_R1_RANGE_LAST: usize = 568;
pub const S563_FAULT_CLASS_COUNT: usize = 6;
pub const S563_CONTAINMENT_DOMAIN_COUNT: usize = 3;
pub const S563_ESCALATION_LEVEL_COUNT: usize = 5;
pub const S563_HALT_LEVEL: u8 = 4;
pub const S563_JOURNAL_CAPACITY: usize = 64;
pub const S563_JOURNAL_ENTRY_BYTES: usize = 16;
pub const S563_JOURNAL_CHECKSUM_BASIS: u32 = 0x811C_9DC5;
pub const S563_JOURNAL_CHECKSUM_PRIME: u32 = 0x0100_0193;
pub const S563_MAX_TRACKED_DOMAINS: usize = 16;
pub const S563_MAX_FAULT_SEQUENCE_LEN: usize = 256;
pub const S563_SAFE_MODE_ALLOWLIST_LEN: usize = 4;
pub const S563_SAFE_MODE_ALLOWED_SERVICES: [u16; S563_SAFE_MODE_ALLOWLIST_LEN] =
    [0x0001, 0x0002, 0x0003, 0x0004];
pub const S563_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS: usize = 0;
pub const S563_PHYSICAL_OBSERVATIONS: usize = 0;
pub const S563_PHYSICAL_OR_DEVICE_OPERATIONS: usize = 0;
pub const S563_SD_WRITES: usize = 0;
pub const S563_UART_OPENS: usize = 0;
pub const S563_POWER_TRANSITIONS: usize = 0;
pub const S563_NEW_IMMUTABLE_RAW_CAPTURES: usize = 0;
pub const S563_S540_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S563_S543_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S563_AUTOMATIC_PROMOTION: bool = false;
pub const S563_BOOT_TO_UI_PHYSICALLY_OBSERVED: bool = false;
pub const S563_HARDWARE_PRESENT: bool = false;
pub const S563_R1_ACCEPTANCE_COMPLETE: bool = false;
pub const RUNBOOK_EXECUTED_IN_S563: bool = false;

/// Fault classes recognised by the containment model.  The wire code is the
/// discriminant; every other code fails closed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum G8lS563FaultClass {
    DataAbort = 0,
    InstrAbort = 1,
    Oom = 2,
    IpcTimeout = 3,
    WatchdogMiss = 4,
    CapabilityViolation = 5,
}

impl G8lS563FaultClass {
    pub const ALL: [Self; S563_FAULT_CLASS_COUNT] = [
        Self::DataAbort,
        Self::InstrAbort,
        Self::Oom,
        Self::IpcTimeout,
        Self::WatchdogMiss,
        Self::CapabilityViolation,
    ];

    pub const fn from_code(code: u8) -> Option<Self> {
        match code {
            0 => Some(Self::DataAbort),
            1 => Some(Self::InstrAbort),
            2 => Some(Self::Oom),
            3 => Some(Self::IpcTimeout),
            4 => Some(Self::WatchdogMiss),
            5 => Some(Self::CapabilityViolation),
            _ => None,
        }
    }

    pub const fn code(self) -> u8 {
        self as u8
    }

    /// Lowest ladder level at which this class may be handled.
    pub const fn severity_floor(self) -> u8 {
        match self {
            Self::Oom | Self::IpcTimeout => 0,
            Self::DataAbort | Self::InstrAbort => 1,
            Self::WatchdogMiss | Self::CapabilityViolation => 2,
        }
    }
}

/// Containment domain of a fault.  Wider domains start higher on the ladder.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum G8lS563ContainmentDomain {
    Task = 0,
    ServiceGroup = 1,
    Subsystem = 2,
}

impl G8lS563ContainmentDomain {
    pub const ALL: [Self; S563_CONTAINMENT_DOMAIN_COUNT] =
        [Self::Task, Self::ServiceGroup, Self::Subsystem];

    pub const fn from_code(code: u8) -> Option<Self> {
        match code {
            0 => Some(Self::Task),
            1 => Some(Self::ServiceGroup),
            2 => Some(Self::Subsystem),
            _ => None,
        }
    }

    pub const fn code(self) -> u8 {
        self as u8
    }

    pub const fn domain_floor(self) -> u8 {
        match self {
            Self::Task => 0,
            Self::ServiceGroup => 1,
            Self::Subsystem => 2,
        }
    }
}

/// Recovery actions in ladder order; the level is the discriminant.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum G8lS563RecoveryAction {
    Contain = 0,
    RestartTask = 1,
    RestartGroup = 2,
    SafeMode = 3,
    Halt = 4,
}

impl G8lS563RecoveryAction {
    pub const fn from_level(level: u8) -> Option<Self> {
        match level {
            0 => Some(Self::Contain),
            1 => Some(Self::RestartTask),
            2 => Some(Self::RestartGroup),
            3 => Some(Self::SafeMode),
            4 => Some(Self::Halt),
            _ => None,
        }
    }

    pub const fn level(self) -> u8 {
        self as u8
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS563LadderLevel {
    pub action: G8lS563RecoveryAction,
    /// Faults tolerated at this level before escalating to the next one.
    pub budget: u8,
    /// Quiet ticks after which the domain decays back to its floor.
    pub cooldown_ticks: u64,
}

pub const S563_ESCALATION_LADDER: [G8lS563LadderLevel; S563_ESCALATION_LEVEL_COUNT] = [
    G8lS563LadderLevel {
        action: G8lS563RecoveryAction::Contain,
        budget: 3,
        cooldown_ticks: 16,
    },
    G8lS563LadderLevel {
        action: G8lS563RecoveryAction::RestartTask,
        budget: 3,
        cooldown_ticks: 64,
    },
    G8lS563LadderLevel {
        action: G8lS563RecoveryAction::RestartGroup,
        budget: 2,
        cooldown_ticks: 256,
    },
    G8lS563LadderLevel {
        action: G8lS563RecoveryAction::SafeMode,
        budget: 1,
        cooldown_ticks: 1024,
    },
    G8lS563LadderLevel {
        action: G8lS563RecoveryAction::Halt,
        budget: 0,
        cooldown_ticks: 0,
    },
];

pub const fn is_s563_safe_mode_service_allowed(service_id: u16) -> bool {
    let mut index = 0;
    while index < S563_SAFE_MODE_ALLOWLIST_LEN {
        if S563_SAFE_MODE_ALLOWED_SERVICES[index] == service_id {
            return true;
        }
        index += 1;
    }
    false
}

/// Raw fault event as the model receives it.  Codes are decoded fail-closed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS563FaultEvent {
    pub tick: u64,
    pub class_code: u8,
    pub domain_code: u8,
    pub domain_id: u16,
    pub service_id: u16,
}

/// Per-domain escalation history.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct G8lS563ContainmentHistory {
    pub level: u8,
    pub uses_at_level: u8,
    pub last_fault_tick: Option<u64>,
    pub halted: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS563Decision {
    pub action: G8lS563RecoveryAction,
    pub level: u8,
    pub uses_at_level: u8,
    pub decayed: bool,
    pub escalations: u8,
    pub halted: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS563SystemMode {
    Normal,
    SafeMode,
    Halted,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS563JournalEntry {
    pub tick: u64,
    pub class: G8lS563FaultClass,
    pub domain: G8lS563ContainmentDomain,
    pub domain_id: u16,
    pub service_id: u16,
    pub action: G8lS563RecoveryAction,
    pub level: u8,
}

impl G8lS563JournalEntry {
    pub const EMPTY: Self = Self {
        tick: 0,
        class: G8lS563FaultClass::DataAbort,
        domain: G8lS563ContainmentDomain::Task,
        domain_id: 0,
        service_id: 0,
        action: G8lS563RecoveryAction::Contain,
        level: 0,
    };

    pub const fn encode(&self) -> [u8; S563_JOURNAL_ENTRY_BYTES] {
        let tick = self.tick.to_le_bytes();
        let domain_id = self.domain_id.to_le_bytes();
        let service_id = self.service_id.to_le_bytes();
        [
            tick[0],
            tick[1],
            tick[2],
            tick[3],
            tick[4],
            tick[5],
            tick[6],
            tick[7],
            self.class.code(),
            self.domain.code(),
            domain_id[0],
            domain_id[1],
            service_id[0],
            service_id[1],
            self.action.level(),
            self.level,
        ]
    }
}

pub const fn s563_fnv1a32_extend(mut checksum: u32, bytes: &[u8]) -> u32 {
    let mut index = 0;
    while index < bytes.len() {
        checksum ^= bytes[index] as u32;
        checksum = checksum.wrapping_mul(S563_JOURNAL_CHECKSUM_PRIME);
        index += 1;
    }
    checksum
}

/// 64-entry ring.  Overflow evicts the oldest entry but `total_recorded`,
/// `dropped` and the chained checksum keep counting every entry ever seen.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS563FaultJournal {
    entries: [G8lS563JournalEntry; S563_JOURNAL_CAPACITY],
    head: usize,
    len: usize,
    total_recorded: u64,
    dropped: u64,
    checksum: u32,
}

impl G8lS563FaultJournal {
    pub const fn new() -> Self {
        Self {
            entries: [G8lS563JournalEntry::EMPTY; S563_JOURNAL_CAPACITY],
            head: 0,
            len: 0,
            total_recorded: 0,
            dropped: 0,
            checksum: S563_JOURNAL_CHECKSUM_BASIS,
        }
    }

    pub const fn len(&self) -> usize {
        self.len
    }

    pub const fn is_empty(&self) -> bool {
        self.len == 0
    }

    pub const fn total_recorded(&self) -> u64 {
        self.total_recorded
    }

    pub const fn dropped(&self) -> u64 {
        self.dropped
    }

    pub const fn checksum(&self) -> u32 {
        self.checksum
    }

    /// Oldest-first view; index 0 is the oldest retained entry.
    pub fn get(&self, index: usize) -> Option<G8lS563JournalEntry> {
        if index >= self.len {
            return None;
        }
        let start = (self.head + S563_JOURNAL_CAPACITY - self.len) % S563_JOURNAL_CAPACITY;
        Some(self.entries[(start + index) % S563_JOURNAL_CAPACITY])
    }

    pub fn record(
        &mut self,
        entry: G8lS563JournalEntry,
    ) -> Result<(), G8lS563BoundedRecoveryError> {
        let total = self
            .total_recorded
            .checked_add(1)
            .ok_or(G8lS563BoundedRecoveryError::JournalAccountingOverflow)?;
        if self.len == S563_JOURNAL_CAPACITY {
            self.dropped = self
                .dropped
                .checked_add(1)
                .ok_or(G8lS563BoundedRecoveryError::JournalAccountingOverflow)?;
        } else {
            self.len += 1;
        }
        self.entries[self.head] = entry;
        self.head = (self.head + 1) % S563_JOURNAL_CAPACITY;
        self.total_recorded = total;
        self.checksum = s563_fnv1a32_extend(self.checksum, &entry.encode());
        Ok(())
    }

    pub fn accounting_is_consistent(&self) -> bool {
        (self.len as u64)
            .checked_add(self.dropped)
            .map(|sum| sum == self.total_recorded)
            .unwrap_or(false)
            && self.len <= S563_JOURNAL_CAPACITY
    }
}

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

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS563BoundedRecoveryReceipt {
    pub sequence: usize,
    pub predecessor_sequence: usize,
    pub r1_stage: u8,
    pub input_digest: u32,
    pub fault_count: usize,
    pub tracked_domains: usize,
    pub action_counts: [u32; S563_ESCALATION_LEVEL_COUNT],
    pub class_counts: [u32; S563_FAULT_CLASS_COUNT],
    pub decays: u32,
    pub escalations: u32,
    pub journal_len: usize,
    pub journal_dropped: u64,
    pub journal_total: u64,
    pub journal_checksum: u32,
    pub final_mode: G8lS563SystemMode,
    pub safe_mode_entered_at_tick: Option<u64>,
    pub halted_at_tick: Option<u64>,
    pub s540_physical_verdict_retained_red: bool,
    pub s543_physical_verdict_retained_red: bool,
    pub automatic_promotion: bool,
    pub hardware_present: bool,
    pub physical_observations: usize,
    pub runbook_executed: bool,
}

#[derive(Debug)]
pub struct G8lS563BoundedRecoveryState {
    receipt: Option<G8lS563BoundedRecoveryReceipt>,
}

impl G8lS563BoundedRecoveryState {
    pub const fn new() -> Self {
        Self { receipt: None }
    }

    pub const fn receipt(&self) -> Option<G8lS563BoundedRecoveryReceipt> {
        self.receipt
    }
}

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

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS563BoundedRecoveryOutcome {
    Published(G8lS563BoundedRecoveryReceipt),
    Retained(G8lS563BoundedRecoveryReceipt),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS563BoundedRecoveryError {
    EmptyFaultSequence,
    FaultSequenceTooLong,
    NonMonotonicTick,
    UnknownFaultClass,
    UnknownContainmentDomain,
    ReservedServiceId,
    ServiceNotAllowedInSafeMode,
    FaultAfterHalt,
    DomainTableFull,
    JournalAccountingOverflow,
    JournalAccountingMismatch,
    CounterOverflow,
    PublishedStateDrift,
}

impl G8lS563BoundedRecoveryError {
    pub const fn diagnostic_code(self) -> u64 {
        match self {
            Self::EmptyFaultSequence => 1,
            Self::FaultSequenceTooLong => 2,
            Self::NonMonotonicTick => 3,
            Self::UnknownFaultClass => 4,
            Self::UnknownContainmentDomain => 5,
            Self::ReservedServiceId => 6,
            Self::ServiceNotAllowedInSafeMode => 7,
            Self::FaultAfterHalt => 8,
            Self::DomainTableFull => 9,
            Self::JournalAccountingOverflow => 10,
            Self::JournalAccountingMismatch => 11,
            Self::CounterOverflow => 12,
            Self::PublishedStateDrift => 13,
        }
    }
}

const fn max_u8(a: u8, b: u8) -> u8 {
    if a > b {
        a
    } else {
        b
    }
}

/// Deterministic decision function: `(fault, history) -> action`.
///
/// 1. a halted domain stays halted;
/// 2. the effective floor is `max(class severity floor, domain floor)`;
/// 3. if the quiet time since the last fault reaches the current level's
///    cool-down, the history decays to the floor with zero uses;
/// 4. the fault consumes one use; while the use count exceeds the level's
///    budget the ladder escalates one level (uses reset to 1);
/// 5. reaching the `Halt` level is terminal (budget 0, sticky).
pub fn decide_s563_action(
    class: G8lS563FaultClass,
    domain: G8lS563ContainmentDomain,
    tick: u64,
    history: &G8lS563ContainmentHistory,
) -> Result<G8lS563Decision, G8lS563BoundedRecoveryError> {
    if history.halted || history.level >= S563_HALT_LEVEL {
        return Ok(G8lS563Decision {
            action: G8lS563RecoveryAction::Halt,
            level: S563_HALT_LEVEL,
            uses_at_level: history.uses_at_level,
            decayed: false,
            escalations: 0,
            halted: true,
        });
    }
    let floor = max_u8(class.severity_floor(), domain.domain_floor());
    let mut level = max_u8(history.level, floor);
    let mut uses = history.uses_at_level;
    let mut decayed = false;
    if let Some(last) = history.last_fault_tick {
        let elapsed = tick
            .checked_sub(last)
            .ok_or(G8lS563BoundedRecoveryError::NonMonotonicTick)?;
        if elapsed >= S563_ESCALATION_LADDER[history.level as usize].cooldown_ticks {
            level = floor;
            decayed = true;
        }
    }
    if decayed || level != history.level {
        uses = 0;
    }
    uses = uses
        .checked_add(1)
        .ok_or(G8lS563BoundedRecoveryError::CounterOverflow)?;
    let mut escalations = 0u8;
    while level < S563_HALT_LEVEL && uses > S563_ESCALATION_LADDER[level as usize].budget {
        level += 1;
        uses = 1;
        escalations += 1;
    }
    let action = G8lS563RecoveryAction::from_level(level)
        .ok_or(G8lS563BoundedRecoveryError::CounterOverflow)?;
    Ok(G8lS563Decision {
        action,
        level,
        uses_at_level: uses,
        decayed,
        escalations,
        halted: level == S563_HALT_LEVEL,
    })
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct TrackedDomain {
    domain: G8lS563ContainmentDomain,
    domain_id: u16,
    history: G8lS563ContainmentHistory,
}

/// Bounded containment engine driven event by event.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS563ContainmentEngine {
    domains: [Option<TrackedDomain>; S563_MAX_TRACKED_DOMAINS],
    journal: G8lS563FaultJournal,
    mode: G8lS563SystemMode,
    last_tick: Option<u64>,
    fault_count: usize,
    action_counts: [u32; S563_ESCALATION_LEVEL_COUNT],
    class_counts: [u32; S563_FAULT_CLASS_COUNT],
    decays: u32,
    escalations: u32,
    safe_mode_entered_at_tick: Option<u64>,
    halted_at_tick: Option<u64>,
    input_digest: u32,
}

impl G8lS563ContainmentEngine {
    pub const fn new() -> Self {
        Self {
            domains: [None; S563_MAX_TRACKED_DOMAINS],
            journal: G8lS563FaultJournal::new(),
            mode: G8lS563SystemMode::Normal,
            last_tick: None,
            fault_count: 0,
            action_counts: [0; S563_ESCALATION_LEVEL_COUNT],
            class_counts: [0; S563_FAULT_CLASS_COUNT],
            decays: 0,
            escalations: 0,
            safe_mode_entered_at_tick: None,
            halted_at_tick: None,
            input_digest: S563_JOURNAL_CHECKSUM_BASIS,
        }
    }

    pub const fn mode(&self) -> G8lS563SystemMode {
        self.mode
    }

    pub const fn journal(&self) -> &G8lS563FaultJournal {
        &self.journal
    }

    pub const fn action_counts(&self) -> [u32; S563_ESCALATION_LEVEL_COUNT] {
        self.action_counts
    }

    pub fn history(
        &self,
        domain: G8lS563ContainmentDomain,
        domain_id: u16,
    ) -> Option<G8lS563ContainmentHistory> {
        self.domains.iter().flatten().find_map(|tracked| {
            (tracked.domain == domain && tracked.domain_id == domain_id).then_some(tracked.history)
        })
    }

    pub fn tracked_domains(&self) -> usize {
        self.domains.iter().flatten().count()
    }

    fn slot(
        &mut self,
        domain: G8lS563ContainmentDomain,
        domain_id: u16,
    ) -> Result<usize, G8lS563BoundedRecoveryError> {
        let mut first_free = None;
        for (index, entry) in self.domains.iter().enumerate() {
            match entry {
                Some(tracked) if tracked.domain == domain && tracked.domain_id == domain_id => {
                    return Ok(index)
                }
                None if first_free.is_none() => first_free = Some(index),
                _ => {}
            }
        }
        let index = first_free.ok_or(G8lS563BoundedRecoveryError::DomainTableFull)?;
        self.domains[index] = Some(TrackedDomain {
            domain,
            domain_id,
            history: G8lS563ContainmentHistory::default(),
        });
        Ok(index)
    }

    /// Applies one fault; every invalid input fails closed before any state
    /// mutation.
    pub fn apply(
        &mut self,
        event: G8lS563FaultEvent,
    ) -> Result<G8lS563Decision, G8lS563BoundedRecoveryError> {
        let class = G8lS563FaultClass::from_code(event.class_code)
            .ok_or(G8lS563BoundedRecoveryError::UnknownFaultClass)?;
        let domain = G8lS563ContainmentDomain::from_code(event.domain_code)
            .ok_or(G8lS563BoundedRecoveryError::UnknownContainmentDomain)?;
        if event.service_id == 0 {
            return Err(G8lS563BoundedRecoveryError::ReservedServiceId);
        }
        if let Some(last) = self.last_tick {
            if event.tick < last {
                return Err(G8lS563BoundedRecoveryError::NonMonotonicTick);
            }
        }
        match self.mode {
            G8lS563SystemMode::Halted => return Err(G8lS563BoundedRecoveryError::FaultAfterHalt),
            G8lS563SystemMode::SafeMode if !is_s563_safe_mode_service_allowed(event.service_id) => {
                return Err(G8lS563BoundedRecoveryError::ServiceNotAllowedInSafeMode)
            }
            _ => {}
        }
        let fault_count = self
            .fault_count
            .checked_add(1)
            .filter(|count| *count <= S563_MAX_FAULT_SEQUENCE_LEN)
            .ok_or(G8lS563BoundedRecoveryError::FaultSequenceTooLong)?;
        let index = self.slot(domain, event.domain_id)?;
        let history = self.domains[index]
            .map(|tracked| tracked.history)
            .ok_or(G8lS563BoundedRecoveryError::DomainTableFull)?;
        let decision = decide_s563_action(class, domain, event.tick, &history)?;
        let action_count = self.action_counts[decision.level as usize]
            .checked_add(1)
            .ok_or(G8lS563BoundedRecoveryError::CounterOverflow)?;
        let class_count = self.class_counts[class.code() as usize]
            .checked_add(1)
            .ok_or(G8lS563BoundedRecoveryError::CounterOverflow)?;
        let decays = self
            .decays
            .checked_add(u32::from(decision.decayed))
            .ok_or(G8lS563BoundedRecoveryError::CounterOverflow)?;
        let escalations = self
            .escalations
            .checked_add(u32::from(decision.escalations))
            .ok_or(G8lS563BoundedRecoveryError::CounterOverflow)?;
        self.journal.record(G8lS563JournalEntry {
            tick: event.tick,
            class,
            domain,
            domain_id: event.domain_id,
            service_id: event.service_id,
            action: decision.action,
            level: decision.level,
        })?;
        if let Some(tracked) = self.domains[index].as_mut() {
            tracked.history = G8lS563ContainmentHistory {
                level: decision.level,
                uses_at_level: decision.uses_at_level,
                last_fault_tick: Some(event.tick),
                halted: decision.halted,
            };
        }
        self.fault_count = fault_count;
        self.action_counts[decision.level as usize] = action_count;
        self.class_counts[class.code() as usize] = class_count;
        self.decays = decays;
        self.escalations = escalations;
        self.last_tick = Some(event.tick);
        self.input_digest = s563_fnv1a32_extend(
            self.input_digest,
            &[
                event.class_code,
                event.domain_code,
                event.domain_id.to_le_bytes()[0],
                event.domain_id.to_le_bytes()[1],
                event.service_id.to_le_bytes()[0],
                event.service_id.to_le_bytes()[1],
            ],
        );
        self.input_digest = s563_fnv1a32_extend(self.input_digest, &event.tick.to_le_bytes());
        match decision.action {
            G8lS563RecoveryAction::Halt => {
                self.mode = G8lS563SystemMode::Halted;
                if self.halted_at_tick.is_none() {
                    self.halted_at_tick = Some(event.tick);
                }
            }
            G8lS563RecoveryAction::SafeMode => {
                if self.mode == G8lS563SystemMode::Normal {
                    self.mode = G8lS563SystemMode::SafeMode;
                    self.safe_mode_entered_at_tick = Some(event.tick);
                }
            }
            _ => {}
        }
        Ok(decision)
    }

    pub fn receipt(&self) -> Result<G8lS563BoundedRecoveryReceipt, G8lS563BoundedRecoveryError> {
        if !self.journal.accounting_is_consistent()
            || self.journal.total_recorded() != self.fault_count as u64
        {
            return Err(G8lS563BoundedRecoveryError::JournalAccountingMismatch);
        }
        Ok(G8lS563BoundedRecoveryReceipt {
            sequence: S563_SEQUENCE,
            predecessor_sequence: S563_EXPECTED_PREDECESSOR,
            r1_stage: S563_R1_STAGE,
            input_digest: self.input_digest,
            fault_count: self.fault_count,
            tracked_domains: self.tracked_domains(),
            action_counts: self.action_counts,
            class_counts: self.class_counts,
            decays: self.decays,
            escalations: self.escalations,
            journal_len: self.journal.len(),
            journal_dropped: self.journal.dropped(),
            journal_total: self.journal.total_recorded(),
            journal_checksum: self.journal.checksum(),
            final_mode: self.mode,
            safe_mode_entered_at_tick: self.safe_mode_entered_at_tick,
            halted_at_tick: self.halted_at_tick,
            s540_physical_verdict_retained_red: S563_S540_PHYSICAL_VERDICT_RETAINED_RED,
            s543_physical_verdict_retained_red: S563_S543_PHYSICAL_VERDICT_RETAINED_RED,
            automatic_promotion: S563_AUTOMATIC_PROMOTION,
            hardware_present: S563_HARDWARE_PRESENT,
            physical_observations: S563_PHYSICAL_OBSERVATIONS,
            runbook_executed: RUNBOOK_EXECUTED_IN_S563,
        })
    }
}

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

/// Runs a complete fault sequence through a fresh engine and publishes the
/// resulting receipt.  Exact replay retains the receipt; any divergence after
/// publication fails closed.
pub fn service_s563_model_contain_faults(
    state: &mut G8lS563BoundedRecoveryState,
    faults: &[G8lS563FaultEvent],
) -> Result<G8lS563BoundedRecoveryOutcome, G8lS563BoundedRecoveryError> {
    if faults.is_empty() {
        return Err(G8lS563BoundedRecoveryError::EmptyFaultSequence);
    }
    if faults.len() > S563_MAX_FAULT_SEQUENCE_LEN {
        return Err(G8lS563BoundedRecoveryError::FaultSequenceTooLong);
    }
    let mut engine = G8lS563ContainmentEngine::new();
    for event in faults {
        engine.apply(*event)?;
    }
    let receipt = engine.receipt()?;
    if let Some(published) = state.receipt {
        if published != receipt {
            return Err(G8lS563BoundedRecoveryError::PublishedStateDrift);
        }
        return Ok(G8lS563BoundedRecoveryOutcome::Retained(published));
    }
    state.receipt = Some(receipt);
    Ok(G8lS563BoundedRecoveryOutcome::Published(receipt))
}
snippet sha256: c4e89737df17file sha256: c4e89737df17
02 · Doğrulayan test kodu

Operations komutuna bağlı focused test

tam dosyaL1–L710
simulation/tests/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s563_r1_bounded_recovery_fault_containment_model.rs::S563 r1 bounded recovery fault containment model focused tests
use aselsan_microkernel_simulation::g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s563_r1_bounded_recovery_fault_containment_model::*;
use std::collections::BTreeSet;

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

const INIT_SERVICE: u16 = 0x0001;
const APP_SERVICE: u16 = 0x0100;

fn event(
    tick: u64,
    class: G8lS563FaultClass,
    domain: G8lS563ContainmentDomain,
    domain_id: u16,
    service_id: u16,
) -> G8lS563FaultEvent {
    G8lS563FaultEvent {
        tick,
        class_code: class.code(),
        domain_code: domain.code(),
        domain_id,
        service_id,
    }
}

fn oom_burst(count: u64, service_id: u16) -> Vec<G8lS563FaultEvent> {
    (0..count)
        .map(|tick| {
            event(
                tick,
                G8lS563FaultClass::Oom,
                G8lS563ContainmentDomain::Task,
                7,
                service_id,
            )
        })
        .collect()
}

fn publish(
    state: &mut G8lS563BoundedRecoveryState,
    faults: &[G8lS563FaultEvent],
) -> Result<G8lS563BoundedRecoveryOutcome, G8lS563BoundedRecoveryError> {
    service_s563_model_contain_faults(state, faults)
}

#[test]
fn sequence_scope_and_nonpromotion_are_exact() {
    assert_eq!(S563_SEQUENCE, 563);
    assert_eq!(S563_EXPECTED_PREDECESSOR, 562);
    assert_eq!(S563_R1_STAGE, 4);
    assert_eq!(S563_R1_RANGE_FIRST, 536);
    assert_eq!(S563_R1_RANGE_LAST, 568);
    assert_eq!(S563_FAULT_CLASS_COUNT, 6);
    assert_eq!(S563_CONTAINMENT_DOMAIN_COUNT, 3);
    assert_eq!(S563_ESCALATION_LEVEL_COUNT, 5);
    assert_eq!(S563_HALT_LEVEL, 4);
    assert_eq!(S563_JOURNAL_CAPACITY, 64);
    assert_eq!(S563_JOURNAL_ENTRY_BYTES, 16);
    assert_eq!(S563_MAX_TRACKED_DOMAINS, 16);
    assert_eq!(S563_MAX_FAULT_SEQUENCE_LEN, 256);
    assert_eq!(S563_SAFE_MODE_ALLOWLIST_LEN, 4);
    assert_eq!(S563_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS, 0);
    assert_eq!(S563_PHYSICAL_OBSERVATIONS, 0);
    assert_eq!(S563_PHYSICAL_OR_DEVICE_OPERATIONS, 0);
    assert_eq!(S563_SD_WRITES, 0);
    assert_eq!(S563_UART_OPENS, 0);
    assert_eq!(S563_POWER_TRANSITIONS, 0);
    assert_eq!(S563_NEW_IMMUTABLE_RAW_CAPTURES, 0);
    assert!(S563_S540_PHYSICAL_VERDICT_RETAINED_RED);
    assert!(S563_S543_PHYSICAL_VERDICT_RETAINED_RED);
    assert!(!S563_AUTOMATIC_PROMOTION);
    assert!(!S563_BOOT_TO_UI_PHYSICALLY_OBSERVED);
    assert!(!S563_HARDWARE_PRESENT);
    assert!(!S563_R1_ACCEPTANCE_COMPLETE);
    assert!(!RUNBOOK_EXECUTED_IN_S563);
}

#[test]
fn module_is_registered_in_kernel_and_simulation() {
    let module = "g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s563_r1_bounded_recovery_fault_containment_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("performs no device operation"));
    assert!(SOURCE.contains("Nothing here is wired into a boot"));
    assert!(SOURCE.contains("S563_PHYSICAL_OBSERVATIONS: usize = 0"));
    assert!(SOURCE.contains("S563_HARDWARE_PRESENT: bool = false"));
    assert!(SOURCE.contains("S563_R1_ACCEPTANCE_COMPLETE: bool = false"));
    assert!(SOURCE.contains("RUNBOOK_EXECUTED_IN_S563: bool = false"));
}

#[test]
fn diagnostic_codes_are_nonzero_and_unique() {
    let errors = [
        G8lS563BoundedRecoveryError::EmptyFaultSequence,
        G8lS563BoundedRecoveryError::FaultSequenceTooLong,
        G8lS563BoundedRecoveryError::NonMonotonicTick,
        G8lS563BoundedRecoveryError::UnknownFaultClass,
        G8lS563BoundedRecoveryError::UnknownContainmentDomain,
        G8lS563BoundedRecoveryError::ReservedServiceId,
        G8lS563BoundedRecoveryError::ServiceNotAllowedInSafeMode,
        G8lS563BoundedRecoveryError::FaultAfterHalt,
        G8lS563BoundedRecoveryError::DomainTableFull,
        G8lS563BoundedRecoveryError::JournalAccountingOverflow,
        G8lS563BoundedRecoveryError::JournalAccountingMismatch,
        G8lS563BoundedRecoveryError::CounterOverflow,
        G8lS563BoundedRecoveryError::PublishedStateDrift,
    ];
    let codes: BTreeSet<_> = errors
        .into_iter()
        .map(G8lS563BoundedRecoveryError::diagnostic_code)
        .collect();
    assert_eq!(codes.len(), errors.len());
    assert!(!codes.contains(&0));
}

#[test]
fn exact_replay_retains_the_same_receipt() {
    let mut state = G8lS563BoundedRecoveryState::new();
    let faults = oom_burst(5, APP_SERVICE);
    let G8lS563BoundedRecoveryOutcome::Published(receipt) = publish(&mut state, &faults).unwrap()
    else {
        panic!("first publication missing")
    };
    assert_eq!(state.receipt(), Some(receipt));
    assert_eq!(receipt.sequence, S563_SEQUENCE);
    assert_eq!(receipt.predecessor_sequence, S563_EXPECTED_PREDECESSOR);
    assert_eq!(receipt.r1_stage, S563_R1_STAGE);
    assert_eq!(receipt.fault_count, 5);
    assert_eq!(receipt.final_mode, G8lS563SystemMode::Normal);
    assert!(receipt.s540_physical_verdict_retained_red);
    assert!(receipt.s543_physical_verdict_retained_red);
    assert!(!receipt.automatic_promotion);
    assert!(!receipt.hardware_present);
    assert_eq!(receipt.physical_observations, 0);
    assert!(!receipt.runbook_executed);
    assert_eq!(
        publish(&mut state, &faults),
        Ok(G8lS563BoundedRecoveryOutcome::Retained(receipt))
    );
}

#[test]
fn divergent_input_after_publication_fails_closed() {
    let mut state = G8lS563BoundedRecoveryState::new();
    let faults = oom_burst(5, APP_SERVICE);
    let published = publish(&mut state, &faults).unwrap();
    // Same decisions and counters, different service id: the input digest
    // still separates the two sequences.
    let same_shape = oom_burst(5, APP_SERVICE + 1);
    assert_eq!(
        publish(&mut state, &same_shape),
        Err(G8lS563BoundedRecoveryError::PublishedStateDrift)
    );
    let longer = oom_burst(6, APP_SERVICE);
    assert_eq!(
        publish(&mut state, &longer),
        Err(G8lS563BoundedRecoveryError::PublishedStateDrift)
    );
    let G8lS563BoundedRecoveryOutcome::Published(receipt) = published else {
        panic!("first publication missing")
    };
    assert_eq!(state.receipt(), Some(receipt));
}

#[test]
fn fault_class_and_domain_codes_round_trip_and_unknown_codes_fail_closed() {
    for (index, class) in G8lS563FaultClass::ALL.into_iter().enumerate() {
        assert_eq!(class.code() as usize, index);
        assert_eq!(G8lS563FaultClass::from_code(class.code()), Some(class));
        assert!(class.severity_floor() < S563_HALT_LEVEL);
    }
    for (index, domain) in G8lS563ContainmentDomain::ALL.into_iter().enumerate() {
        assert_eq!(domain.code() as usize, index);
        assert_eq!(G8lS563ContainmentDomain::from_code(domain.code()), Some(domain));
        assert_eq!(domain.domain_floor(), index as u8);
    }
    for code in 6..=u8::MAX {
        assert_eq!(G8lS563FaultClass::from_code(code), None);
    }
    for code in 3..=u8::MAX {
        assert_eq!(G8lS563ContainmentDomain::from_code(code), None);
    }
    let mut engine = G8lS563ContainmentEngine::new();
    let mut unknown_class = event(0, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 1, APP_SERVICE);
    unknown_class.class_code = 6;
    assert_eq!(
        engine.apply(unknown_class),
        Err(G8lS563BoundedRecoveryError::UnknownFaultClass)
    );
    let mut unknown_domain = event(0, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 1, APP_SERVICE);
    unknown_domain.domain_code = 3;
    assert_eq!(
        engine.apply(unknown_domain),
        Err(G8lS563BoundedRecoveryError::UnknownContainmentDomain)
    );
    assert_eq!(engine.journal().len(), 0);
    assert_eq!(engine.tracked_domains(), 0);
    let mut state = G8lS563BoundedRecoveryState::new();
    assert_eq!(
        publish(&mut state, &[unknown_domain]),
        Err(G8lS563BoundedRecoveryError::UnknownContainmentDomain)
    );
    assert_eq!(state.receipt(), None);
}

#[test]
fn escalation_ladder_table_is_monotonic_with_terminal_halt() {
    for (level, entry) in S563_ESCALATION_LADDER.iter().enumerate() {
        assert_eq!(entry.action.level() as usize, level);
        assert_eq!(G8lS563RecoveryAction::from_level(level as u8), Some(entry.action));
    }
    assert_eq!(G8lS563RecoveryAction::from_level(5), None);
    let budgets: Vec<u8> = S563_ESCALATION_LADDER.iter().map(|entry| entry.budget).collect();
    assert_eq!(budgets, vec![3, 3, 2, 1, 0]);
    let cooldowns: Vec<u64> = S563_ESCALATION_LADDER
        .iter()
        .map(|entry| entry.cooldown_ticks)
        .collect();
    assert_eq!(cooldowns, vec![16, 64, 256, 1024, 0]);
    assert!(cooldowns[..4].windows(2).all(|pair| pair[0] < pair[1]));
    assert!(budgets.windows(2).all(|pair| pair[0] >= pair[1]));
    let halt = S563_ESCALATION_LADDER[S563_HALT_LEVEL as usize];
    assert_eq!(halt.action, G8lS563RecoveryAction::Halt);
    assert_eq!(halt.budget, 0);
    assert_eq!(halt.cooldown_ticks, 0);
}

#[test]
fn single_task_oom_is_contained_at_level_zero() {
    let mut engine = G8lS563ContainmentEngine::new();
    let decision = engine
        .apply(event(10, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 3, APP_SERVICE))
        .unwrap();
    assert_eq!(
        decision,
        G8lS563Decision {
            action: G8lS563RecoveryAction::Contain,
            level: 0,
            uses_at_level: 1,
            decayed: false,
            escalations: 0,
            halted: false,
        }
    );
    assert_eq!(engine.mode(), G8lS563SystemMode::Normal);
    assert_eq!(
        engine.history(G8lS563ContainmentDomain::Task, 3),
        Some(G8lS563ContainmentHistory {
            level: 0,
            uses_at_level: 1,
            last_fault_tick: Some(10),
            halted: false,
        })
    );
    assert_eq!(engine.history(G8lS563ContainmentDomain::Task, 4), None);
    let entry = engine.journal().get(0).unwrap();
    assert_eq!(entry.tick, 10);
    assert_eq!(entry.class, G8lS563FaultClass::Oom);
    assert_eq!(entry.domain, G8lS563ContainmentDomain::Task);
    assert_eq!(entry.domain_id, 3);
    assert_eq!(entry.service_id, APP_SERVICE);
    assert_eq!(entry.action, G8lS563RecoveryAction::Contain);
    assert_eq!(entry.level, 0);
    assert_eq!(engine.journal().get(1), None);
}

#[test]
fn repeated_faults_walk_the_ladder_to_halt_with_exact_budgets() {
    let mut engine = G8lS563ContainmentEngine::new();
    let mut actions = Vec::new();
    for tick in 0..10 {
        let decision = engine
            .apply(event(tick, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 7, INIT_SERVICE))
            .unwrap();
        actions.push(decision.action);
    }
    use G8lS563RecoveryAction::*;
    assert_eq!(
        actions,
        vec![
            Contain, Contain, Contain, RestartTask, RestartTask, RestartTask, RestartGroup,
            RestartGroup, SafeMode, Halt,
        ]
    );
    assert_eq!(engine.action_counts(), [3, 3, 2, 1, 1]);
    assert_eq!(engine.mode(), G8lS563SystemMode::Halted);
    let receipt = engine.receipt().unwrap();
    assert_eq!(receipt.safe_mode_entered_at_tick, Some(8));
    assert_eq!(receipt.halted_at_tick, Some(9));
    assert_eq!(receipt.escalations, 4);
    assert_eq!(receipt.decays, 0);
    assert_eq!(receipt.class_counts, [0, 0, 10, 0, 0, 0]);
    assert_eq!(receipt.tracked_domains, 1);
    assert_eq!(
        engine.history(G8lS563ContainmentDomain::Task, 7).map(|history| history.halted),
        Some(true)
    );
}

#[test]
fn budget_exhaustion_at_safe_mode_fails_closed_to_halt() {
    let history = G8lS563ContainmentHistory {
        level: 3,
        uses_at_level: 1,
        last_fault_tick: Some(100),
        halted: false,
    };
    let decision = decide_s563_action(
        G8lS563FaultClass::IpcTimeout,
        G8lS563ContainmentDomain::Task,
        101,
        &history,
    )
    .unwrap();
    assert_eq!(decision.action, G8lS563RecoveryAction::Halt);
    assert_eq!(decision.level, S563_HALT_LEVEL);
    assert!(decision.halted);
    assert_eq!(decision.escalations, 1);
    let halted = G8lS563ContainmentHistory {
        halted: true,
        level: S563_HALT_LEVEL,
        ..history
    };
    let sticky = decide_s563_action(
        G8lS563FaultClass::Oom,
        G8lS563ContainmentDomain::Task,
        5000,
        &halted,
    )
    .unwrap();
    assert_eq!(sticky.action, G8lS563RecoveryAction::Halt);
    assert!(!sticky.decayed);
    assert!(sticky.halted);
}

#[test]
fn cooldown_decays_history_back_to_the_floor() {
    let mut engine = G8lS563ContainmentEngine::new();
    for tick in 0..4 {
        engine
            .apply(event(tick, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 1, APP_SERVICE))
            .unwrap();
    }
    assert_eq!(
        engine.history(G8lS563ContainmentDomain::Task, 1).map(|history| history.level),
        Some(1)
    );
    // Level 1 cool-down is 64 ticks; 63 quiet ticks are not enough.
    let still_hot = engine
        .apply(event(3 + 63, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 1, APP_SERVICE))
        .unwrap();
    assert_eq!(still_hot.action, G8lS563RecoveryAction::RestartTask);
    assert!(!still_hot.decayed);
    assert_eq!(still_hot.uses_at_level, 2);
    // Exactly 64 quiet ticks decay to the floor with a single use.
    let decayed = engine
        .apply(event(66 + 64, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 1, APP_SERVICE))
        .unwrap();
    assert_eq!(decayed.action, G8lS563RecoveryAction::Contain);
    assert!(decayed.decayed);
    assert_eq!(decayed.uses_at_level, 1);
    assert_eq!(decayed.escalations, 0);
    assert_eq!(engine.receipt().unwrap().decays, 1);
}

#[test]
fn severity_and_domain_floors_start_higher_on_the_ladder() {
    let fresh = G8lS563ContainmentHistory::default();
    let cases = [
        (G8lS563FaultClass::IpcTimeout, G8lS563ContainmentDomain::Task, G8lS563RecoveryAction::Contain),
        (G8lS563FaultClass::DataAbort, G8lS563ContainmentDomain::Task, G8lS563RecoveryAction::RestartTask),
        (G8lS563FaultClass::InstrAbort, G8lS563ContainmentDomain::Task, G8lS563RecoveryAction::RestartTask),
        (G8lS563FaultClass::Oom, G8lS563ContainmentDomain::ServiceGroup, G8lS563RecoveryAction::RestartTask),
        (G8lS563FaultClass::WatchdogMiss, G8lS563ContainmentDomain::Task, G8lS563RecoveryAction::RestartGroup),
        (G8lS563FaultClass::CapabilityViolation, G8lS563ContainmentDomain::ServiceGroup, G8lS563RecoveryAction::RestartGroup),
        (G8lS563FaultClass::IpcTimeout, G8lS563ContainmentDomain::Subsystem, G8lS563RecoveryAction::RestartGroup),
    ];
    for (class, domain, expected) in cases {
        let decision = decide_s563_action(class, domain, 0, &fresh).unwrap();
        assert_eq!(decision.action, expected, "{class:?}/{domain:?}");
        assert_eq!(decision.uses_at_level, 1);
        assert_eq!(decision.escalations, 0);
    }
    // A floor above the current level resets the use counter.
    let low = G8lS563ContainmentHistory {
        level: 0,
        uses_at_level: 3,
        last_fault_tick: Some(0),
        halted: false,
    };
    let lifted = decide_s563_action(
        G8lS563FaultClass::WatchdogMiss,
        G8lS563ContainmentDomain::Task,
        1,
        &low,
    )
    .unwrap();
    assert_eq!(lifted.action, G8lS563RecoveryAction::RestartGroup);
    assert_eq!(lifted.uses_at_level, 1);
    // A subsystem never decays below its own floor.
    let hot_subsystem = G8lS563ContainmentHistory {
        level: 3,
        uses_at_level: 1,
        last_fault_tick: Some(0),
        halted: false,
    };
    let decayed = decide_s563_action(
        G8lS563FaultClass::Oom,
        G8lS563ContainmentDomain::Subsystem,
        1024,
        &hot_subsystem,
    )
    .unwrap();
    assert!(decayed.decayed);
    assert_eq!(decayed.action, G8lS563RecoveryAction::RestartGroup);
}

#[test]
fn journal_ring_wraps_at_64_without_losing_counts() {
    let mut journal = G8lS563FaultJournal::new();
    assert!(journal.is_empty());
    assert_eq!(journal.checksum(), S563_JOURNAL_CHECKSUM_BASIS);
    for tick in 0..100u64 {
        journal
            .record(G8lS563JournalEntry {
                tick,
                ..G8lS563JournalEntry::EMPTY
            })
            .unwrap();
        assert!(journal.accounting_is_consistent());
    }
    assert_eq!(journal.len(), 64);
    assert_eq!(journal.total_recorded(), 100);
    assert_eq!(journal.dropped(), 36);
    assert_eq!(journal.get(0).map(|entry| entry.tick), Some(36));
    assert_eq!(journal.get(63).map(|entry| entry.tick), Some(99));
    assert_eq!(journal.get(64), None);
    let mut engine = G8lS563ContainmentEngine::new();
    for tick in 0..80u64 {
        // Alternate 16 task domains so no single domain escalates past
        // RestartGroup: 5 faults per domain over 80 ticks.
        let domain_id = (tick % 16) as u16;
        engine
            .apply(event(tick, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, domain_id, APP_SERVICE))
            .unwrap();
    }
    let receipt = engine.receipt().unwrap();
    assert_eq!(receipt.fault_count, 80);
    assert_eq!(receipt.journal_len, 64);
    assert_eq!(receipt.journal_dropped, 16);
    assert_eq!(receipt.journal_total, 80);
    assert_eq!(receipt.tracked_domains, 16);
    assert_eq!(receipt.final_mode, G8lS563SystemMode::Normal);
}

#[test]
fn journal_checksum_is_deterministic_and_order_sensitive() {
    let a = G8lS563JournalEntry {
        tick: 1,
        class: G8lS563FaultClass::DataAbort,
        domain: G8lS563ContainmentDomain::Task,
        domain_id: 0x1234,
        service_id: 0xABCD,
        action: G8lS563RecoveryAction::RestartTask,
        level: 1,
    };
    let b = G8lS563JournalEntry {
        tick: 2,
        class: G8lS563FaultClass::WatchdogMiss,
        ..a
    };
    assert_eq!(
        a.encode(),
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x34, 0x12, 0xCD, 0xAB, 1, 1]
    );
    assert_eq!(s563_fnv1a32_extend(S563_JOURNAL_CHECKSUM_BASIS, b""), 0x811C_9DC5);
    assert_eq!(s563_fnv1a32_extend(S563_JOURNAL_CHECKSUM_BASIS, b"a"), 0xE40C_292C);
    let mut ab = G8lS563FaultJournal::new();
    ab.record(a).unwrap();
    ab.record(b).unwrap();
    let mut ab_again = G8lS563FaultJournal::new();
    ab_again.record(a).unwrap();
    ab_again.record(b).unwrap();
    let mut ba = G8lS563FaultJournal::new();
    ba.record(b).unwrap();
    ba.record(a).unwrap();
    assert_eq!(ab.checksum(), ab_again.checksum());
    assert_ne!(ab.checksum(), ba.checksum());
    assert_eq!(
        ab.checksum(),
        s563_fnv1a32_extend(
            s563_fnv1a32_extend(S563_JOURNAL_CHECKSUM_BASIS, &a.encode()),
            &b.encode()
        )
    );
    // The checksum keeps covering evicted entries after the ring wraps.
    let mut full = G8lS563FaultJournal::new();
    let mut expected = S563_JOURNAL_CHECKSUM_BASIS;
    for tick in 0..70u64 {
        let entry = G8lS563JournalEntry { tick, ..a };
        expected = s563_fnv1a32_extend(expected, &entry.encode());
        full.record(entry).unwrap();
    }
    assert_eq!(full.checksum(), expected);
    assert_eq!(full.dropped(), 6);
}

#[test]
fn safe_mode_rejects_non_allowlisted_services() {
    assert_eq!(S563_SAFE_MODE_ALLOWED_SERVICES, [1, 2, 3, 4]);
    for service in S563_SAFE_MODE_ALLOWED_SERVICES {
        assert!(is_s563_safe_mode_service_allowed(service));
    }
    assert!(!is_s563_safe_mode_service_allowed(0));
    assert!(!is_s563_safe_mode_service_allowed(5));
    assert!(!is_s563_safe_mode_service_allowed(APP_SERVICE));
    assert!(!is_s563_safe_mode_service_allowed(u16::MAX));
    let mut engine = G8lS563ContainmentEngine::new();
    // A subsystem capability violation escalates: RG, RG, SafeMode.
    for tick in 0..3 {
        engine
            .apply(event(
                tick,
                G8lS563FaultClass::CapabilityViolation,
                G8lS563ContainmentDomain::Subsystem,
                1,
                APP_SERVICE,
            ))
            .unwrap();
    }
    assert_eq!(engine.mode(), G8lS563SystemMode::SafeMode);
    assert_eq!(engine.action_counts(), [0, 0, 2, 1, 0]);
    let snapshot = engine;
    assert_eq!(
        engine.apply(event(3, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 9, APP_SERVICE)),
        Err(G8lS563BoundedRecoveryError::ServiceNotAllowedInSafeMode)
    );
    assert_eq!(engine, snapshot, "rejected fault must not mutate the engine");
    let allowed = engine
        .apply(event(3, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 9, INIT_SERVICE))
        .unwrap();
    assert_eq!(allowed.action, G8lS563RecoveryAction::Contain);
    assert_eq!(engine.mode(), G8lS563SystemMode::SafeMode);
    assert_eq!(engine.receipt().unwrap().safe_mode_entered_at_tick, Some(2));
}

#[test]
fn faults_after_halt_fail_closed() {
    let mut state = G8lS563BoundedRecoveryState::new();
    let mut faults = oom_burst(10, INIT_SERVICE);
    let G8lS563BoundedRecoveryOutcome::Published(receipt) = publish(&mut state, &faults).unwrap()
    else {
        panic!("first publication missing")
    };
    assert_eq!(receipt.final_mode, G8lS563SystemMode::Halted);
    assert_eq!(receipt.halted_at_tick, Some(9));
    faults.push(event(10, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 99, INIT_SERVICE));
    let mut fresh = G8lS563BoundedRecoveryState::new();
    assert_eq!(
        publish(&mut fresh, &faults),
        Err(G8lS563BoundedRecoveryError::FaultAfterHalt)
    );
    assert_eq!(fresh.receipt(), None);
}

#[test]
fn non_monotonic_ticks_and_reserved_service_fail_closed() {
    let mut engine = G8lS563ContainmentEngine::new();
    engine
        .apply(event(50, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 1, APP_SERVICE))
        .unwrap();
    assert_eq!(
        engine.apply(event(49, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 1, APP_SERVICE)),
        Err(G8lS563BoundedRecoveryError::NonMonotonicTick)
    );
    assert_eq!(
        engine.apply(event(50, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 1, 0)),
        Err(G8lS563BoundedRecoveryError::ReservedServiceId)
    );
    // Equal ticks are accepted (same-tick faults are ordered by arrival).
    assert!(engine
        .apply(event(50, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 2, APP_SERVICE))
        .is_ok());
    let stale = G8lS563ContainmentHistory {
        level: 0,
        uses_at_level: 1,
        last_fault_tick: Some(10),
        halted: false,
    };
    assert_eq!(
        decide_s563_action(G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 9, &stale),
        Err(G8lS563BoundedRecoveryError::NonMonotonicTick)
    );
    assert_eq!(engine.journal().total_recorded(), 2);
}

#[test]
fn empty_and_oversized_sequences_fail_closed() {
    let mut state = G8lS563BoundedRecoveryState::new();
    assert_eq!(
        publish(&mut state, &[]),
        Err(G8lS563BoundedRecoveryError::EmptyFaultSequence)
    );
    let oversized: Vec<G8lS563FaultEvent> = (0..=S563_MAX_FAULT_SEQUENCE_LEN as u64)
        .map(|tick| {
            event(tick * 20, G8lS563FaultClass::IpcTimeout, G8lS563ContainmentDomain::Task, 1, APP_SERVICE)
        })
        .collect();
    assert_eq!(oversized.len(), 257);
    assert_eq!(
        publish(&mut state, &oversized),
        Err(G8lS563BoundedRecoveryError::FaultSequenceTooLong)
    );
    let maximal = &oversized[..S563_MAX_FAULT_SEQUENCE_LEN];
    let G8lS563BoundedRecoveryOutcome::Published(receipt) = publish(&mut state, maximal).unwrap()
    else {
        panic!("maximal sequence must publish")
    };
    // 20-tick spacing exceeds the level-0 cool-down, so every fault decays.
    assert_eq!(receipt.fault_count, 256);
    assert_eq!(receipt.action_counts, [256, 0, 0, 0, 0]);
    assert_eq!(receipt.decays, 255);
    assert_eq!(receipt.journal_dropped, 192);
    let mut engine = G8lS563ContainmentEngine::new();
    for fault in maximal {
        engine.apply(*fault).unwrap();
    }
    assert_eq!(
        engine.apply(oversized[256]),
        Err(G8lS563BoundedRecoveryError::FaultSequenceTooLong)
    );
}

#[test]
fn domain_table_full_fails_closed() {
    let mut engine = G8lS563ContainmentEngine::new();
    for domain_id in 0..S563_MAX_TRACKED_DOMAINS as u16 {
        engine
            .apply(event(0, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, domain_id, APP_SERVICE))
            .unwrap();
    }
    assert_eq!(engine.tracked_domains(), 16);
    let snapshot = engine;
    assert_eq!(
        engine.apply(event(1, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::ServiceGroup, 0, APP_SERVICE)),
        Err(G8lS563BoundedRecoveryError::DomainTableFull)
    );
    assert_eq!(engine, snapshot);
    // The same (domain, id) key is still accepted; a different domain kind
    // with the same id is a distinct key.
    assert!(engine
        .apply(event(1, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::Task, 0, APP_SERVICE))
        .is_ok());
    assert_eq!(engine.journal().total_recorded(), 17);
}

#[test]
fn receipt_action_counters_match_journal_entries() {
    let mut engine = G8lS563ContainmentEngine::new();
    let script = [
        (0, G8lS563FaultClass::IpcTimeout, G8lS563ContainmentDomain::Task, 1),
        (1, G8lS563FaultClass::DataAbort, G8lS563ContainmentDomain::Task, 1),
        (2, G8lS563FaultClass::InstrAbort, G8lS563ContainmentDomain::Task, 2),
        (3, G8lS563FaultClass::Oom, G8lS563ContainmentDomain::ServiceGroup, 5),
        (4, G8lS563FaultClass::WatchdogMiss, G8lS563ContainmentDomain::ServiceGroup, 5),
        (5, G8lS563FaultClass::CapabilityViolation, G8lS563ContainmentDomain::Subsystem, 1),
    ];
    for (tick, class, domain, id) in script {
        engine.apply(event(tick, class, domain, id, APP_SERVICE)).unwrap();
    }
    let receipt = engine.receipt().unwrap();
    assert_eq!(receipt.class_counts, [1, 1, 1, 1, 1, 1]);
    assert_eq!(receipt.action_counts, [1, 3, 2, 0, 0]);
    assert_eq!(receipt.tracked_domains, 4);
    assert_eq!(receipt.journal_len, 6);
    assert_eq!(receipt.journal_total, 6);
    assert_eq!(receipt.journal_dropped, 0);
    let mut counted = [0u32; S563_ESCALATION_LEVEL_COUNT];
    for index in 0..receipt.journal_len {
        let entry = engine.journal().get(index).unwrap();
        counted[entry.level as usize] += 1;
        assert_eq!(entry.action.level(), entry.level);
    }
    assert_eq!(counted, receipt.action_counts);
    assert_eq!(receipt.action_counts.iter().sum::<u32>() as usize, receipt.fault_count);
    assert_eq!(receipt.journal_checksum, engine.journal().checksum());
    assert_ne!(receipt.journal_checksum, S563_JOURNAL_CHECKSUM_BASIS);
}
snippet sha256: ef6ad34dfb83file sha256: ef6ad34dfb83
03 · Kapı kimlik kaydı

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

tam Operations kaydıL2131–L2189
website/src/lib/operations.ts::g8l-s563-r1-bounded-recovery-fault-containment-model
  {
    id: "g8l-s563-r1-bounded-recovery-fault-containment-model",
    date: "2026-08-30",
    sequence: 563,
    status: "passed",
    umbrella_status: "partial",
    title: "S563 · R1 uygulama: sınırlı toparlanma ve hata sınırlama modeli",
    summary:
      "S563 kaynak/host model kapısı PASS'tir: altı fault sınıfı (DataAbort, InstrAbort, Oom, IpcTimeout, WatchdogMiss, CapabilityViolation), üç containment domain'i (task, service group, subsystem), seviye başına bütçe ve cool-down tick'leri taşıyan beş seviyeli Contain → RestartTask → RestartGroup → SafeMode → Halt eskalasyon merdiveni, zincirli FNV-1a-32 checksum'lı ve overflow'da sayım kaybetmeyen 64 girişlik fault journal ring'i, safe-mode allowed-service allowlist'i ve (fault, history) → action deterministik karar fonksiyonu modellenmiştir. Bütçe tükenmesi fail-closed Halt'a iner; bilinmeyen sınıf/domain kodları, rezerve service id, monoton olmayan tick, safe-mode dışı servis, halt sonrası fault, dolu domain tablosu ve 256 üstü diziler reddedilir. Focused 21/21 PASS'tir; modül kernel ve simulation'da kayıtlıdır fakat hiçbir boot, IRQ, scheduler veya watchdog yoluna bağlı değildir. S540 ve S543 fiziksel RED immutable kalır; physical observation=0, RUNBOOK_EXECUTED_IN_S563=NO, Boot-to-UI=false ve R1 acceptance=false'dur. S564 aynı R1 4. aşamanın host-only update paket manifest/hash zinciri model kapısıdır.",
    evidence: [
      "S563, S562'den ayrı kernel model modülü, 21-test focused binary, proof, status bloğu, Operations kaydı ve complete Code kartına sahiptir; modül kernel main.rs ve simulation lib.rs'te kayıtlıdır fakat hiçbir boot, exception, IRQ, scheduler, watchdog veya sürücü yoluna bağlanmamıştır.",
      "Dar S563 source/host model status=PASS; R1 umbrella=PARTIAL, S540 ve S543 physical gate status=RED olarak ayrı tutulur.",
      "Fault sınıfı kümesi kapalıdır: DataAbort(0), InstrAbort(1), Oom(2), IpcTimeout(3), WatchdogMiss(4), CapabilityViolation(5); 6 ve üzeri her kod UnknownFaultClass ile fail-closed reddedilir.",
      "Containment domain kümesi kapalıdır: Task(0, taban 0), ServiceGroup(1, taban 1), Subsystem(2, taban 2); bilinmeyen domain kodu UnknownContainmentDomain ile reddedilir.",
      "Eskalasyon merdiveni tablo sabitidir: Contain bütçe 3 / cool-down 16 tick, RestartTask 3/64, RestartGroup 2/256, SafeMode 1/1024, Halt 0/0 terminal ve yapışkandır; bütçeler artmaz, cool-down'lar kesin artar.",
      "Deterministik karar fonksiyonu decide_s563_action(fault, history) → action'dır: etkin taban max(sınıf tabanı, domain tabanı)'dır, mevcut seviyenin cool-down'u kadar sessiz tick geçerse history tabana decay olur, her fault bir kullanım tüketir ve bütçe aşımında merdiven bir seviye eskalasyon yapar; seviye 4 Halt'tır.",
      "Tek task domain'ine art arda on Oom fault'u exact Contain x3, RestartTask x3, RestartGroup x2, SafeMode x1, Halt x1 üretir; bütçe tükenmesi fail-closed Halt'a iner ve sonrası FaultAfterHalt ile reddedilir.",
      "Fault journal 64 girişlik ring'dir; her giriş 16 byte kodlanır ve zincirli FNV-1a-32 checksum (basis 0x811C9DC5, prime 0x01000193) atılan girişleri de kapsamaya devam eder.",
      "Journal overflow sayım kaybetmez: len + dropped == total_recorded değişmezi checked aritmetikle korunur; 100 kayıt sonrası len=64, dropped=36, total=100 exact doğrulanır ve tutarsızlık JournalAccountingMismatch ile reddedilir.",
      "Safe-mode allowlist'i dört servistir (0x0001-0x0004); SafeMode'a girildikten sonra allowlist dışı servislerden gelen fault'lar ServiceNotAllowedInSafeMode ile reddedilir ve reddedilen fault engine durumunu değiştirmez.",
      "Sınırlar fail-closed'dur: en fazla 16 izlenen (domain, id) anahtarı (DomainTableFull), dizi başına en fazla 256 fault (FaultSequenceTooLong), boş dizi (EmptyFaultSequence), rezerve service id 0 (ReservedServiceId) ve monoton olmayan tick (NonMonotonicTick).",
      "service_s563_model_contain_faults exact replay'de aynı receipt ile Retained döner; input digest sayesinde aynı sayaçları üreten farklı diziler dahil her divergence PublishedStateDrift ile reddedilir.",
      "On üç hata varyantı 1..13 arası sıfırdan farklı benzersiz diagnostic kod taşır.",
      "Focused target 1 grup / 21 passed / 0 failed / 0 ignored / 0 filtered verdi.",
      "Implementation 26725 B / c4e89737df176ab345343a20ffbc1fbab3497c52adb5ecc3b9e48264421895ab; focused test 27401 B / ef6ad34dfb83004603f659d098c2d84562ef19e4068e8113230723d70f4e76b0 SHA-256'dır.",
      "Proof 5319 B'dir ve model tablolarını, fail-closed koşullarını ve non-claim listesini aynen taşır.",
      "S563 sırasında hiçbir SD write/read-back/eject, UART open/capture, power transition, fiziksel retry veya yeni immutable raw üretimi yapılmadı; bu kapı için panel/modem/touch/board yoktur.",
      "RUNBOOK_EXECUTED_IN_S563=NO; supported-profile runtime observations=0, physical observations=0, Boot-to-UI physically observed=false ve R1 acceptance=false'dur.",
      "S564 aynı R1 uygulama/recovery/update aşamasında host-only update paket manifest ve hash zinciri modelini kuracaktır; aygıt 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_s563_r1_bounded_recovery_fault_containment_model -- --test-threads=1",
    ],
    terminalSessions: [
      {
        id: "s563-focused",
        title: "S563 bounded recovery ve fault containment focused target",
        commandLines: [
          "CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s563_r1_bounded_recovery_fault_containment_model -- --test-threads=1",
        ],
        outputLines: [
          "test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s",
          "S563 focused=1 group / 21 passed / 0 failed",
          "hardware=none physical=0 runbook=NO",
        ],
        exitCode: 0,
        outputMode: "complete",
      },
    ],
    terminalSessionsNote:
      "S563 kaynak/host model kapısıdır; supported-profile runtime veya fiziksel PASS değildir. S540 ve S543 RED raw'ları ve kararları değişmez.",
    limitations: [
      "S563 modeli yalnız host'ta focused test ile doğrulanmıştır; hiçbir donanım/panel/modem/board gözlemi yoktur ve model hiçbir production boot/IRQ/scheduler yoluna bağlanmamıştır.",
      "S540 ve S543 fiziksel RED immutable kalır; otomatik promotion yoktur ve bu kapı fiziksel kararı değiştiremez.",
      "Merdiven bütçeleri, cool-down tick'leri ve safe-mode allowlist'i host tablo değerleridir; gerçek watchdog, abort veya OOM davranışı gözlenmemiştir.",
      "Boot-to-UI physically observed=false ve R1 acceptance complete=false kalır; RUNBOOK_EXECUTED_IN_S563=NO'dur.",
      "S564 update paket manifest/hash zinciri modeli tamamlanmadan R1 4. aşamanın update gösterimi ilerlemez; S564 de host-only'dir ve aygıt yetkisi vermez.",
    ],
  },
snippet sha256: 0d1d21d97c9bfile 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_s563_r1_bounded_recovery_fault_containment_model -- --test-threads=1
proof: docs/M8.1-RPi5-G8l-S563-R1-Bounded-Recovery-Fault-Containment-Model-Proof.md
Registry schema v5 · generator website/scripts/generate-code-gates.mjs · Tam SHA-256: 91d38c7b6222f0b4c117be786454853543da55a160e543d9b951057cc20dcc06