# План 7 — v0.3 Power-safe ядро (FSM + graceful shutdown) > REQUIRED SUB-SKILL: `executing-plans` (или `subagent-driven-development`) + **TDD**. Спека: `docs/specs/v0.3-power-safe.md`. > Шаги — чекбоксы `- [ ]`. P7.4/P7.5 — тяжёлые (Lima); VM уже поднята. **Goal:** стаб Power → реальный lifecycle-FSM: ACC → graceful shutdown с durable-write до PONR → переживание срыва питания. **Architecture:** чистый `PowerFsm` (состояния/события/действия, юнит-тестируемый) в `shturman-power`; сервис оборачивает его (D-Bus state/signals из FSM, dev-mock кормит события, grace-таймер на монотонике, durable-barrier `sync` на commit). Teardown/unmount — через systemd (реальный poweroff) / харнесс (in-VM-цикл). Подход A спеки §6. **Tech Stack:** Rust, zbus 4 (signals/properties), tokio (grace-таймер), systemd (watchdog/savetime), Lima E2E (bash). --- ## File Structure - **Create** `crates/core/shturman-power/src/fsm.rs` — `PowerFsm` (State/Event/Action/step) + проекции в `PowerState`/`IgnitionState`/`PowerSource`. - **Modify** `crates/core/shturman-power/src/lib.rs` — `pub mod fsm;`. - **Modify** `crates/core/shturman-power/src/service.rs` — обернуть FSM: D-Bus из FSM; dev-mock кормит события; `apply_event` (grace-таймер + durable-barrier). - **Create** `systemd/watchdog-shturman.conf` — `RuntimeWatchdogSec`/`RebootWatchdogSec` (system.conf.d). - **Create** `systemd/shturman-savetime.service` + `systemd/shturman-savetime.timer` (B07 periodic save). - **Modify** `lima/shturman.yaml` (разложить watchdog/savetime), `tests/e2e/run.sh` (блок power-safe). - **Modify (P7.5)** `docs/domains/b-power-lifecycle.md`, `docs/contracts/ipc.md`, `docs/specs/v0.1-v0.6-foundation.md` §5.2, `CLAUDE.md`. --- ## P7.1: `PowerFsm` — чистый FSM питания (B03) **Files:** Create `crates/core/shturman-power/src/fsm.rs`; Modify `lib.rs`. - [ ] **Шаг 1 — реализация** `crates/core/shturman-power/src/fsm.rs` (тесты — в этом же файле, шаг 2): ```rust //! Чистый FSM питания (B03, спека v0.3 §5). Без D-Bus/async/I/O — сервис исполняет `Action`. use shturman_ipc::types::{IgnitionState, PowerSource, PowerState, ShutdownReason}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Phase { Abortable, Committed, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum State { Off, Accessory, Running, ShuttingDown { phase: Phase, reason: ShutdownReason }, Sleep, // зарезервировано (полные sleep/wake — v1/v2) BatteryCutoff, // зарезервировано (long-park — v1/v2) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Event { AccOn, AccOff, EngineOn, EngineOff, UnderVoltage, ThermalTrip, GraceExpired, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Action { ShutdownImminent(ShutdownReason), ShutdownAborted, AccChanged(bool), StartGrace, // сервис запускает grace-таймер (длительность — конфиг сервиса) Commit, // durable-barrier (sync) → PONR } /// FSM питания. v0: старт в `Running` (как стаб v0.1). Чистый: `step` без I/O. pub struct PowerFsm { state: State, } impl Default for PowerFsm { fn default() -> Self { Self { state: State::Running } } } impl PowerFsm { pub fn new() -> Self { Self::default() } pub fn state(&self) -> State { self.state } pub fn power_state(&self) -> PowerState { match self.state { State::Off => PowerState::Off, State::Accessory => PowerState::Accessory, State::Running => PowerState::Running, State::ShuttingDown { .. } => PowerState::ShuttingDown, State::Sleep => PowerState::Sleep, State::BatteryCutoff => PowerState::BatteryCutoff, } } pub fn ignition(&self) -> IgnitionState { match self.state { State::Running => IgnitionState::Running, State::Accessory => IgnitionState::Accessory, _ => IgnitionState::Off, } } pub fn source(&self) -> PowerSource { match self.state { State::ShuttingDown { reason: ShutdownReason::UnderVoltage, .. } => PowerSource::LowBattery, State::ShuttingDown { .. } => PowerSource::HoldupCap, _ => PowerSource::Vehicle12v, } } /// Шаг FSM. Возвращает действия для исполнения сервисом (спека §5). pub fn step(&mut self, ev: Event) -> Vec { use Event as E; use Phase::*; use State::*; match (self.state, ev) { (Off, E::AccOn) => { self.state = Accessory; vec![Action::AccChanged(true)] } (Accessory, E::EngineOn) => { self.state = Running; vec![] } (Running, E::EngineOff) => { self.state = Accessory; vec![] } (Accessory | Running, E::AccOff) => self.begin_shutdown(ShutdownReason::AccOff), (Accessory | Running, E::UnderVoltage) => self.begin_shutdown(ShutdownReason::UnderVoltage), (Accessory | Running, E::ThermalTrip) => self.begin_shutdown(ShutdownReason::Thermal), (ShuttingDown { phase: Abortable, .. }, E::AccOn) => { self.state = Running; vec![Action::ShutdownAborted, Action::AccChanged(true)] } (ShuttingDown { phase: Abortable, reason }, E::GraceExpired) => { self.state = ShuttingDown { phase: Committed, reason }; vec![Action::Commit] } // committed/off/sleep/battery_cutoff + всё прочее — no-op (инвариант: committed не abort-ится) _ => vec![], } } fn begin_shutdown(&mut self, reason: ShutdownReason) -> Vec { self.state = State::ShuttingDown { phase: Phase::Abortable, reason }; vec![Action::ShutdownImminent(reason), Action::StartGrace] } } #[cfg(test)] mod tests { use super::*; #[test] fn off_acc_on_to_accessory() { let mut f = PowerFsm { state: State::Off }; assert_eq!(f.step(Event::AccOn), vec![Action::AccChanged(true)]); assert_eq!(f.state(), State::Accessory); } #[test] fn accessory_engine_on_to_running_and_back() { let mut f = PowerFsm { state: State::Accessory }; assert_eq!(f.step(Event::EngineOn), vec![]); assert_eq!(f.state(), State::Running); assert_eq!(f.step(Event::EngineOff), vec![]); assert_eq!(f.state(), State::Accessory); } #[test] fn acc_off_begins_abortable_shutdown() { let mut f = PowerFsm::new(); // Running assert_eq!( f.step(Event::AccOff), vec![Action::ShutdownImminent(ShutdownReason::AccOff), Action::StartGrace] ); assert_eq!(f.power_state(), PowerState::ShuttingDown); assert_eq!(f.source(), PowerSource::HoldupCap); } #[test] fn under_voltage_reason_and_source() { let mut f = PowerFsm::new(); let a = f.step(Event::UnderVoltage); assert_eq!(a[0], Action::ShutdownImminent(ShutdownReason::UnderVoltage)); assert_eq!(f.source(), PowerSource::LowBattery); } #[test] fn abort_before_ponr() { let mut f = PowerFsm::new(); f.step(Event::AccOff); assert_eq!( f.step(Event::AccOn), vec![Action::ShutdownAborted, Action::AccChanged(true)] ); assert_eq!(f.state(), State::Running); } #[test] fn grace_expired_commits_and_is_irreversible() { let mut f = PowerFsm::new(); f.step(Event::AccOff); assert_eq!(f.step(Event::GraceExpired), vec![Action::Commit]); // committed: abort игнорируется assert_eq!(f.step(Event::AccOn), vec![]); assert!(matches!(f.state(), State::ShuttingDown { phase: Phase::Committed, .. })); } #[test] fn reserved_states_noop() { let mut f = PowerFsm { state: State::Sleep }; assert_eq!(f.step(Event::AccOn), vec![]); assert_eq!(f.state(), State::Sleep); } } ``` - [ ] **Шаг 2 — `lib.rs`:** добавить `pub mod fsm;`. (Прочитать `crates/core/shturman-power/src/lib.rs`, добавить строку рядом с другими `mod`.) - [ ] **Шаг 3 — прогон.** Run: `cargo test -p shturman-power fsm`. Expected: PASS (7 тестов). - [ ] **Шаг 4 — commit.** ```bash git add crates/core/shturman-power/src/fsm.rs crates/core/shturman-power/src/lib.rs git commit -s -m "feat(v0.3): чистый PowerFsm (состояния/переходы B03)" ``` --- ## P7.2: обернуть FSM в сервис (D-Bus из FSM + dev-mock кормит события + grace + barrier) **Files:** Modify `crates/core/shturman-power/src/service.rs`. Test: `crates/core/shturman-power/tests/integration.rs` (расширить). - [ ] **Шаг 1 — переписать `service.rs`** (полностью — заменяет плоский `State` на FSM): ```rust //! Server `ru.shturman.Power1` + (feature `dev-mocks`) `ru.shturman.dev.PowerMock1` (fake-ACC). //! v0.3: оборачивает чистый `PowerFsm` (спека §5–§7). dev-mock кормит входы FSM (не флипает состояние). use crate::fsm::{Action, Event, PowerFsm}; use shturman_common::monotonic_secs; use std::sync::{Arc, Mutex}; use std::time::Duration; use zbus::interface; use zbus::object_server::SignalContext; /// Grace-окно (сек): и поле сигнала `ShutdownImminent`, и длительность таймера. v0.3 — фикс. (конфиг — позже). const GRACE_SECS: u32 = 2; pub struct PowerService { fsm: Arc>, } impl Default for PowerService { fn default() -> Self { Self { fsm: Arc::new(Mutex::new(PowerFsm::new())) } } } impl PowerService { pub fn new() -> Self { Self::default() } pub fn power_state(&self) -> shturman_ipc::types::PowerState { self.fsm.lock().unwrap().power_state() } pub fn ignition(&self) -> shturman_ipc::types::IgnitionState { self.fsm.lock().unwrap().ignition() } pub fn source(&self) -> shturman_ipc::types::PowerSource { self.fsm.lock().unwrap().source() } #[cfg(feature = "dev-mocks")] pub fn mock(&self) -> PowerMock { PowerMock { fsm: Arc::clone(&self.fsm) } } } /// Шагнуть FSM и исполнить действия (эмит сигналов, grace-таймер, durable-barrier). Свободная функция — /// чтобы её мог звать и dev-mock, и фоновый grace-таймер (с owned-контекстом). async fn apply_event( fsm: &Arc>, ev: Event, ctx: &SignalContext<'_>, ) -> zbus::Result<()> { let actions = fsm.lock().unwrap().step(ev); for a in actions { match a { Action::ShutdownImminent(r) => { PowerService::shutdown_imminent(ctx, GRACE_SECS, r.as_str()).await? } Action::ShutdownAborted => PowerService::shutdown_aborted(ctx).await?, Action::AccChanged(on) => PowerService::acc_changed(ctx, on).await?, Action::StartGrace => { let fsm = Arc::clone(fsm); let octx = ctx.to_owned(); tokio::spawn(async move { tokio::time::sleep(Duration::from_secs(GRACE_SECS as u64)).await; let _ = apply_event(&fsm, Event::GraceExpired, &octx).await; }); } Action::Commit => { // Durable-write barrier (#5): сбросить грязные страницы /data ДО PONR. Settings уже синхронен. let _ = std::process::Command::new("sync").status(); tracing::info!("power: commit (PONR) — durable barrier sync; load-shed: amp/backlight/modem (нет реальных нагрузок в v0)"); } } } Ok(()) } #[interface(name = "ru.shturman.Power1")] impl PowerService { async fn get_power_state(&self) -> String { self.power_state().as_str().to_string() } /// Внутренний; sleep/wake — v1/v2 (B §7). В v0.3 — no-op. async fn request_sleep(&self) {} #[zbus(property)] async fn ignition_state(&self) -> String { self.ignition().as_str().to_string() } #[zbus(property)] async fn uptime(&self) -> u64 { monotonic_secs() } #[zbus(property)] async fn power_source(&self) -> String { self.source().as_str().to_string() } #[zbus(signal)] async fn acc_changed(ctx: &SignalContext<'_>, on: bool) -> zbus::Result<()>; #[zbus(signal)] async fn shutdown_imminent(ctx: &SignalContext<'_>, seconds: u32, reason: &str) -> zbus::Result<()>; #[zbus(signal)] async fn shutdown_aborted(ctx: &SignalContext<'_>) -> zbus::Result<()>; #[zbus(signal)] async fn sleep(ctx: &SignalContext<'_>) -> zbus::Result<()>; #[zbus(signal)] async fn wake(ctx: &SignalContext<'_>) -> zbus::Result<()>; } /// dev-mock «fake-ACC/voltage/thermal» — кормит входы FSM. Прод (`--no-default-features`) не регистрирует. #[cfg(feature = "dev-mocks")] pub struct PowerMock { fsm: Arc>, } #[cfg(feature = "dev-mocks")] #[interface(name = "ru.shturman.dev.PowerMock1")] impl PowerMock { async fn set_acc(&self, on: bool, #[zbus(signal_context)] ctx: SignalContext<'_>) { let ev = if on { Event::AccOn } else { Event::AccOff }; let _ = apply_event(&self.fsm, ev, &ctx).await; } async fn set_ignition(&self, state: String, #[zbus(signal_context)] ctx: SignalContext<'_>) { // accessory↔running — через EngineOn/Off; off — AccOff. let ev = match state.as_str() { "running" => Event::EngineOn, "accessory" => Event::EngineOff, _ => Event::AccOff, }; let _ = apply_event(&self.fsm, ev, &ctx).await; } async fn trigger_shutdown( &self, _seconds: u32, reason: String, #[zbus(signal_context)] ctx: SignalContext<'_>, ) { let ev = match reason.as_str() { "thermal" => Event::ThermalTrip, "under_voltage" => Event::UnderVoltage, _ => Event::AccOff, }; let _ = apply_event(&self.fsm, ev, &ctx).await; } async fn abort_shutdown(&self, #[zbus(signal_context)] ctx: SignalContext<'_>) { let _ = apply_event(&self.fsm, Event::AccOn, &ctx).await; } } #[cfg(test)] mod tests { use super::*; use shturman_ipc::types::{IgnitionState, PowerState, PowerSource}; #[test] fn defaults_running() { let svc = PowerService::new(); assert_eq!(svc.power_state(), PowerState::Running); assert_eq!(svc.ignition(), IgnitionState::Running); assert_eq!(svc.source(), PowerSource::Vehicle12v); } } ``` - [ ] **Шаг 2 — прогон unit.** Run: `cargo test -p shturman-power`. Expected: PASS (fsm 7 + service 1). - [ ] **Шаг 3 — расширить integration-тест** `crates/core/shturman-power/tests/integration.rs` — добавить тест abort (после существующего `power_state_and_fake_acc`): ```rust #[tokio::test] #[ignore = "нужна session-шина: just test-integration"] async fn shutdown_imminent_then_abort() { use futures_util::StreamExt; let svc = PowerService::new(); let mock = svc.mock(); let server = zbus::Connection::session().await.unwrap(); server.object_server().at(names::power::PATH, svc).await.unwrap(); server.object_server().at(names::power::PATH, mock).await.unwrap(); server.request_name(names::power::NAME).await.unwrap(); let client = zbus::Connection::session().await.unwrap(); let power = PowerClient::new(&client).await.unwrap(); let mut imminent = power.proxy().receive_shutdown_imminent().await.unwrap(); let mut aborted = power.proxy().receive_shutdown_aborted().await.unwrap(); // ACC-off → ShutdownImminent(acc_off) client.call_method(Some(names::power::NAME), names::power::PATH, Some(names::power::MOCK_IFACE), "SetAcc", &(false,)).await.unwrap(); let sig = imminent.next().await.unwrap(); assert_eq!(sig.args().unwrap().reason(), "acc_off"); assert_eq!(power.power_state().await.unwrap(), PowerState::ShuttingDown); // re-power до grace → ShutdownAborted + running client.call_method(Some(names::power::NAME), names::power::PATH, Some(names::power::MOCK_IFACE), "SetAcc", &(true,)).await.unwrap(); aborted.next().await.unwrap(); assert_eq!(power.power_state().await.unwrap(), PowerState::Running); } ``` (`Power1Proxy` уже объявляет сигналы `shutdown_imminent`/`shutdown_aborted` — `crates/shturman-ipc/src/proxy.rs:29,31`; zbus генерит `receive_shutdown_imminent()`/`receive_shutdown_aborted()`. `sig.args().unwrap().reason()` → `&str`.) - [ ] **Шаг 4 — прогон integration.** Run: `just test-integration` (или `dbus-run-session -- cargo test -p shturman-power -- --ignored`). Expected: PASS (оба теста). - [ ] **Шаг 5 — lint + commit.** ```bash cargo clippy -p shturman-power --all-targets -- -D warnings git add crates/core/shturman-power/ git commit -s -m "feat(v0.3): Power-сервис на FSM — dev-mock кормит события, grace+durable-barrier" ``` --- ## P7.3: systemd watchdog drop-in + save-time timer (B05/A14/B07) **Files:** Create `systemd/watchdog-shturman.conf`, `systemd/shturman-savetime.service`, `systemd/shturman-savetime.timer`. - [ ] **Шаг 1 — `systemd/watchdog-shturman.conf`** (system.conf.d; реальный `/dev/watchdog` — HW, в VM no-op): ```ini # Watchdog (B05/A14): systemd пингует HW-watchdog в runtime + дедлайн на shutdown-фазу. # Установка: /etc/systemd/system.conf.d/shturman-watchdog.conf. В VM /dev/watchdog нет → дисциплина (HW-арминг — v0.4). [Manager] RuntimeWatchdogSec=30s RebootWatchdogSec=60s ``` - [ ] **Шаг 2 — `systemd/shturman-savetime.service`** (B07 periodic save last-known-time): ```ini [Unit] Description=Штурман save last-known-time (fake-hwclock → /data, B07) After=data.mount Requires=data.mount [Service] Type=oneshot # FILE из /etc/default/fake-hwclock (→ /data; v0.6). Сервис в Lima masked → зовём напрямую с env. ExecStart=/bin/sh -c '. /etc/default/fake-hwclock 2>/dev/null; FILE="${FILE:-/data/state/fake-hwclock.data}" fake-hwclock save' ``` - [ ] **Шаг 3 — `systemd/shturman-savetime.timer`** (периодика ~5 мин): ```ini [Unit] Description=Штурман periodic save-time (B07) [Timer] OnBootSec=2min OnUnitActiveSec=5min [Install] WantedBy=shturman-stage2.target ``` - [ ] **Шаг 4 — commit** (проверка — P7.4/Lima). ```bash git add systemd/watchdog-shturman.conf systemd/shturman-savetime.service systemd/shturman-savetime.timer git commit -s -m "feat(v0.3): watchdog-конфиг (B05/A14) + save-time timer (B07)" ``` --- ## P7.4: lima + E2E-блок power-safe (гибрид §9.3) **Files:** Modify `lima/shturman.yaml`, `tests/e2e/run.sh`. - [ ] **Шаг 1 — `lima/shturman.yaml`:** в блоке раскладки юнитов добавить watchdog + savetime: ```bash install -d /etc/systemd/system.conf.d install -m644 /shturman/systemd/watchdog-shturman.conf /etc/systemd/system.conf.d/shturman-watchdog.conf # savetime.service/.timer ловит glob shturman-*.service/.timer? Нет — .timer не под *.service. Ставим явно: install -m644 /shturman/systemd/shturman-savetime.service /shturman/systemd/shturman-savetime.timer /etc/systemd/system/ ``` (Существующий `install /shturman/systemd/shturman-*.service` НЕ ловит `.timer` — ставим явно выше.) - [ ] **Шаг 2 — `tests/e2e/run.sh`: установка savetime + watchdog в раскладке.** К блоку install (рядом с tmpfiles) добавить: ```bash sudo install -d /etc/systemd/system.conf.d sudo install -m644 systemd/watchdog-shturman.conf /etc/systemd/system.conf.d/shturman-watchdog.conf sudo install -m644 systemd/shturman-savetime.service systemd/shturman-savetime.timer /etc/systemd/system/ ``` - [ ] **Шаг 3 — `tests/e2e/run.sh`: блок power-safe.** Вставить после блока «Stage 0/1/2», до §8: ```bash # ---- power-safe (v0.3): FSM + N циклов зажигания + abort + power-cut ---- info "power-safe: ShutdownImminent + N=3 цикла зажигания + abort + power-cut" P_CALL() { busctl --system call "$P_NAME" "$P_PATH" "$P_MOCK" "$@"; } busctl --system call "$S_NAME" "$S_PATH" "$S_IFACE" Set sv ui.theme s night >/dev/null echo 0 | sudo tee /data/state/power-cycles >/dev/null observe_imminent() { # SetAcc(false) → ждём ShutdownImminent local mon; mon=$(mktemp) # shellcheck disable=SC2024 sudo busctl --system monitor "$P_NAME" >"$mon" 2>&1 & local M=$! sleep 0.7; P_CALL SetAcc b false; sleep 0.7 sudo kill "$M" 2>/dev/null; wait "$M" 2>/dev/null grep -q ShutdownImminent "$mon" || { echo "--- mon ---"; cat "$mon"; rm -f "$mon"; return 1; } rm -f "$mon" } for i in 1 2 3; do observe_imminent || fail "цикл $i: ShutdownImminent не наблюдаем" n=$(($(sudo cat /data/state/power-cycles) + 1)) sudo systemctl stop shturman-stage1.target # освобождает /data sync; sudo umount /data || fail "цикл $i: umount /data" sudo mount /data || fail "цикл $i: mount /data" echo "$n" | sudo tee /data/state/power-cycles >/dev/null sudo systemctl start shturman.target for _ in $(seq 1 15); do systemctl is-active --quiet shturman-settings && break; sleep 1; done pass "цикл зажигания $i: stop→umount→remount→restart" done got=$(busctl --system call "$S_NAME" "$S_PATH" "$S_IFACE" Get s ui.theme 2>/dev/null) echo "$got" | grep -q '"night"' || fail "ui.theme потерян после циклов" [ "$(sudo cat /data/state/power-cycles)" = 3 ] || fail "счётчик циклов != 3" pass "N=3 цикла: /data + счётчик целы (нет потери)" # abort до PONR mon=$(mktemp) # shellcheck disable=SC2024 sudo busctl --system monitor "$P_NAME" >"$mon" 2>&1 & M=$! sleep 0.7; P_CALL SetAcc b false; sleep 0.3; P_CALL SetAcc b true; sleep 0.7 sudo kill "$M" 2>/dev/null; wait "$M" 2>/dev/null grep -q ShutdownAborted "$mon" || { cat "$mon"; rm -f "$mon"; fail "ShutdownAborted не наблюдаем"; } rm -f "$mon" findmnt /data >/dev/null || fail "/data не смонтирован после abort" busctl --system call "$P_NAME" "$P_PATH" "$P_IFACE" GetPowerState | grep -q running || fail "не running после abort" pass "abort до PONR: ShutdownAborted + /data RW + running" # power-cut-сим: SIGKILL во время shutdown → /data консистентен P_CALL SetAcc b false; sleep 0.3 sudo systemctl kill -s KILL shturman-power.service shturman-settings.service 2>/dev/null || true sudo systemctl stop shturman-stage1.target 2>/dev/null || true sudo umount /data 2>/dev/null || true sudo fsck.ext4 -n /var/lib/shturman/data.img >/dev/null 2>&1 || fail "fsck /data не clean после power-cut" sudo mount /data sudo grep -q night /data/settings/settings.json || fail "last durable value потерян после power-cut" pass "power-cut-сим: /data консистентен (fsck clean, night present)" sudo systemctl start shturman.target for _ in $(seq 1 15); do systemctl is-active --quiet shturman-settings && break; sleep 1; done # watchdog/save-time конфиг присутствует test -f /etc/systemd/system.conf.d/shturman-watchdog.conf || fail "нет watchdog-конфига" systemctl is-active --quiet shturman-savetime.timer && pass "savetime.timer активен" || echo " WARN: savetime.timer не активен" pass "watchdog-конфиг (RuntimeWatchdogSec) на месте" ``` - [ ] **Шаг 4 — shellcheck + commit.** ```bash shellcheck -S warning tests/e2e/run.sh git add lima/shturman.yaml tests/e2e/run.sh git commit -s -m "feat(v0.3): lima/E2E блок power-safe (N циклов + abort + power-cut)" ``` --- ## P7.5: verify в Lima + acceptance + синхронизация доков - [ ] **Шаг 1 — host-гейт.** Run: `just ci`. Expected: exit 0 (fsm-юниты + service + integration `#[ignore]`; clippy; deny). Плюс `just test-integration` (session-шина) — оба Power-теста зелёные. - [ ] **Шаг 2 — чистый E2E.** Run: `just vm-reset && just e2e`. Expected: exit 0; power-safe-блок зелёный (N=3 цикла /data цел, abort→ShutdownAborted, power-cut fsck clean); **регресс v0.1/v0.2 зелёный**; `E2E OK ✅`. - [ ] **Шаг 3 — итерации** по реальным ошибкам (grace-таймауты, umount-holders, fsck, proxy-сигналы) — систематически, один симптом → одна правка, повтор P7.5 шаг 2. - [ ] **Шаг 4 — prod-build-gate.** Run: `cargo build -p shturman-power --no-default-features && ! strings target/debug/shturman-power | grep -q PowerMock1`. Expected: сборка ок, `PowerMock1` отсутствует. - [ ] **Шаг 5 — синхронизация доков (швы §10):** `docs/domains/b-power-lifecycle.md` (реализованные срезы B01–B07 в v0.3; abort/PONR-модель VM; HW/MCU/B08-B09 → v0.4); `docs/contracts/ipc.md` §3 (Power оживлён из FSM); `docs/specs/v0.1-v0.6-foundation.md` §5.2 («стаб» → реальный FSM); `CLAUDE.md` (статус v0.3 готово → v0.4/v0.5). - [ ] **Шаг 6 — commit доков.** ```bash git add docs/ CLAUDE.md git commit -s -m "docs(v0.3): синхронизация швов power-safe + статус" ``` - [ ] **Шаг 7 — finishing-a-development-branch** (merge/PR — спросить пользователя; в `main` без явного «ок» не мержить). --- ## Acceptance (спека v0.3 §9.4) - [ ] FSM: все переходы §5 — unit-тесты; sleep/battery_cutoff — no-op. - [ ] `ShutdownImminent` на ACC-off; **abort до PONR → `ShutdownAborted`**; commit после grace + durable-barrier. - [ ] **N=3 цикла зажигания — `/data` + счётчик целы**. - [ ] power-cut-сим — `/data` консистентен (`fsck -n` clean, last value present). - [ ] `Uptime` монотонен; watchdog/save-time конфиг на месте. - [ ] Регресс v0.1/v0.2 зелёный; `just ci` зелёный; prod-build-gate (нет `PowerMock1`); красные линии целы (нет CAN/actuator).