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: c4e89737df17…file 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: ef6ad34dfb83…file 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: 0d1d21d97c9b…file 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=1proof: 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: 3050638b71a684d8f8f947a8a6faa237a17fa8db5dc0db04fb207b668b462af9