ASELSANMicrokernel
S565 · SOURCE-BOUND GATE EVIDENCE

S565 · R1 güncelleme: aşamalı uygulama ve geri alma modeli

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

S565Focused kod testiOperations id exactsource SHA exacttest target exact

operation: g8l-s565-r1-staged-update-apply-rollback-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–L874
kernel/src/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s565_r1_staged_update_apply_rollback_model.rs::S565 r1 staged update apply rollback model implementation
//! S565 models the A/B staged update apply and rollback state machine of the
//! R1 phone target: two boot slots (A/B) each carrying a version, a manifest
//! hash, a `boot_ok` flag and a `tries_remaining` counter (3), an explicit
//! transition table `Idle -> Staged -> Verified -> Committed -> BootPending ->
//! Confirmed | RolledBack`, a verification step that consumes a manifest-chain
//! boolean plus hashes, an ordered apply journal of entry writes with modelled
//! fsync barrier markers, power-loss replay at any journal index, boot-attempt
//! decrement with automatic rollback at zero, and a receipt per accepted
//! command.
//!
//! The gate is a pure source/host model.  No SD card, flash, panel, modem,
//! UART, power transition or board exists for it; the journal, the barriers
//! and the power loss are modelled in memory only.  The module is not wired
//! into any boot, IRQ, scheduler or driver path; the focused test is its only
//! caller.  It does not depend on the S564 manifest-chain module: the chain
//! verdict enters as a boolean plus hashes.  Predecessor: S564.  Next: S566.
//!
//! S540 and S543 remain immutable physical RED.  `RUNBOOK_EXECUTED_IN_S565=NO`.

use alloc::vec::Vec;

pub const S565_SEQUENCE: usize = 565;
pub const S565_EXPECTED_PREDECESSOR: usize = 564;
pub const S565_R1_STAGE: u8 = 4;
pub const S565_R1_RANGE_FIRST: usize = 536;
pub const S565_R1_RANGE_LAST: usize = 568;
pub const S565_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS: usize = 0;
pub const S565_PHYSICAL_OBSERVATIONS: usize = 0;
pub const S565_PHYSICAL_OR_DEVICE_OPERATIONS: usize = 0;
pub const S565_SD_WRITES: usize = 0;
pub const S565_UART_OPENS: usize = 0;
pub const S565_POWER_TRANSITIONS: usize = 0;
pub const S565_NEW_IMMUTABLE_RAW_CAPTURES: usize = 0;
pub const S565_S540_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S565_S543_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S565_AUTOMATIC_PROMOTION: bool = false;
pub const S565_BOOT_TO_UI_PHYSICALLY_OBSERVED: bool = false;
pub const S565_HARDWARE_PRESENT: bool = false;
pub const S565_R1_ACCEPTANCE_COMPLETE: bool = false;
pub const RUNBOOK_EXECUTED_IN_S565: bool = false;

pub const S565_SLOT_COUNT: usize = 2;
pub const S565_BOOT_TRIES_MAX: u8 = 3;
pub const S565_MAX_ENTRIES: usize = 8;
pub const S565_JOURNAL_CAPACITY: usize = 16;
pub const S565_STATE_COUNT: usize = 7;
pub const S565_TRANSITION_TABLE_ROWS: usize = 11;
pub const S565_HASH_SEED: u64 = 0xcbf2_9ce4_8422_2325;
pub const S565_HASH_PRIME: u64 = 0x0000_0100_0000_01b3;
pub const S565_TORN_WRITE_MASK: u64 = 0xdead_beef_0bad_f00d;
pub const S565_BASELINE_VERSION: u32 = 1;
pub const S565_BASELINE_ENTRIES: [u64; 2] = [0x5650_0000_0000_0001, 0x5650_0000_0000_0002];

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS565Slot {
    A,
    B,
}

impl G8lS565Slot {
    pub const fn other(self) -> Self {
        match self {
            Self::A => Self::B,
            Self::B => Self::A,
        }
    }

    pub const fn index(self) -> usize {
        match self {
            Self::A => 0,
            Self::B => 1,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS565UpdateState {
    Idle,
    Staged,
    Verified,
    Committed,
    BootPending,
    Confirmed,
    RolledBack,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS565CommandKind {
    Stage,
    Verify,
    Abort,
    Apply,
    BootAttempt,
    ConfirmBoot,
    Rollback,
    Finalize,
}

/// Explicit transition table: (from, command) -> nominal target state.
/// `Apply` may also end in `RolledBack` (power loss) and `BootAttempt` may
/// end in `RolledBack` (tries exhausted); both are decided by the model, not
/// by the caller.  Every pair absent from the table is `IllegalTransition`.
pub const S565_TRANSITION_TABLE: [(G8lS565UpdateState, G8lS565CommandKind, G8lS565UpdateState);
    S565_TRANSITION_TABLE_ROWS] = [
    (
        G8lS565UpdateState::Idle,
        G8lS565CommandKind::Stage,
        G8lS565UpdateState::Staged,
    ),
    (
        G8lS565UpdateState::Staged,
        G8lS565CommandKind::Verify,
        G8lS565UpdateState::Verified,
    ),
    (
        G8lS565UpdateState::Staged,
        G8lS565CommandKind::Abort,
        G8lS565UpdateState::Idle,
    ),
    (
        G8lS565UpdateState::Verified,
        G8lS565CommandKind::Apply,
        G8lS565UpdateState::Committed,
    ),
    (
        G8lS565UpdateState::Verified,
        G8lS565CommandKind::Abort,
        G8lS565UpdateState::Idle,
    ),
    (
        G8lS565UpdateState::Committed,
        G8lS565CommandKind::BootAttempt,
        G8lS565UpdateState::BootPending,
    ),
    (
        G8lS565UpdateState::BootPending,
        G8lS565CommandKind::BootAttempt,
        G8lS565UpdateState::BootPending,
    ),
    (
        G8lS565UpdateState::BootPending,
        G8lS565CommandKind::ConfirmBoot,
        G8lS565UpdateState::Confirmed,
    ),
    (
        G8lS565UpdateState::BootPending,
        G8lS565CommandKind::Rollback,
        G8lS565UpdateState::RolledBack,
    ),
    (
        G8lS565UpdateState::Confirmed,
        G8lS565CommandKind::Finalize,
        G8lS565UpdateState::Idle,
    ),
    (
        G8lS565UpdateState::RolledBack,
        G8lS565CommandKind::Finalize,
        G8lS565UpdateState::Idle,
    ),
];

pub fn s565_transition_target(
    from: G8lS565UpdateState,
    kind: G8lS565CommandKind,
) -> Option<G8lS565UpdateState> {
    S565_TRANSITION_TABLE
        .iter()
        .find(|(state, command, _)| *state == from && *command == kind)
        .map(|(_, _, to)| *to)
}

/// Deterministic 64-bit fold over entry words (FNV-1a style mixing; the
/// wrapping multiply is the intended hash arithmetic, not a counter).
pub fn s565_fold_hash(entries: &[u64]) -> u64 {
    let mut hash = S565_HASH_SEED;
    for entry in entries {
        for byte in entry.to_le_bytes() {
            hash ^= u64::from(byte);
            hash = hash.wrapping_mul(S565_HASH_PRIME);
        }
    }
    hash
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS565SlotImage {
    pub version: u32,
    pub manifest_hash: u64,
    pub entry_count: usize,
    pub entries: [u64; S565_MAX_ENTRIES],
    pub boot_ok: bool,
    pub tries_remaining: u8,
}

impl G8lS565SlotImage {
    pub fn empty() -> Self {
        Self {
            version: 0,
            manifest_hash: s565_fold_hash(&[]),
            entry_count: 0,
            entries: [0; S565_MAX_ENTRIES],
            boot_ok: false,
            tries_remaining: 0,
        }
    }

    pub fn from_entries(
        version: u32,
        entries: &[u64],
        boot_ok: bool,
        tries_remaining: u8,
    ) -> Option<Self> {
        if entries.len() > S565_MAX_ENTRIES || tries_remaining > S565_BOOT_TRIES_MAX {
            return None;
        }
        let mut image = Self::empty();
        image.version = version;
        image.entry_count = entries.len();
        image.entries[..entries.len()].copy_from_slice(entries);
        image.manifest_hash = s565_fold_hash(entries);
        image.boot_ok = boot_ok;
        image.tries_remaining = tries_remaining;
        Some(image)
    }

    /// A slot is consistent when its manifest hash equals the fold of its
    /// entries and its counters lie inside the modelled ranges.
    pub fn is_consistent(&self) -> bool {
        self.entry_count <= S565_MAX_ENTRIES
            && self.tries_remaining <= S565_BOOT_TRIES_MAX
            && s565_fold_hash(&self.entries[..self.entry_count]) == self.manifest_hash
    }

    pub const fn summary(&self) -> G8lS565SlotSummary {
        G8lS565SlotSummary {
            version: self.version,
            manifest_hash: self.manifest_hash,
            entry_count: self.entry_count,
            boot_ok: self.boot_ok,
            tries_remaining: self.tries_remaining,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS565SlotSummary {
    pub version: u32,
    pub manifest_hash: u64,
    pub entry_count: usize,
    pub boot_ok: bool,
    pub tries_remaining: u8,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS565Package {
    pub version: u32,
    pub manifest_hash: u64,
    pub entry_count: usize,
    pub entries: [u64; S565_MAX_ENTRIES],
}

impl G8lS565Package {
    pub fn from_entries(version: u32, entries: &[u64]) -> Option<Self> {
        if entries.is_empty() || entries.len() > S565_MAX_ENTRIES {
            return None;
        }
        let mut package = Self {
            version,
            manifest_hash: s565_fold_hash(entries),
            entry_count: entries.len(),
            entries: [0; S565_MAX_ENTRIES],
        };
        package.entries[..entries.len()].copy_from_slice(entries);
        Some(package)
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS565JournalRecord {
    EntryWrite {
        slot: G8lS565Slot,
        index: usize,
        value: u64,
    },
    FsyncBarrier,
    ManifestCommit {
        slot: G8lS565Slot,
        version: u32,
        manifest_hash: u64,
        entry_count: usize,
    },
    ActiveSwitch {
        slot: G8lS565Slot,
    },
}

/// Builds the ordered apply journal: entry writes, barrier, manifest commit,
/// barrier, active-slot switch, barrier.
pub fn s565_build_apply_journal(
    target: G8lS565Slot,
    package: &G8lS565Package,
) -> Result<Vec<G8lS565JournalRecord>, G8lS565UpdateError> {
    if package.entry_count == 0 || package.entry_count > S565_MAX_ENTRIES {
        return Err(G8lS565UpdateError::PackageEntryCountOutOfRange);
    }
    let length = package
        .entry_count
        .checked_add(5)
        .ok_or(G8lS565UpdateError::JournalCapacityExceeded)?;
    if length > S565_JOURNAL_CAPACITY {
        return Err(G8lS565UpdateError::JournalCapacityExceeded);
    }
    let mut journal = Vec::with_capacity(length);
    for (index, value) in package.entries[..package.entry_count].iter().enumerate() {
        journal.push(G8lS565JournalRecord::EntryWrite {
            slot: target,
            index,
            value: *value,
        });
    }
    journal.push(G8lS565JournalRecord::FsyncBarrier);
    journal.push(G8lS565JournalRecord::ManifestCommit {
        slot: target,
        version: package.version,
        manifest_hash: package.manifest_hash,
        entry_count: package.entry_count,
    });
    journal.push(G8lS565JournalRecord::FsyncBarrier);
    journal.push(G8lS565JournalRecord::ActiveSwitch { slot: target });
    journal.push(G8lS565JournalRecord::FsyncBarrier);
    Ok(journal)
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS565ReplayReport {
    pub issued_len: usize,
    pub durable_len: usize,
    pub torn_records: usize,
    pub active_after: G8lS565Slot,
}

/// Replays the journal onto the slot pair.  With `power_loss_at = Some(k)`
/// only records `0..k` were issued; records up to and including the last
/// barrier before `k` are durable, later entry writes are torn (their value is
/// corrupted by `S565_TORN_WRITE_MASK`) and later manifest/switch records are
/// dropped (single-record atomicity is the modelled medium contract).
pub fn s565_replay_journal(
    slots: &mut [G8lS565SlotImage; S565_SLOT_COUNT],
    active: &mut G8lS565Slot,
    journal: &[G8lS565JournalRecord],
    power_loss_at: Option<usize>,
) -> Result<G8lS565ReplayReport, G8lS565UpdateError> {
    if journal.len() > S565_JOURNAL_CAPACITY {
        return Err(G8lS565UpdateError::JournalCapacityExceeded);
    }
    let issued_len = match power_loss_at {
        Some(index) if index > journal.len() => {
            return Err(G8lS565UpdateError::PowerLossIndexOutOfRange)
        }
        Some(index) => index,
        None => journal.len(),
    };
    let original_active = *active;
    let mut barrier_since_write = true;
    let mut barrier_since_commit = false;
    for record in journal {
        match record {
            G8lS565JournalRecord::EntryWrite { slot, index, .. } => {
                if *slot == original_active {
                    return Err(G8lS565UpdateError::JournalTargetsActiveSlot);
                }
                if *index >= S565_MAX_ENTRIES {
                    return Err(G8lS565UpdateError::JournalEntryIndexOutOfRange);
                }
                barrier_since_write = false;
            }
            G8lS565JournalRecord::FsyncBarrier => {
                barrier_since_write = true;
                barrier_since_commit = true;
            }
            G8lS565JournalRecord::ManifestCommit {
                slot, entry_count, ..
            } => {
                if *slot == original_active {
                    return Err(G8lS565UpdateError::JournalTargetsActiveSlot);
                }
                if *entry_count > S565_MAX_ENTRIES {
                    return Err(G8lS565UpdateError::JournalEntryIndexOutOfRange);
                }
                if !barrier_since_write {
                    return Err(G8lS565UpdateError::JournalBarrierMissing);
                }
                barrier_since_commit = false;
            }
            G8lS565JournalRecord::ActiveSwitch { slot } => {
                if *slot == original_active {
                    return Err(G8lS565UpdateError::JournalTargetsActiveSlot);
                }
                if !barrier_since_commit {
                    return Err(G8lS565UpdateError::JournalBarrierMissing);
                }
            }
        }
    }
    let durable_len = journal[..issued_len]
        .iter()
        .rposition(|record| *record == G8lS565JournalRecord::FsyncBarrier)
        .map_or(0, |position| position + 1);
    let mut torn_records = 0usize;
    for (position, record) in journal[..issued_len].iter().enumerate() {
        let durable = position < durable_len;
        match *record {
            G8lS565JournalRecord::EntryWrite { slot, index, value } => {
                slots[slot.index()].entries[index] = if durable {
                    value
                } else {
                    value ^ S565_TORN_WRITE_MASK
                };
                if !durable {
                    torn_records += 1;
                }
            }
            G8lS565JournalRecord::FsyncBarrier => {}
            G8lS565JournalRecord::ManifestCommit {
                slot,
                version,
                manifest_hash,
                entry_count,
            } => {
                if durable {
                    let image = &mut slots[slot.index()];
                    image.version = version;
                    image.manifest_hash = manifest_hash;
                    image.entry_count = entry_count;
                    image.boot_ok = false;
                    image.tries_remaining = S565_BOOT_TRIES_MAX;
                } else {
                    torn_records += 1;
                }
            }
            G8lS565JournalRecord::ActiveSwitch { slot } => {
                if durable {
                    *active = slot;
                } else {
                    torn_records += 1;
                }
            }
        }
    }
    Ok(G8lS565ReplayReport {
        issued_len,
        durable_len,
        torn_records,
        active_after: *active,
    })
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS565Command {
    Stage(G8lS565Package),
    Verify {
        chain_ok: bool,
        expected_manifest_hash: u64,
    },
    Abort,
    Apply {
        power_loss_at: Option<usize>,
    },
    BootAttempt,
    ConfirmBoot,
    Rollback,
    Finalize,
}

impl G8lS565Command {
    pub const fn kind(&self) -> G8lS565CommandKind {
        match self {
            Self::Stage(_) => G8lS565CommandKind::Stage,
            Self::Verify { .. } => G8lS565CommandKind::Verify,
            Self::Abort => G8lS565CommandKind::Abort,
            Self::Apply { .. } => G8lS565CommandKind::Apply,
            Self::BootAttempt => G8lS565CommandKind::BootAttempt,
            Self::ConfirmBoot => G8lS565CommandKind::ConfirmBoot,
            Self::Rollback => G8lS565CommandKind::Rollback,
            Self::Finalize => G8lS565CommandKind::Finalize,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS565CommandEvent {
    pub id: u64,
    pub command: G8lS565Command,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS565UpdateReceipt {
    pub sequence: usize,
    pub predecessor_sequence: usize,
    pub transition_sequence: u64,
    pub command_id: u64,
    pub command_kind: G8lS565CommandKind,
    pub from_state: G8lS565UpdateState,
    pub to_state: G8lS565UpdateState,
    pub active_slot: G8lS565Slot,
    pub target_slot: G8lS565Slot,
    pub slot_a: G8lS565SlotSummary,
    pub slot_b: G8lS565SlotSummary,
    pub pending_version: u32,
    pub chain_verified: bool,
    pub journal_len: usize,
    pub journal_durable_len: usize,
    pub torn_records: usize,
    pub hardware_present: bool,
    pub physical_observations: usize,
    pub runbook_executed: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS565UpdateOutcome {
    Staged(G8lS565UpdateReceipt),
    Verified(G8lS565UpdateReceipt),
    Aborted(G8lS565UpdateReceipt),
    Committed(G8lS565UpdateReceipt),
    PowerLossRecovered(G8lS565UpdateReceipt),
    BootAttempted(G8lS565UpdateReceipt),
    Confirmed(G8lS565UpdateReceipt),
    RolledBack(G8lS565UpdateReceipt),
    Finalized(G8lS565UpdateReceipt),
    Retained(G8lS565UpdateReceipt),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS565UpdateError {
    CommandOrder,
    PublishedCommandDivergence,
    IllegalTransition,
    ActiveSlotCorrupt,
    PackageEntryCountOutOfRange,
    PackageManifestHashMismatch,
    VersionNotNewer,
    ManifestChainNotOk,
    ExpectedManifestHashMismatch,
    JournalCapacityExceeded,
    JournalBarrierMissing,
    JournalTargetsActiveSlot,
    JournalEntryIndexOutOfRange,
    PowerLossIndexOutOfRange,
    TransitionCounterOverflow,
    SlotEntryIndexOutOfRange,
    PendingPackageMissing,
}

impl G8lS565UpdateError {
    pub const fn diagnostic_code(self) -> u64 {
        match self {
            Self::CommandOrder => 1,
            Self::PublishedCommandDivergence => 2,
            Self::IllegalTransition => 3,
            Self::ActiveSlotCorrupt => 4,
            Self::PackageEntryCountOutOfRange => 5,
            Self::PackageManifestHashMismatch => 6,
            Self::VersionNotNewer => 7,
            Self::ManifestChainNotOk => 8,
            Self::ExpectedManifestHashMismatch => 9,
            Self::JournalCapacityExceeded => 10,
            Self::JournalBarrierMissing => 11,
            Self::JournalTargetsActiveSlot => 12,
            Self::JournalEntryIndexOutOfRange => 13,
            Self::PowerLossIndexOutOfRange => 14,
            Self::TransitionCounterOverflow => 15,
            Self::SlotEntryIndexOutOfRange => 16,
            Self::PendingPackageMissing => 17,
        }
    }
}

#[derive(Clone, Debug)]
pub struct G8lS565StagedUpdateState {
    slots: [G8lS565SlotImage; S565_SLOT_COUNT],
    active: G8lS565Slot,
    state: G8lS565UpdateState,
    pending: Option<G8lS565Package>,
    chain_verified: bool,
    journal: Vec<G8lS565JournalRecord>,
    transition_sequence: u64,
    next_command_id: u64,
    last_event: Option<G8lS565CommandEvent>,
    last_receipt: Option<G8lS565UpdateReceipt>,
}

impl G8lS565StagedUpdateState {
    /// Baseline: slot A carries version 1 with two entries, confirmed;
    /// slot B is empty.
    pub fn new() -> Self {
        let slot_a = G8lS565SlotImage::from_entries(
            S565_BASELINE_VERSION,
            &S565_BASELINE_ENTRIES,
            true,
            S565_BOOT_TRIES_MAX,
        )
        .unwrap_or_else(G8lS565SlotImage::empty);
        Self::from_slots(slot_a, G8lS565SlotImage::empty(), G8lS565Slot::A)
    }

    pub const fn from_slots(
        slot_a: G8lS565SlotImage,
        slot_b: G8lS565SlotImage,
        active: G8lS565Slot,
    ) -> Self {
        Self {
            slots: [slot_a, slot_b],
            active,
            state: G8lS565UpdateState::Idle,
            pending: None,
            chain_verified: false,
            journal: Vec::new(),
            transition_sequence: 0,
            next_command_id: 1,
            last_event: None,
            last_receipt: None,
        }
    }

    pub const fn state(&self) -> G8lS565UpdateState {
        self.state
    }

    pub const fn active_slot(&self) -> G8lS565Slot {
        self.active
    }

    pub fn slot(&self, slot: G8lS565Slot) -> G8lS565SlotImage {
        self.slots[slot.index()]
    }

    pub fn journal(&self) -> &[G8lS565JournalRecord] {
        &self.journal
    }

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

    pub const fn last_receipt(&self) -> Option<G8lS565UpdateReceipt> {
        self.last_receipt
    }

    /// Model-only fault injection: flips one entry word of a slot so the
    /// slot's manifest hash no longer matches.  Not a device operation.
    pub fn inject_slot_corruption(
        &mut self,
        slot: G8lS565Slot,
        index: usize,
    ) -> Result<(), G8lS565UpdateError> {
        if index >= S565_MAX_ENTRIES {
            return Err(G8lS565UpdateError::SlotEntryIndexOutOfRange);
        }
        self.slots[slot.index()].entries[index] ^= S565_TORN_WRITE_MASK;
        Ok(())
    }
}

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

fn receipt_for(
    state: &G8lS565StagedUpdateState,
    event: G8lS565CommandEvent,
    from_state: G8lS565UpdateState,
    transition_sequence: u64,
    durable_len: usize,
    torn_records: usize,
) -> G8lS565UpdateReceipt {
    G8lS565UpdateReceipt {
        sequence: S565_SEQUENCE,
        predecessor_sequence: S565_EXPECTED_PREDECESSOR,
        transition_sequence,
        command_id: event.id,
        command_kind: event.command.kind(),
        from_state,
        to_state: state.state,
        active_slot: state.active,
        target_slot: state.active.other(),
        slot_a: state.slots[0].summary(),
        slot_b: state.slots[1].summary(),
        pending_version: state.pending.map_or(0, |package| package.version),
        chain_verified: state.chain_verified,
        journal_len: state.journal.len(),
        journal_durable_len: durable_len,
        torn_records,
        hardware_present: S565_HARDWARE_PRESENT,
        physical_observations: S565_PHYSICAL_OBSERVATIONS,
        runbook_executed: RUNBOOK_EXECUTED_IN_S565,
    }
}

/// Drives one command through the staged-update state machine.  The next
/// snapshot is computed on a local copy and committed only on success, so
/// every `Err` leaves slots, state, journal and counters unchanged.
pub fn service_s565_model_staged_update(
    state: &mut G8lS565StagedUpdateState,
    event: G8lS565CommandEvent,
) -> Result<G8lS565UpdateOutcome, G8lS565UpdateError> {
    if let (Some(last_event), Some(last_receipt)) = (state.last_event, state.last_receipt) {
        if event.id == last_event.id {
            if event.command == last_event.command {
                return Ok(G8lS565UpdateOutcome::Retained(last_receipt));
            }
            return Err(G8lS565UpdateError::PublishedCommandDivergence);
        }
    }
    if event.id != state.next_command_id {
        return Err(G8lS565UpdateError::CommandOrder);
    }
    if !state.slots[state.active.index()].is_consistent() {
        return Err(G8lS565UpdateError::ActiveSlotCorrupt);
    }
    let from_state = state.state;
    let nominal = s565_transition_target(from_state, event.command.kind())
        .ok_or(G8lS565UpdateError::IllegalTransition)?;
    let transition_sequence = state
        .transition_sequence
        .checked_add(1)
        .ok_or(G8lS565UpdateError::TransitionCounterOverflow)?;
    let next_command_id = event
        .id
        .checked_add(1)
        .ok_or(G8lS565UpdateError::TransitionCounterOverflow)?;

    let mut next = state.clone();
    let mut durable_len = 0usize;
    let mut torn_records = 0usize;
    let active_version = state.slots[state.active.index()].version;
    let outcome_kind = match event.command {
        G8lS565Command::Stage(package) => {
            if package.entry_count == 0 || package.entry_count > S565_MAX_ENTRIES {
                return Err(G8lS565UpdateError::PackageEntryCountOutOfRange);
            }
            if s565_fold_hash(&package.entries[..package.entry_count]) != package.manifest_hash {
                return Err(G8lS565UpdateError::PackageManifestHashMismatch);
            }
            if package.version <= active_version {
                return Err(G8lS565UpdateError::VersionNotNewer);
            }
            next.pending = Some(package);
            next.chain_verified = false;
            next.journal.clear();
            next.state = nominal;
            G8lS565UpdateOutcome::Staged
        }
        G8lS565Command::Verify {
            chain_ok,
            expected_manifest_hash,
        } => {
            let package = state
                .pending
                .ok_or(G8lS565UpdateError::PendingPackageMissing)?;
            if !chain_ok {
                return Err(G8lS565UpdateError::ManifestChainNotOk);
            }
            if expected_manifest_hash != package.manifest_hash
                || s565_fold_hash(&package.entries[..package.entry_count]) != package.manifest_hash
            {
                return Err(G8lS565UpdateError::ExpectedManifestHashMismatch);
            }
            next.chain_verified = true;
            next.state = nominal;
            G8lS565UpdateOutcome::Verified
        }
        G8lS565Command::Abort => {
            next.pending = None;
            next.chain_verified = false;
            next.journal.clear();
            next.state = nominal;
            G8lS565UpdateOutcome::Aborted
        }
        G8lS565Command::Apply { power_loss_at } => {
            let package = state
                .pending
                .ok_or(G8lS565UpdateError::PendingPackageMissing)?;
            if !state.chain_verified {
                return Err(G8lS565UpdateError::ManifestChainNotOk);
            }
            let target = state.active.other();
            let journal = s565_build_apply_journal(target, &package)?;
            let report =
                s565_replay_journal(&mut next.slots, &mut next.active, &journal, power_loss_at)?;
            durable_len = report.durable_len;
            torn_records = report.torn_records;
            next.journal = journal;
            let target_image = next.slots[target.index()];
            if next.active == target
                && target_image.is_consistent()
                && target_image.version == package.version
            {
                next.state = G8lS565UpdateState::Committed;
                G8lS565UpdateOutcome::Committed
            } else {
                // Power loss before the durable switch: the old slot is
                // untouched; the torn target is invalidated at recovery.
                next.active = state.active;
                next.slots[target.index()] = G8lS565SlotImage::empty();
                next.pending = None;
                next.chain_verified = false;
                next.state = G8lS565UpdateState::RolledBack;
                G8lS565UpdateOutcome::PowerLossRecovered
            }
        }
        G8lS565Command::BootAttempt => {
            let image = &mut next.slots[state.active.index()];
            if image.tries_remaining == 0 {
                // Automatic rollback at zero: switch back to the other slot.
                image.boot_ok = false;
                next.active = state.active.other();
                if !next.slots[next.active.index()].is_consistent() {
                    return Err(G8lS565UpdateError::ActiveSlotCorrupt);
                }
                next.pending = None;
                next.chain_verified = false;
                next.state = G8lS565UpdateState::RolledBack;
                G8lS565UpdateOutcome::RolledBack
            } else {
                image.tries_remaining -= 1;
                next.state = nominal;
                G8lS565UpdateOutcome::BootAttempted
            }
        }
        G8lS565Command::ConfirmBoot => {
            next.slots[state.active.index()].boot_ok = true;
            next.state = nominal;
            G8lS565UpdateOutcome::Confirmed
        }
        G8lS565Command::Rollback => {
            next.slots[state.active.index()].boot_ok = false;
            next.active = state.active.other();
            if !next.slots[next.active.index()].is_consistent() {
                return Err(G8lS565UpdateError::ActiveSlotCorrupt);
            }
            next.pending = None;
            next.chain_verified = false;
            next.state = nominal;
            G8lS565UpdateOutcome::RolledBack
        }
        G8lS565Command::Finalize => {
            next.pending = None;
            next.chain_verified = false;
            next.journal.clear();
            next.state = nominal;
            G8lS565UpdateOutcome::Finalized
        }
    };
    if !next.slots[next.active.index()].is_consistent() {
        return Err(G8lS565UpdateError::ActiveSlotCorrupt);
    }
    next.transition_sequence = transition_sequence;
    next.next_command_id = next_command_id;
    next.last_event = Some(event);
    let receipt = receipt_for(
        &next,
        event,
        from_state,
        transition_sequence,
        durable_len,
        torn_records,
    );
    next.last_receipt = Some(receipt);
    *state = next;
    Ok(outcome_kind(receipt))
}
snippet sha256: 226862fbb3a8file sha256: 226862fbb3a8
02 · Doğrulayan test kodu

Operations komutuna bağlı focused test

tam dosyaL1–L647
simulation/tests/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s565_r1_staged_update_apply_rollback_model.rs::S565 r1 staged update apply rollback model focused tests
use aselsan_microkernel_simulation::g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s565_r1_staged_update_apply_rollback_model::*;
use std::collections::BTreeSet;

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

type Outcome = G8lS565UpdateOutcome;
type Error = G8lS565UpdateError;
type State = G8lS565UpdateState;
type Command = G8lS565Command;
type Slot = G8lS565Slot;
type Record = G8lS565JournalRecord;

const NEW_ENTRIES: [u64; 3] = [0x5651_0000_0000_0001, 0x5651_0000_0000_0002, 0x5651_0000_0000_0003];

fn package() -> G8lS565Package {
    G8lS565Package::from_entries(2, &NEW_ENTRIES).unwrap()
}

fn event(id: u64, command: Command) -> G8lS565CommandEvent {
    G8lS565CommandEvent { id, command }
}

/// Drives the next command using the state's expected id.
fn drive(state: &mut G8lS565StagedUpdateState, command: Command) -> Result<Outcome, Error> {
    let id = state.expected_command_id();
    service_s565_model_staged_update(state, event(id, command))
}

fn receipt_of(outcome: Outcome) -> G8lS565UpdateReceipt {
    match outcome {
        Outcome::Staged(receipt)
        | Outcome::Verified(receipt)
        | Outcome::Aborted(receipt)
        | Outcome::Committed(receipt)
        | Outcome::PowerLossRecovered(receipt)
        | Outcome::BootAttempted(receipt)
        | Outcome::Confirmed(receipt)
        | Outcome::RolledBack(receipt)
        | Outcome::Finalized(receipt)
        | Outcome::Retained(receipt) => receipt,
    }
}

fn verify() -> Command {
    Command::Verify { chain_ok: true, expected_manifest_hash: package().manifest_hash }
}

fn staged() -> G8lS565StagedUpdateState {
    let mut state = G8lS565StagedUpdateState::new();
    drive(&mut state, Command::Stage(package())).unwrap();
    state
}

fn verified() -> G8lS565StagedUpdateState {
    let mut state = staged();
    drive(&mut state, verify()).unwrap();
    state
}

fn committed() -> G8lS565StagedUpdateState {
    let mut state = verified();
    assert!(matches!(drive(&mut state, Command::Apply { power_loss_at: None }), Ok(Outcome::Committed(_))));
    state
}

fn snapshot(state: &G8lS565StagedUpdateState) -> (State, Slot, G8lS565SlotImage, G8lS565SlotImage, Option<G8lS565UpdateReceipt>, u64, usize) {
    (
        state.state(),
        state.active_slot(),
        state.slot(Slot::A),
        state.slot(Slot::B),
        state.last_receipt(),
        state.expected_command_id(),
        state.journal().len(),
    )
}

#[test]
fn sequence_scope_and_nonpromotion_are_exact() {
    assert_eq!(S565_SEQUENCE, 565);
    assert_eq!(S565_EXPECTED_PREDECESSOR, 564);
    assert_eq!(S565_R1_STAGE, 4);
    assert_eq!(S565_R1_RANGE_FIRST, 536);
    assert_eq!(S565_R1_RANGE_LAST, 568);
    assert_eq!(S565_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS, 0);
    assert_eq!(S565_PHYSICAL_OBSERVATIONS, 0);
    assert_eq!(S565_PHYSICAL_OR_DEVICE_OPERATIONS, 0);
    assert_eq!(S565_SD_WRITES, 0);
    assert_eq!(S565_UART_OPENS, 0);
    assert_eq!(S565_POWER_TRANSITIONS, 0);
    assert_eq!(S565_NEW_IMMUTABLE_RAW_CAPTURES, 0);
    assert!(S565_S540_PHYSICAL_VERDICT_RETAINED_RED);
    assert!(S565_S543_PHYSICAL_VERDICT_RETAINED_RED);
    assert!(!S565_AUTOMATIC_PROMOTION);
    assert!(!S565_BOOT_TO_UI_PHYSICALLY_OBSERVED);
    assert!(!S565_HARDWARE_PRESENT);
    assert!(!S565_R1_ACCEPTANCE_COMPLETE);
    assert!(!RUNBOOK_EXECUTED_IN_S565);
    assert_eq!(S565_SLOT_COUNT, 2);
    assert_eq!(S565_BOOT_TRIES_MAX, 3);
    assert_eq!(S565_MAX_ENTRIES, 8);
    assert_eq!(S565_JOURNAL_CAPACITY, 16);
    assert_eq!(S565_STATE_COUNT, 7);
    assert_eq!(S565_TRANSITION_TABLE_ROWS, 11);
}

#[test]
fn module_is_registered_in_kernel_and_simulation() {
    let module = "g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s565_r1_staged_update_apply_rollback_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::",
        "s564_",
    ] {
        assert!(!SOURCE.contains(forbidden), "forbidden token: {forbidden}");
    }
    assert!(SOURCE.contains("S565_HARDWARE_PRESENT: bool = false"));
    assert!(SOURCE.contains("RUNBOOK_EXECUTED_IN_S565: bool = false"));
    assert!(SOURCE.contains("S565_R1_ACCEPTANCE_COMPLETE: bool = false"));
}

#[test]
fn diagnostic_codes_are_nonzero_and_unique() {
    let errors = [
        Error::CommandOrder,
        Error::PublishedCommandDivergence,
        Error::IllegalTransition,
        Error::ActiveSlotCorrupt,
        Error::PackageEntryCountOutOfRange,
        Error::PackageManifestHashMismatch,
        Error::VersionNotNewer,
        Error::ManifestChainNotOk,
        Error::ExpectedManifestHashMismatch,
        Error::JournalCapacityExceeded,
        Error::JournalBarrierMissing,
        Error::JournalTargetsActiveSlot,
        Error::JournalEntryIndexOutOfRange,
        Error::PowerLossIndexOutOfRange,
        Error::TransitionCounterOverflow,
        Error::SlotEntryIndexOutOfRange,
        Error::PendingPackageMissing,
    ];
    let codes: BTreeSet<_> = errors.into_iter().map(Error::diagnostic_code).collect();
    assert_eq!(codes.len(), errors.len());
    assert_eq!(codes.len(), 17);
    assert!(!codes.contains(&0));
    assert_eq!(codes.iter().max(), Some(&17));
}

#[test]
fn exact_replay_retains_the_same_receipt() {
    let mut state = G8lS565StagedUpdateState::new();
    let receipt = receipt_of(service_s565_model_staged_update(&mut state, event(1, Command::Stage(package()))).unwrap());
    assert_eq!(
        service_s565_model_staged_update(&mut state, event(1, Command::Stage(package()))),
        Ok(Outcome::Retained(receipt))
    );
    assert_eq!(state.expected_command_id(), 2);
    assert_eq!(state.state(), State::Staged);
}

#[test]
fn divergent_input_after_publication_fails_closed() {
    let mut state = G8lS565StagedUpdateState::new();
    service_s565_model_staged_update(&mut state, event(1, Command::Stage(package()))).unwrap();
    let before = snapshot(&state);
    let other = G8lS565Package::from_entries(3, &NEW_ENTRIES).unwrap();
    assert_eq!(
        service_s565_model_staged_update(&mut state, event(1, Command::Stage(other))),
        Err(Error::PublishedCommandDivergence)
    );
    assert_eq!(
        service_s565_model_staged_update(&mut state, event(1, Command::Abort)),
        Err(Error::PublishedCommandDivergence)
    );
    assert_eq!(snapshot(&state), before);
}

#[test]
fn command_ids_must_be_contiguous() {
    let mut state = G8lS565StagedUpdateState::new();
    for id in [0, 2, u64::MAX] {
        assert_eq!(
            service_s565_model_staged_update(&mut state, event(id, Command::Stage(package()))),
            Err(Error::CommandOrder)
        );
    }
    service_s565_model_staged_update(&mut state, event(1, Command::Stage(package()))).unwrap();
    assert_eq!(service_s565_model_staged_update(&mut state, event(3, verify())), Err(Error::CommandOrder));
    assert_eq!(state.expected_command_id(), 2);
}

#[test]
fn transition_table_is_explicit_and_absent_pairs_are_illegal() {
    assert_eq!(S565_TRANSITION_TABLE.len(), 11);
    assert_eq!(s565_transition_target(State::Idle, G8lS565CommandKind::Stage), Some(State::Staged));
    assert_eq!(s565_transition_target(State::BootPending, G8lS565CommandKind::ConfirmBoot), Some(State::Confirmed));
    assert_eq!(s565_transition_target(State::Idle, G8lS565CommandKind::Apply), None);
    assert_eq!(s565_transition_target(State::Confirmed, G8lS565CommandKind::BootAttempt), None);
    let mut state = G8lS565StagedUpdateState::new();
    let before = snapshot(&state);
    for command in [verify(), Command::Apply { power_loss_at: None }, Command::BootAttempt, Command::ConfirmBoot, Command::Rollback, Command::Finalize, Command::Abort] {
        assert_eq!(drive(&mut state, command), Err(Error::IllegalTransition));
    }
    assert_eq!(snapshot(&state), before);
    let mut state = committed();
    for command in [Command::Stage(package()), verify(), Command::ConfirmBoot, Command::Rollback, Command::Finalize, Command::Abort] {
        assert_eq!(drive(&mut state, command), Err(Error::IllegalTransition));
    }
    assert_eq!(state.state(), State::Committed);
}

#[test]
fn happy_path_stage_verify_apply_boot_confirm_finalize() {
    let mut state = G8lS565StagedUpdateState::new();
    assert_eq!(state.state(), State::Idle);
    assert_eq!(state.active_slot(), Slot::A);
    assert_eq!(state.slot(Slot::A).version, S565_BASELINE_VERSION);
    assert!(state.slot(Slot::A).boot_ok);
    assert_eq!(state.slot(Slot::B), G8lS565SlotImage::empty());

    let staged = receipt_of(drive(&mut state, Command::Stage(package())).unwrap());
    assert_eq!((staged.from_state, staged.to_state, staged.pending_version), (State::Idle, State::Staged, 2));
    assert_eq!(staged.transition_sequence, 1);
    assert_eq!(staged.sequence, 565);
    assert_eq!(staged.predecessor_sequence, 564);
    assert!(!staged.chain_verified);

    let verified = receipt_of(drive(&mut state, verify()).unwrap());
    assert_eq!(verified.to_state, State::Verified);
    assert!(verified.chain_verified);

    let Ok(Outcome::Committed(committed)) = drive(&mut state, Command::Apply { power_loss_at: None }) else {
        panic!("apply must commit")
    };
    assert_eq!(committed.to_state, State::Committed);
    assert_eq!(committed.active_slot, Slot::B);
    assert_eq!(committed.target_slot, Slot::A);
    assert_eq!(committed.journal_len, NEW_ENTRIES.len() + 5);
    assert_eq!(committed.journal_durable_len, committed.journal_len);
    assert_eq!(committed.torn_records, 0);
    assert_eq!(committed.slot_b.version, 2);
    assert_eq!(committed.slot_b.tries_remaining, 3);
    assert!(!committed.slot_b.boot_ok);
    assert_eq!(committed.slot_a.version, 1);
    assert!(state.slot(Slot::B).is_consistent());

    let Ok(Outcome::BootAttempted(attempt)) = drive(&mut state, Command::BootAttempt) else {
        panic!("boot attempt missing")
    };
    assert_eq!(attempt.to_state, State::BootPending);
    assert_eq!(attempt.slot_b.tries_remaining, 2);

    let Ok(Outcome::Confirmed(confirmed)) = drive(&mut state, Command::ConfirmBoot) else {
        panic!("confirm missing")
    };
    assert!(confirmed.slot_b.boot_ok);
    assert_eq!(confirmed.to_state, State::Confirmed);

    let Ok(Outcome::Finalized(finalized)) = drive(&mut state, Command::Finalize) else {
        panic!("finalize missing")
    };
    assert_eq!(finalized.to_state, State::Idle);
    assert_eq!(finalized.transition_sequence, 6);
    assert_eq!(finalized.journal_len, 0);
    assert!(!finalized.hardware_present);
    assert_eq!(finalized.physical_observations, 0);
    assert!(!finalized.runbook_executed);
    assert_eq!(state.active_slot(), Slot::B);
    assert_eq!(state.expected_command_id(), 7);
}

#[test]
fn stage_rejects_malformed_or_older_packages() {
    let mut state = G8lS565StagedUpdateState::new();
    let before = snapshot(&state);
    let mut empty = package();
    empty.entry_count = 0;
    assert_eq!(drive(&mut state, Command::Stage(empty)), Err(Error::PackageEntryCountOutOfRange));
    let mut oversized = package();
    oversized.entry_count = S565_MAX_ENTRIES + 1;
    assert_eq!(drive(&mut state, Command::Stage(oversized)), Err(Error::PackageEntryCountOutOfRange));
    let mut tampered = package();
    tampered.entries[1] ^= 1;
    assert_eq!(drive(&mut state, Command::Stage(tampered)), Err(Error::PackageManifestHashMismatch));
    let mut bad_hash = package();
    bad_hash.manifest_hash ^= 1;
    assert_eq!(drive(&mut state, Command::Stage(bad_hash)), Err(Error::PackageManifestHashMismatch));
    for version in [0, S565_BASELINE_VERSION] {
        let older = G8lS565Package::from_entries(version, &NEW_ENTRIES).unwrap();
        assert_eq!(drive(&mut state, Command::Stage(older)), Err(Error::VersionNotNewer));
    }
    assert_eq!(snapshot(&state), before);
    assert!(G8lS565Package::from_entries(2, &[]).is_none());
    assert!(G8lS565Package::from_entries(2, &[0; 9]).is_none());
    assert!(G8lS565Package::from_entries(u32::MAX, &[0; 8]).is_some());
}

#[test]
fn verify_requires_chain_ok_and_exact_manifest_hash() {
    let mut state = staged();
    let before = snapshot(&state);
    assert_eq!(
        drive(&mut state, Command::Verify { chain_ok: false, expected_manifest_hash: package().manifest_hash }),
        Err(Error::ManifestChainNotOk)
    );
    assert_eq!(
        drive(&mut state, Command::Verify { chain_ok: true, expected_manifest_hash: package().manifest_hash ^ 1 }),
        Err(Error::ExpectedManifestHashMismatch)
    );
    assert_eq!(
        drive(&mut state, Command::Verify { chain_ok: true, expected_manifest_hash: 0 }),
        Err(Error::ExpectedManifestHashMismatch)
    );
    assert_eq!(snapshot(&state), before);
    assert!(matches!(drive(&mut state, verify()), Ok(Outcome::Verified(_))));
    assert_eq!(state.state(), State::Verified);
}

#[test]
fn abort_from_staged_or_verified_returns_to_idle_without_touching_slots() {
    for mut state in [staged(), verified()] {
        let slots_before = (state.slot(Slot::A), state.slot(Slot::B));
        let Ok(Outcome::Aborted(receipt)) = drive(&mut state, Command::Abort) else {
            panic!("abort missing")
        };
        assert_eq!(receipt.to_state, State::Idle);
        assert_eq!(receipt.pending_version, 0);
        assert!(!receipt.chain_verified);
        assert_eq!((state.slot(Slot::A), state.slot(Slot::B)), slots_before);
        assert_eq!(state.active_slot(), Slot::A);
        assert!(matches!(drive(&mut state, Command::Stage(package())), Ok(Outcome::Staged(_))));
    }
}

#[test]
fn apply_journal_orders_entry_writes_then_barriered_manifest_and_switch() {
    let journal = s565_build_apply_journal(Slot::B, &package()).unwrap();
    assert_eq!(journal.len(), 8);
    for (index, value) in NEW_ENTRIES.iter().enumerate() {
        assert_eq!(journal[index], Record::EntryWrite { slot: Slot::B, index, value: *value });
    }
    assert_eq!(journal[3], Record::FsyncBarrier);
    assert_eq!(
        journal[4],
        Record::ManifestCommit { slot: Slot::B, version: 2, manifest_hash: package().manifest_hash, entry_count: 3 }
    );
    assert_eq!(journal[5], Record::FsyncBarrier);
    assert_eq!(journal[6], Record::ActiveSwitch { slot: Slot::B });
    assert_eq!(journal[7], Record::FsyncBarrier);
    let full = G8lS565Package::from_entries(2, &[7; 8]).unwrap();
    assert_eq!(s565_build_apply_journal(Slot::B, &full).unwrap().len(), 13);
    let mut zero = package();
    zero.entry_count = 0;
    assert_eq!(s565_build_apply_journal(Slot::B, &zero), Err(Error::PackageEntryCountOutOfRange));
    let mut oversized = package();
    oversized.entry_count = 9;
    assert_eq!(s565_build_apply_journal(Slot::B, &oversized), Err(Error::PackageEntryCountOutOfRange));
    let mut long = s565_build_apply_journal(Slot::B, &full).unwrap();
    long.extend([Record::FsyncBarrier; 4]);
    assert_eq!(long.len(), 17);
    let mut slots = [G8lS565StagedUpdateState::new().slot(Slot::A), G8lS565SlotImage::empty()];
    let mut active = Slot::A;
    assert_eq!(s565_replay_journal(&mut slots, &mut active, &long, None), Err(Error::JournalCapacityExceeded));
}

#[test]
fn power_loss_at_every_journal_index_leaves_old_or_new_slot_fully_consistent() {
    let baseline = G8lS565StagedUpdateState::new();
    let old_image = baseline.slot(Slot::A);
    let journal = s565_build_apply_journal(Slot::B, &package()).unwrap();
    let mut committed_seen = 0usize;
    let mut recovered_seen = 0usize;
    for power_loss_at in 0..=journal.len() {
        let mut slots = [old_image, G8lS565SlotImage::empty()];
        let mut active = Slot::A;
        let report = s565_replay_journal(&mut slots, &mut active, &journal, Some(power_loss_at)).unwrap();
        assert_eq!(report.issued_len, power_loss_at);
        assert!(report.durable_len <= power_loss_at);
        assert_eq!(slots[0], old_image, "old slot must never be written at index {power_loss_at}");
        if active == Slot::B {
            assert!(slots[1].is_consistent(), "new slot must be consistent once switched at {power_loss_at}");
            assert_eq!(slots[1].version, 2);
            assert_eq!(slots[1].tries_remaining, 3);
            assert!(!slots[1].boot_ok);
            assert_eq!(report.torn_records, 0);
        } else {
            assert!(slots[0].is_consistent());
            assert!(slots[0].boot_ok);
        }
        let mut state = verified();
        match drive(&mut state, Command::Apply { power_loss_at: Some(power_loss_at) }).unwrap() {
            Outcome::Committed(receipt) => {
                committed_seen += 1;
                assert_eq!(receipt.active_slot, Slot::B);
                assert_eq!(receipt.torn_records, 0);
                assert_eq!(state.state(), State::Committed);
            }
            Outcome::PowerLossRecovered(receipt) => {
                recovered_seen += 1;
                assert_eq!(receipt.to_state, State::RolledBack);
                assert_eq!(receipt.active_slot, Slot::A);
                assert_eq!(receipt.journal_durable_len, report.durable_len);
                assert_eq!(receipt.torn_records, report.torn_records);
                assert_eq!(receipt.pending_version, 0);
                assert_eq!(state.slot(Slot::A), old_image);
                assert_eq!(state.slot(Slot::B), G8lS565SlotImage::empty());
                assert!(matches!(drive(&mut state, Command::Finalize), Ok(Outcome::Finalized(_))));
                assert_eq!(state.state(), State::Idle);
            }
            other => panic!("unexpected outcome {other:?} at {power_loss_at}"),
        }
    }
    assert_eq!(committed_seen, 1);
    assert_eq!(recovered_seen, journal.len());
    let torn_index = NEW_ENTRIES.len() - 1;
    let mut slots = [old_image, G8lS565SlotImage::empty()];
    let mut active = Slot::A;
    let report = s565_replay_journal(&mut slots, &mut active, &journal, Some(torn_index)).unwrap();
    assert_eq!(report.durable_len, 0);
    assert_eq!(report.torn_records, torn_index);
    assert_eq!(slots[1].entries[0], NEW_ENTRIES[0] ^ S565_TORN_WRITE_MASK);
}

#[test]
fn journal_without_barrier_before_manifest_or_switch_is_rejected() {
    let old_image = G8lS565StagedUpdateState::new().slot(Slot::A);
    let journal = s565_build_apply_journal(Slot::B, &package()).unwrap();
    let mut no_write_barrier = journal.clone();
    no_write_barrier.remove(3);
    let mut no_commit_barrier = journal.clone();
    no_commit_barrier.remove(5);
    for broken in [no_write_barrier, no_commit_barrier] {
        let mut slots = [old_image, G8lS565SlotImage::empty()];
        let mut active = Slot::A;
        assert_eq!(s565_replay_journal(&mut slots, &mut active, &broken, None), Err(Error::JournalBarrierMissing));
        assert_eq!(slots[1], G8lS565SlotImage::empty());
        assert_eq!(active, Slot::A);
    }
}

#[test]
fn journal_targeting_the_active_slot_or_bad_index_is_rejected() {
    let old_image = G8lS565StagedUpdateState::new().slot(Slot::A);
    let mut slots = [old_image, G8lS565SlotImage::empty()];
    let mut active = Slot::A;
    let onto_active = s565_build_apply_journal(Slot::A, &package()).unwrap();
    assert_eq!(s565_replay_journal(&mut slots, &mut active, &onto_active, None), Err(Error::JournalTargetsActiveSlot));
    let switch_to_active = [Record::FsyncBarrier, Record::ActiveSwitch { slot: Slot::A }];
    assert_eq!(s565_replay_journal(&mut slots, &mut active, &switch_to_active, None), Err(Error::JournalTargetsActiveSlot));
    let switch_without_barrier = [Record::ActiveSwitch { slot: Slot::B }];
    assert_eq!(s565_replay_journal(&mut slots, &mut active, &switch_without_barrier, None), Err(Error::JournalBarrierMissing));
    let bad_index = [Record::EntryWrite { slot: Slot::B, index: S565_MAX_ENTRIES, value: 1 }, Record::FsyncBarrier];
    assert_eq!(s565_replay_journal(&mut slots, &mut active, &bad_index, None), Err(Error::JournalEntryIndexOutOfRange));
    let bad_count = [
        Record::FsyncBarrier,
        Record::ManifestCommit { slot: Slot::B, version: 2, manifest_hash: 0, entry_count: 9 },
    ];
    assert_eq!(s565_replay_journal(&mut slots, &mut active, &bad_count, None), Err(Error::JournalEntryIndexOutOfRange));
    assert_eq!(slots, [old_image, G8lS565SlotImage::empty()]);
    assert_eq!(active, Slot::A);
}

#[test]
fn power_loss_index_beyond_the_journal_is_rejected() {
    let journal = s565_build_apply_journal(Slot::B, &package()).unwrap();
    let mut slots = [G8lS565StagedUpdateState::new().slot(Slot::A), G8lS565SlotImage::empty()];
    let mut active = Slot::A;
    for index in [journal.len() + 1, usize::MAX] {
        assert_eq!(
            s565_replay_journal(&mut slots, &mut active, &journal, Some(index)),
            Err(Error::PowerLossIndexOutOfRange)
        );
    }
    let mut state = verified();
    let before = snapshot(&state);
    assert_eq!(drive(&mut state, Command::Apply { power_loss_at: Some(journal.len() + 1) }), Err(Error::PowerLossIndexOutOfRange));
    assert_eq!(drive(&mut state, Command::Apply { power_loss_at: Some(usize::MAX) }), Err(Error::PowerLossIndexOutOfRange));
    assert_eq!(snapshot(&state), before);
    assert!(matches!(drive(&mut state, Command::Apply { power_loss_at: Some(journal.len()) }), Ok(Outcome::Committed(_))));
}

#[test]
fn apply_requires_verified_chain_and_pending_package() {
    let mut state = G8lS565StagedUpdateState::from_slots(
        G8lS565StagedUpdateState::new().slot(Slot::A),
        G8lS565SlotImage::empty(),
        Slot::A,
    );
    assert_eq!(drive(&mut state, Command::Apply { power_loss_at: None }), Err(Error::IllegalTransition));
    assert_eq!(drive(&mut state, verify()), Err(Error::IllegalTransition));
    let mut state = staged();
    assert_eq!(drive(&mut state, Command::Apply { power_loss_at: None }), Err(Error::IllegalTransition));
    assert_eq!(state.state(), State::Staged);
}

#[test]
fn boot_attempts_decrement_and_the_attempt_at_zero_rolls_back_automatically() {
    let mut state = committed();
    let mut expected = [2u8, 1, 0];
    for tries in expected.iter_mut() {
        let Ok(Outcome::BootAttempted(receipt)) = drive(&mut state, Command::BootAttempt) else {
            panic!("boot attempt missing")
        };
        assert_eq!(receipt.slot_b.tries_remaining, *tries);
        assert_eq!(receipt.to_state, State::BootPending);
        assert_eq!(state.active_slot(), Slot::B);
    }
    assert_eq!(state.slot(Slot::B).tries_remaining, 0);
    let Ok(Outcome::RolledBack(receipt)) = drive(&mut state, Command::BootAttempt) else {
        panic!("automatic rollback missing")
    };
    assert_eq!(receipt.to_state, State::RolledBack);
    assert_eq!(receipt.active_slot, Slot::A);
    assert!(!receipt.slot_b.boot_ok);
    assert_eq!(receipt.slot_b.tries_remaining, 0);
    assert!(receipt.slot_a.boot_ok);
    assert_eq!(receipt.slot_a.version, 1);
    assert_eq!(state.slot(Slot::A), G8lS565StagedUpdateState::new().slot(Slot::A));
    assert_eq!(drive(&mut state, Command::BootAttempt), Err(Error::IllegalTransition));
    assert!(matches!(drive(&mut state, Command::Finalize), Ok(Outcome::Finalized(_))));
    assert_eq!(state.active_slot(), Slot::A);
    assert_eq!(state.state(), State::Idle);
}

#[test]
fn manual_rollback_from_boot_pending_restores_the_old_slot() {
    let mut state = committed();
    drive(&mut state, Command::BootAttempt).unwrap();
    let Ok(Outcome::RolledBack(receipt)) = drive(&mut state, Command::Rollback) else {
        panic!("manual rollback missing")
    };
    assert_eq!(receipt.active_slot, Slot::A);
    assert_eq!(receipt.slot_b.tries_remaining, 2);
    assert!(!receipt.slot_b.boot_ok);
    assert!(state.slot(Slot::B).is_consistent());
    assert_eq!(drive(&mut state, Command::ConfirmBoot), Err(Error::IllegalTransition));
    assert!(matches!(drive(&mut state, Command::Finalize), Ok(Outcome::Finalized(_))));
    let next = G8lS565Package::from_entries(2, &[0x5652; 4]).unwrap();
    assert!(matches!(drive(&mut state, Command::Stage(next)), Ok(Outcome::Staged(_))));
}

#[test]
fn corrupted_active_slot_fails_closed_and_leaves_state_unchanged() {
    let mut state = G8lS565StagedUpdateState::new();
    assert_eq!(state.inject_slot_corruption(Slot::A, S565_MAX_ENTRIES), Err(Error::SlotEntryIndexOutOfRange));
    state.inject_slot_corruption(Slot::A, 0).unwrap();
    assert!(!state.slot(Slot::A).is_consistent());
    let before = snapshot(&state);
    assert_eq!(drive(&mut state, Command::Stage(package())), Err(Error::ActiveSlotCorrupt));
    assert_eq!(snapshot(&state), before);

    let mut state = committed();
    state.inject_slot_corruption(Slot::B, 1).unwrap();
    let before = snapshot(&state);
    assert_eq!(drive(&mut state, Command::BootAttempt), Err(Error::ActiveSlotCorrupt));
    assert_eq!(snapshot(&state), before);

    let mut state = committed();
    drive(&mut state, Command::BootAttempt).unwrap();
    state.inject_slot_corruption(Slot::A, 0).unwrap();
    let before = snapshot(&state);
    assert_eq!(drive(&mut state, Command::Rollback), Err(Error::ActiveSlotCorrupt));
    assert_eq!(snapshot(&state), before);
    assert!(matches!(drive(&mut state, Command::ConfirmBoot), Ok(Outcome::Confirmed(_))));
}

#[test]
fn inconsistent_inactive_slot_is_tolerated_until_it_would_become_active() {
    let mut state = committed();
    for _ in 0..3 {
        drive(&mut state, Command::BootAttempt).unwrap();
    }
    state.inject_slot_corruption(Slot::A, 1).unwrap();
    let before = snapshot(&state);
    assert_eq!(drive(&mut state, Command::BootAttempt), Err(Error::ActiveSlotCorrupt));
    assert_eq!(snapshot(&state), before);
    assert_eq!(state.state(), State::BootPending);
}

#[test]
fn slot_image_bounds_and_fold_hash_are_deterministic() {
    assert!(G8lS565SlotImage::from_entries(1, &[0; 9], false, 0).is_none());
    assert!(G8lS565SlotImage::from_entries(1, &[0; 8], false, S565_BOOT_TRIES_MAX + 1).is_none());
    let image = G8lS565SlotImage::from_entries(1, &[0; 8], false, S565_BOOT_TRIES_MAX).unwrap();
    assert!(image.is_consistent());
    let mut tries_out_of_range = image;
    tries_out_of_range.tries_remaining = 4;
    assert!(!tries_out_of_range.is_consistent());
    let mut count_out_of_range = image;
    count_out_of_range.entry_count = 9;
    assert!(!count_out_of_range.is_consistent());
    assert!(G8lS565SlotImage::empty().is_consistent());
    assert_eq!(s565_fold_hash(&[]), S565_HASH_SEED);
    assert_eq!(s565_fold_hash(&NEW_ENTRIES), s565_fold_hash(&NEW_ENTRIES));
    assert_ne!(s565_fold_hash(&NEW_ENTRIES), s565_fold_hash(&NEW_ENTRIES[..2]));
    assert_ne!(s565_fold_hash(&[1, 2]), s565_fold_hash(&[2, 1]));
    assert_eq!(package().manifest_hash, s565_fold_hash(&NEW_ENTRIES));
    let summary = image.summary();
    assert_eq!((summary.version, summary.entry_count, summary.tries_remaining, summary.boot_ok), (1, 8, 3, false));
}

#[test]
fn receipts_carry_monotonic_transition_sequence_and_zero_physical_claims() {
    let mut state = G8lS565StagedUpdateState::new();
    let commands = [
        Command::Stage(package()),
        verify(),
        Command::Apply { power_loss_at: None },
        Command::BootAttempt,
        Command::BootAttempt,
        Command::ConfirmBoot,
        Command::Finalize,
    ];
    let mut previous = 0u64;
    for (index, command) in commands.into_iter().enumerate() {
        let receipt = receipt_of(drive(&mut state, command).unwrap());
        assert_eq!(receipt.transition_sequence, previous + 1);
        assert_eq!(receipt.command_id, index as u64 + 1);
        assert_eq!(receipt.command_kind, command.kind());
        assert_eq!(state.last_receipt(), Some(receipt));
        assert!(!receipt.hardware_present);
        assert_eq!(receipt.physical_observations, 0);
        assert!(!receipt.runbook_executed);
        previous = receipt.transition_sequence;
    }
    assert_eq!(previous, 7);
    assert_eq!(state.slot(Slot::B).tries_remaining, 1);
    assert!(state.slot(Slot::B).boot_ok);
}
snippet sha256: 0ba7e32c573bfile sha256: 0ba7e32c573b
03 · Kapı kimlik kaydı

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

tam Operations kaydıL2012–L2073
website/src/lib/operations.ts::g8l-s565-r1-staged-update-apply-rollback-model
  {
    id: "g8l-s565-r1-staged-update-apply-rollback-model",
    date: "2026-08-30",
    sequence: 565,
    status: "passed",
    umbrella_status: "partial",
    title: "S565 · R1 güncelleme: aşamalı uygulama ve geri alma modeli",
    summary:
      "S565 kaynak/host model kapısı PASS'tir: A/B iki boot slotu (version, manifest hash, boot_ok bayrağı, tries_remaining=3) üzerinde Idle→Staged→Verified→Committed→BootPending→Confirmed / →RolledBack aşamalı güncelleme durum makinesi, 11 satırlık açık geçiş tablosu, manifest zinciri kararını boolean+hash olarak tüketen doğrulama adımı, fsync bariyer işaretli sıralı apply journal'ı, her journal indeksinde güç kaybı replay'i, boot denemesi başına tries azaltma ve sıfırda otomatik geri alma olarak modellendi. Her journal indeksindeki güç kaybı replay ile test edildi: switch dayanıklı değilse eski slot bayt-özdeş tutarlı kalır, dayanıklıysa yeni slot yeni sürümle tam tutarlıdır; geçersiz geçişler, slot bozulması, bariyeri eksik journal, bayat sürüm ve zincir/hash uyumsuzluğu fail-closed reddedilir ve durumu değiştirmez. Focused 24/24 PASS'tir. S540 ve S543 fiziksel RED değişmez kalır; hardware=none, physical observation=0, RUNBOOK_EXECUTED_IN_S565=NO, Boot-to-UI=false ve R1 acceptance=false'dur. S566 host-only laboratuvar güncelleme gösterimi runbook sözleşmesi kapısıdır.",
    evidence: [
      "S565, R1 telefon hedefinin A/B aşamalı güncelleme uygulama ve geri alma durum makinesini salt kaynak/host modeli olarak tanımlar; hiçbir SD kart, flash ortamı, panel, modem, board veya UART bu kapıda mevcut değildir ve journal/bariyer/güç kaybı yalnız bellekte modellenir.",
      "İki slot G8lS565SlotImage version, manifest hash, en fazla 8 entry kelimesi, boot_ok bayrağı ve S565_BOOT_TRIES_MAX=3 ile sınırlı tries_remaining taşır; slot tutarlılığı entry'lerin FNV-1a tarzı fold hash'inin manifest hash'e eşitliğiyle tanımlıdır ve baseline slot A version 1 / boot_ok=true, slot B boştur.",
      "Geçiş tablosu S565_TRANSITION_TABLE 11 açık satırdan oluşur: Idle→Staged (Stage), Staged→Verified (Verify), Staged/Verified→Idle (Abort), Verified→Committed (Apply, güç kaybında RolledBack), Committed/BootPending→BootPending (BootAttempt, sıfırda RolledBack), BootPending→Confirmed (ConfirmBoot), BootPending→RolledBack (Rollback) ve Confirmed/RolledBack→Idle (Finalize); tabloda olmayan her (durum, komut) çifti IllegalTransition'dır.",
      "Verify adımı manifest zinciri kararını chain_ok boolean'ı ve beklenen manifest hash olarak tüketir; S564 modülü import edilmez ve kapılar arası bağlaşım kurulmaz. chain_ok=false ManifestChainNotOk, hash uyumsuzluğu ExpectedManifestHashMismatch döner.",
      "Stage adımı 1..=8 entry, entry fold'una eşit manifest hash ve aktif slottan kesin yeni sürüm ister; bayat veya eşit sürüm VersionNotNewer ile reddedilir (anti-rollback).",
      "Apply, sıralı journal'ı kurar: hedef slota entry başına EntryWrite, FsyncBarrier, tek kayıtlık ManifestCommit (version, hash, entry sayısı, boot_ok=false, tries=3), FsyncBarrier, ActiveSwitch, FsyncBarrier; kapasite S565_JOURNAL_CAPACITY=16 ve en büyük kullanılan uzunluk 13'tür.",
      "Journal doğrulaması aktif slota yazan kaydı (JournalTargetsActiveSlot), 8 üstü entry indeks/sayısını, kapasite aşımını ve son entry yazısı ile manifest commit arasında veya commit ile switch arasında bariyeri eksik şekli (JournalBarrierMissing) uygulama başlamadan reddeder.",
      "Güç kaybı k indeksinde modellenir: k'den önceki son bariyere kadar olan kayıtlar dayanıklıdır, sonrası entry yazıları S565_TORN_WRITE_MASK ile yırtılır ve manifest/switch kayıtları düşer; focused test her k∈0..=journal_len için replay yapar ve switch dayanıklı değilken eski slotun bayt-özdeş tutarlı, dayanıklıyken yeni slotun yeni sürümle tam tutarlı olduğunu doğrular.",
      "Aynı süpürme service üzerinden tam olarak bir Committed (kayıpsız koşu) ve diğer her indekste PowerLossRecovered→RolledBack üretir; yırtık hedef slot kurtarmada geçersizlenir, eski slot dokunulmamış kalır ve Finalize ile Idle'a dönülür.",
      "Her BootAttempt tries_remaining'i azaltır (3→2→1→0); sıfırdaki deneme otomatik geri almadır: aktif işaretçi eski slota döner, reddedilen slotun boot_ok'u false kalır ve durum RolledBack olur. ConfirmBoot boot_ok=true ile Confirmed, Rollback manuel geri alma yoludur.",
      "Kabul edilen her komut sequence=565, predecessor=564, artan transition_sequence, komut id/kind, from/to durum, aktif/hedef slot, iki slot özeti, journal/dayanıklı/yırtık uzunlukları, hardware_present=false, physical_observations=0 ve runbook_executed=false alanlı bir G8lS565UpdateReceipt üretir.",
      "service_s565_model_staged_update sonraki anlık görüntüyü yerel kopyada hesaplar ve yalnız başarıda commit eder; komut id'leri bitişik olmak zorundadır, son kabul edilen komutun birebir tekrarı Retained ile aynı receipt'i döndürür ve aynı id altında farklı payload PublishedCommandDivergence'tır.",
      "On yedi hata kodu 1..=17 aralığında benzersiz ve sıfırdan farklıdır; aktif slot bozulması (inaktifken bozulup rollback hedefi olan slot dahil) ActiveSlotCorrupt ile fail-closed reddedilir ve her Err slotları, durumu, journal'ı ve sayaçları değiştirmeden bırakır.",
      "Focused target 1 grup / 24 passed / 0 failed / 0 ignored / 0 filtered verdi; 6 sözleşme testi ve 18 alan testi (mutlu yol, sınır değerleri, bozuk journal, güç kaybı süpürmesi, otomatik geri alma, sıralama) içerir.",
      "Implementation 29548 B / 226862fbb3a85e5353f5a80e09f2f76e739ba5f75b945750a43cdcbaae297bd4; focused test 28573 B / 0ba7e32c573b14d2302576bcbd2f79bca4a90c45a8ef7291efbb0a472e77ab52 SHA-256'dır.",
      "Proof 7127 B'dır.",
      "Modül hiçbir boot, IRQ, scheduler veya sürücü yoluna bağlanmamıştır; unsafe, asm!, write_volatile, crate::uart, crate::arch, #[no_mangle] ve spin:: yüzeyi içermez.",
      "S540 immutable raw 20525 B ve S543 immutable raw 20509 B fiziksel RED kararlarıyla byte-exact korunur; automatic promotion=false ve rerun=false'dur.",
      "S565 sırasında candidate freeze, SD write/read-back/eject, UART open/capture, power transition, fiziksel koşu veya yeni immutable raw üretimi yapılmadı.",
      "RUNBOOK_EXECUTED_IN_S565=NO; supported-profile runtime observations=0, physical observations=0, hardware present=false, Boot-to-UI physically observed=false ve R1 acceptance=false'dur.",
      "S566 yalnız host üzerinde laboratuvar güncelleme gösterimi runbook sözleşmesini tanımlayacaktır; aygıt, SD, UART, güç 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_s565_r1_staged_update_apply_rollback_model -- --test-threads=1",
    ],
    terminalSessions: [
      {
        id: "s565-focused",
        title: "S565 staged update apply/rollback model focused acceptance",
        commandLines: [
          "CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s565_r1_staged_update_apply_rollback_model -- --test-threads=1",
        ],
        outputLines: [
          "test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s",
          "S565 focused=1 group / 24 passed / 0 failed",
          "hardware=none physical=0 runbook=NO",
        ],
        exitCode: 0,
        outputMode: "complete",
      },
    ],
    terminalSessionsNote:
      "S565 kaynak/host model PASS'tir; supported-profile runtime veya fiziksel PASS değildir. S540 ve S543 RED raw ve kararları değişmez.",
    limitations: [
      "S565 yalnız kaynak/host A/B güncelleme modelidir; hiçbir donanım/panel/modem/board gözlemi yoktur, gerçek flash ortamı veya bootloader yoktur ve modül hiçbir üretim çağrı noktasına bağlanmamıştır.",
      "Journal, fsync bariyerleri ve güç kaybı bellekte modellenir; gerçek medium dayanıklılığı, sektör atomikliği veya gerçek güç kesintisi bu kapıda gözlenmemiştir.",
      "Manifest zinciri kararı boolean+hash girdisi olarak tüketilir; S564 zincir modülüne üretim bağlantısı kurulmamıştır ve kriptografik imza doğrulaması modellenmemiştir.",
      "S540 ve S543 fiziksel RED immutable kalır; otomatik yükseltme veya yeniden koşu yapılmaz.",
      "Boot-to-UI fiziksel olarak gözlenmedi; R1 acceptance false kalır ve RUNBOOK_EXECUTED_IN_S565=NO'dur.",
      "S566 host-only laboratuvar güncelleme gösterimi runbook sözleşmesi tamamlanmadan R1 4. aşama gösterim zinciri ilerlemez; yeni SD/UART/power koşusu ayrı kapı, fresh target revalidation, açık operatör yetkisi ve yeni immutable raw ister.",
    ],
  },
snippet sha256: a3cbf72f3cc8file 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_s565_r1_staged_update_apply_rollback_model -- --test-threads=1
proof: docs/M8.1-RPi5-G8l-S565-R1-Staged-Update-Apply-Rollback-Model-Proof.md
Registry schema v5 · generator website/scripts/generate-code-gates.mjs · Tam SHA-256: 3050638b71a684d8f8f947a8a6faa237a17fa8db5dc0db04fb207b668b462af9