diff --git a/docs/specs/plans/01-workspace-and-common.md b/docs/specs/plans/01-workspace-and-common.md new file mode 100644 index 0000000..f60e859 --- /dev/null +++ b/docs/specs/plans/01-workspace-and-common.md @@ -0,0 +1,747 @@ +# План 1 — Воркспейс + governance + `shturman-common` + +> **For agentic workers:** REQUIRED SUB-SKILL: используй `superpowers:subagent-driven-development` (рекомендуется) +> или `superpowers:executing-plans` для исполнения по задачам. Шаги — чекбоксы (`- [ ]`) для трекинга. + +**Goal:** поднять Rust-воркспейс «Штурмана» с зелёным гейтом (`build`/`test`/`lint`/`deny`), governance-файлами +(MIT/DCO/CONTRIBUTING/README) и общей библиотекой `shturman-common` (layout `/data`, durable-write #5, +монотонные часы, tracing→journald) под TDD. + +**Architecture:** один Cargo-workspace; первый крейт `shturman-common` — фундамент всех сервисов/апов +(не зависит ни от кого). Governance + CI-гейт с дня 1 (principles #12). Без Lima/systemd (они в Плане 5). + +**Tech Stack:** Rust (workspace), `tracing`+`tracing-journald` (логи A10), `tempfile` (dev-тесты), +`cargo-deny` (лицензии), `just` (команды), GitHub Actions ARM64. + +**Контекст:** источник правды — [спека v0.1/v0.6](../v0.1-v0.6-foundation.md). Работаем в ветке от `main` +(напр. `feat/v0-foundation`); в `main` не коммитим без явного «ок». Каждый коммит завершается +`Co-Authored-By: Claude Opus 4.8 `. + +--- + +### Task 1: Воркспейс + скелет `shturman-common` + +**Files:** +- Create: `Cargo.toml` (workspace) +- Create: `rust-toolchain.toml` +- Create: `crates/shturman-common/Cargo.toml` +- Create: `crates/shturman-common/src/lib.rs` + +- [ ] **Step 1: Создать `Cargo.toml` (workspace-манифест)** + +```toml +[workspace] +resolver = "2" +members = ["crates/*", "crates/core/*", "crates/apps/*", "crates/tools/*"] + +[workspace.package] +edition = "2021" +license = "MIT" +rust-version = "1.83" + +[workspace.dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "sync", "time"] } +zbus = "4" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_yaml = "0.9" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-journald = "0.3" +anyhow = "1" +thiserror = "1" +clap = { version = "4", features = ["derive"] } +# dev +tempfile = "3" +# slint — добавляется в Плане 4 (вместе с slint-exception в deny.toml) +``` + +> Версии — опорные; точные пины фиксируются в `Cargo.lock` при первой сборке. + +- [ ] **Step 2: Создать `rust-toolchain.toml`** + +```toml +[toolchain] +channel = "1.83.0" +components = ["rustfmt", "clippy"] +``` + +- [ ] **Step 3: Создать `crates/shturman-common/Cargo.toml`** + +```toml +[package] +name = "shturman-common" +version = "0.0.0" +edition.workspace = true +license.workspace = true + +[dependencies] +tracing.workspace = true +tracing-subscriber.workspace = true +tracing-journald.workspace = true +thiserror.workspace = true + +[dev-dependencies] +tempfile.workspace = true +``` + +- [ ] **Step 4: Создать `crates/shturman-common/src/lib.rs` (скелет)** + +```rust +//! Общая инфраструктура Штурмана: layout `/data`, durable-write, монотонные часы, логи. + +pub mod atomic; +pub mod clock; +pub mod log; +pub mod paths; + +pub use atomic::write_atomic; +pub use clock::monotonic_secs; +pub use log::init_tracing; +pub use paths::Layout; +``` + +Создать пустые модули, чтобы собиралось (наполним в задачах 2–5): + +`crates/shturman-common/src/paths.rs`, `atomic.rs`, `clock.rs`, `log.rs` — пока с заглушкой `// TODO task N` +заменять нельзя по правилу плана; вместо этого создаём их сразу с минимальным валидным содержимым в +следующих задачах. Для Step 4 создать 4 файла с одной строкой каждый: + +```rust +// paths.rs / atomic.rs / clock.rs / log.rs — наполняется в задачах 2–5 +``` + +- [ ] **Step 5: Проверить сборку** + +Run: `cargo build --workspace` +Expected: `Compiling shturman-common v0.0.0 … Finished` (модули пустые — ок; `lib.rs` ссылается на pub-use +из пустых модулей → ошибка «unresolved import»). + +> ⚠️ Если Step 5 падает на `unresolved import` — это ожидаемо: переходим к задачам 2–5, которые добавят +> реальные символы. Чтобы Step 5 был зелёным изолированно, временно закомментируй `pub use`-строки в `lib.rs` +> и раскомментируй их по мере наполнения модулей. + +- [ ] **Step 6: Commit** + +```bash +git add Cargo.toml rust-toolchain.toml crates/shturman-common +git commit -m "chore(workspace): Rust-воркспейс + скелет shturman-common + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 2: `paths::Layout` — раскладка `/data` (TDD) + +**Files:** +- Modify: `crates/shturman-common/src/paths.rs` +- Test: тот же файл (`#[cfg(test)]`) + +- [ ] **Step 1: Написать падающий тест** + +`crates/shturman-common/src/paths.rs`: + +```rust +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + #[test] + fn subdirs_are_under_root() { + let l = Layout::new("/tmp/x"); + assert_eq!(l.root(), Path::new("/tmp/x")); + assert_eq!(l.apps(), Path::new("/tmp/x/apps")); + assert_eq!(l.settings(), Path::new("/tmp/x/settings")); + assert_eq!(l.state(), Path::new("/tmp/x/state")); + assert_eq!(l.log(), Path::new("/tmp/x/log")); + assert_eq!(l.provisioned_marker(), Path::new("/tmp/x/.shturman-provisioned")); + } +} +``` + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Run: `cargo test -p shturman-common paths` +Expected: FAIL — `cannot find type Layout` / методы не определены. + +- [ ] **Step 3: Реализовать `Layout`** + +В начало `crates/shturman-common/src/paths.rs` (над `#[cfg(test)]`): + +```rust +//! Канонические пути персистентного раздела `/data` (a-base §3, §11). +//! Корень настраивается (тесты/dev) — по умолчанию `/data`, либо env `SHTURMAN_DATA_DIR`. + +use std::path::{Path, PathBuf}; + +/// Раскладка `/data`. Все писатели в `/data` берут пути отсюда (нет хардкода). +#[derive(Debug, Clone)] +pub struct Layout { + root: PathBuf, +} + +impl Layout { + /// Явный корень (используется в тестах и при known-mount). + pub fn new(root: impl Into) -> Self { + Self { root: root.into() } + } + + /// Корень из окружения (`SHTURMAN_DATA_DIR`) или дефолт `/data`. + pub fn from_env() -> Self { + let root = std::env::var_os("SHTURMAN_DATA_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/data")); + Self { root } + } + + pub fn root(&self) -> &Path { + &self.root + } + pub fn apps(&self) -> PathBuf { + self.root.join("apps") + } + pub fn settings(&self) -> PathBuf { + self.root.join("settings") + } + pub fn state(&self) -> PathBuf { + self.root.join("state") + } + pub fn log(&self) -> PathBuf { + self.root.join("log") + } + /// Маркер завершённого first-boot (§7.2). + pub fn provisioned_marker(&self) -> PathBuf { + self.root.join(".shturman-provisioned") + } +} +``` + +- [ ] **Step 4: Запустить — убедиться, что проходит** + +Run: `cargo test -p shturman-common paths` +Expected: PASS (1 test). + +- [ ] **Step 5: Commit** + +```bash +git add crates/shturman-common/src/paths.rs +git commit -m "feat(common): Layout — канонические пути /data + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 3: `atomic::write_atomic` — durable-write (TDD, доказательство #5) + +**Files:** +- Modify: `crates/shturman-common/src/atomic.rs` + +- [ ] **Step 1: Написать падающие тесты (контракт + инвариант #5)** + +`crates/shturman-common/src/atomic.rs`: + +```rust +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn persists_full_contents() { + let d = tempfile::tempdir().unwrap(); + let p = d.path().join("settings.json"); + write_atomic(&p, b"{\"v\":1}").unwrap(); + assert_eq!(fs::read(&p).unwrap(), b"{\"v\":1}"); + } + + #[test] + fn overwrites_and_leaves_no_tmp() { + let d = tempfile::tempdir().unwrap(); + let p = d.path().join("settings.json"); + write_atomic(&p, b"old").unwrap(); + write_atomic(&p, b"new").unwrap(); + assert_eq!(fs::read(&p).unwrap(), b"new"); + assert!(!tmp_path(&p).exists(), "осиротевший .tmp не должен оставаться"); + } + + // Доказательство power-safe #5: прерывание ПОСЛЕ записи tmp, но ДО rename + // не должно повреждать основной файл (остаётся прошлая полная версия). + #[test] + fn interrupted_before_rename_keeps_previous_version() { + let d = tempfile::tempdir().unwrap(); + let p = d.path().join("settings.json"); + write_atomic(&p, b"v1").unwrap(); + // эмулируем краш между create(tmp) и rename: оставляем orphan tmp + fs::write(tmp_path(&p), b"v2-incomplete").unwrap(); + // основной файл всё ещё v1 — rename не случился (главный инвариант #5) + assert_eq!(fs::read(&p).unwrap(), b"v1"); + } +} +``` + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Run: `cargo test -p shturman-common atomic` +Expected: FAIL — `cannot find function write_atomic` / `tmp_path`. + +- [ ] **Step 3: Реализовать durable-write** + +В начало `crates/shturman-common/src/atomic.rs`: + +```rust +//! Durable atomic write (a-base §3 / principle #5): +//! `write-temp → fsync(file) → rename → fsync(dir)`. +//! Гарантия: после успеха `path` содержит новые данные целиком; при сбое до `rename` +//! сохраняется прежняя версия (или отсутствие файла) — никогда не частично записанный файл. +//! Расчёт на единственного писателя поддерева (Settings/State, architecture §3) — фикс. имя `.tmp`. + +use std::fs::{self, File}; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; + +/// Путь временного файла рядом с целевым (`settings.json` → `settings.json.tmp`). +pub fn tmp_path(path: &Path) -> PathBuf { + let name = path + .file_name() + .map(|n| format!("{}.tmp", n.to_string_lossy())) + .unwrap_or_else(|| "shturman.tmp".to_string()); + path.with_file_name(name) +} + +pub fn write_atomic(path: &Path, contents: &[u8]) -> io::Result<()> { + let dir = path.parent().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "путь без родительского каталога") + })?; + let tmp = tmp_path(path); + + { + let mut f = File::create(&tmp)?; + f.write_all(contents)?; + f.sync_all()?; // fsync(file) + } + + fs::rename(&tmp, path)?; // atomic в пределах одной ФС + + // fsync(dir) делает сам rename durable + let dir_f = File::open(dir)?; + dir_f.sync_all()?; + Ok(()) +} +``` + +- [ ] **Step 4: Запустить — убедиться, что проходит** + +Run: `cargo test -p shturman-common atomic` +Expected: PASS (3 tests). + +- [ ] **Step 5: Commit** + +```bash +git add crates/shturman-common/src/atomic.rs +git commit -m "feat(common): durable atomic write (power-safe #5) + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 4: `clock::monotonic_secs` — монотонные часы (TDD) + +**Files:** +- Modify: `crates/shturman-common/src/clock.rs` + +- [ ] **Step 1: Написать падающий тест** + +`crates/shturman-common/src/clock.rs`: + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn non_decreasing() { + let a = monotonic_secs(); + let b = monotonic_secs(); + assert!(b >= a, "монотонные секунды не должны идти назад"); + } +} +``` + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Run: `cargo test -p shturman-common clock` +Expected: FAIL — `cannot find function monotonic_secs`. + +- [ ] **Step 3: Реализовать** + +В начало `crates/shturman-common/src/clock.rs`: + +```rust +//! Монотонные часы (B §8): lifecycle-таймеры/`Uptime` — только вперёд, не прыгают при NTP/GPS-синке. +//! На Linux `Instant` опирается на `CLOCK_MONOTONIC`. + +use std::sync::OnceLock; +use std::time::Instant; + +static EPOCH: OnceLock = OnceLock::new(); + +/// Секунды от первого обращения в процессе (монотонно). Достаточно для стаб-`Uptime` (v0). +pub fn monotonic_secs() -> u64 { + let epoch = *EPOCH.get_or_init(Instant::now); + epoch.elapsed().as_secs() +} +``` + +- [ ] **Step 4: Запустить — убедиться, что проходит** + +Run: `cargo test -p shturman-common clock` +Expected: PASS (1 test). + +- [ ] **Step 5: Commit** + +```bash +git add crates/shturman-common/src/clock.rs +git commit -m "feat(common): монотонные часы (B §8) + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 5: `log::init_tracing` — tracing→journald (A10) + +**Files:** +- Modify: `crates/shturman-common/src/log.rs` + +- [ ] **Step 1: Написать smoke-тест** + +`crates/shturman-common/src/log.rs`: + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn init_does_not_panic() { + // глобальный subscriber ставится один раз; повторный вызов безопасен (try_init) + init_tracing("shturman-test"); + tracing::info!("smoke"); + } +} +``` + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Run: `cargo test -p shturman-common log` +Expected: FAIL — `cannot find function init_tracing`. + +- [ ] **Step 3: Реализовать** + +В начало `crates/shturman-common/src/log.rs`: + +```rust +//! Инициализация логирования (A10): `tracing` → journald, если доступен; иначе stderr +//! (dev на macOS-хосте без journald). Политика volatile/rate-limit — на стороне journald (Плана 5). + +use tracing_subscriber::prelude::*; +use tracing_subscriber::EnvFilter; + +/// Идемпотентно (повторный вызов проглатывается `try_init`). Уровень — из `RUST_LOG`, дефолт `info`. +pub fn init_tracing(service: &str) { + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + let registry = tracing_subscriber::registry().with(filter); + + match tracing_journald::layer() { + Ok(journald) => { + let _ = registry + .with(journald.with_syslog_identifier(service.to_string())) + .try_init(); + } + Err(_) => { + let _ = registry.with(tracing_subscriber::fmt::layer()).try_init(); + } + } +} +``` + +- [ ] **Step 4: Запустить — весь крейт зелёный** + +Run: `cargo test -p shturman-common` +Expected: PASS (все тесты paths/atomic/clock/log). +Затем убедиться, что `lib.rs` `pub use`-строки раскомментированы и `cargo build --workspace` зелёный. + +- [ ] **Step 5: Commit** + +```bash +git add crates/shturman-common/src/log.rs crates/shturman-common/src/lib.rs +git commit -m "feat(common): init_tracing → journald (A10) + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 6: Governance-файлы (LICENSE / DCO / CONTRIBUTING / README) + +**Files:** +- Create: `LICENSE`, `DCO`, `CONTRIBUTING.md`, `README.md` + +- [ ] **Step 1: `LICENSE`** — дословно из [спеки §10](../v0.1-v0.6-foundation.md) (MIT, `Copyright (c) 2026 K9 Shturman`). + +- [ ] **Step 2: `DCO`** — текст Developer Certificate of Origin 1.1: + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +- [ ] **Step 3: `CONTRIBUTING.md`** — развернуть [спеку §11](../v0.1-v0.6-foundation.md) в прозу (13 пунктов: + философия; красные линии #1–#2; docs как источник правды; лицензионная гигиена #12 + LGPL гранулярно + + slint-exception; рабочий цикл спека→TDD→verify→commit; стиль `rustfmt`/`clippy`; тесты #13; коммиты/ветки + + Co-Authored-By; PR-чеклист; DCO `-s`; CoC-указатель; где обсуждать). Завершить ссылкой на `CLAUDE.md` и `docs/`. + +- [ ] **Step 4: `README.md`** — мини-спека (спека §11 п.13): + +```markdown +# Штурман + +Open-source русскоязычный companion-слой («ОС поверх Linux») для авто на **RK3588**: быстрый Slint-UI + +голосовой RU-ассистент, читающий OBD/CAN **только на чтение**, + расширяемый Plugin API. Лицензия **MIT**. + +## Красные линии (нерушимы) +- **Никогда не safety-critical** (двигатель/тормоза/ABS/ESP/руль/подушки — нет actuator-путей). +- **CAN только на чтение** (OBD-II read; запрещены write/actuator/Mode-04/UDS-write). + +## Документация +- Точка входа: [`CLAUDE.md`](CLAUDE.md) · дизайн (источник правды): [`docs/`](docs/) · + план реализации: [`docs/roadmap.md`](docs/roadmap.md), спеки — [`docs/specs/`](docs/specs/). + +## Быстрый старт (dev, Lima-VM — появляется в Плане 5) +``` +just vm-up # поднять dev-VM (Lima) +just run # boot → стаб-сервисы на D-Bus → первый Slint-кадр +just ci # lint + test + deny +``` + +## Лицензия +MIT (см. [`LICENSE`](LICENSE)). Контрибьюции — по DCO (`git commit -s`, см. [`CONTRIBUTING.md`](CONTRIBUTING.md)). +**Примечание:** UI-тулкит Slint для embedded доступен бесплатно под GPL-3.0 → шипимый UI-бинарь прод-образа +(v4) будет под GPL-3.0; решение по тулкиту/лицензии отложено к v4 (см. `docs/specs/`). +``` + +- [ ] **Step 5: Commit** + +```bash +git add LICENSE DCO CONTRIBUTING.md README.md +git commit -m "docs(governance): LICENSE (MIT) + DCO + CONTRIBUTING + README + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 7: `deny.toml` + cargo-deny зелёный + +**Files:** +- Create: `deny.toml` + +- [ ] **Step 1: Создать `deny.toml`** + +```toml +# Лицензионная гигиена (#12) + advisories. slint GPL-3.0-exception добавится в Плане 4. +[advisories] +version = 2 + +[licenses] +version = 2 +allow = [ + "MIT", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-3.0", + "Unicode-DFS-2016", + "Zlib", +] +confidence-threshold = 0.9 +# LGPL — гранулярно (не blanket-deny): рассматриваем точечно при появлении (#12, spec §3). + +[bans] +multiple-versions = "warn" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +``` + +- [ ] **Step 2: Установить cargo-deny (если нет) и проверить** + +Run: `cargo install cargo-deny --locked` (один раз) · затем `cargo deny check` +Expected: `licenses ok`, `advisories ok`, `bans ok`, `sources ok` — на текущем графе (только пермиссивные +зависимости `shturman-common`). Если какой-то транзитив имеет лицензию вне allow (напр. `Unicode-3.0` +у `unicode-ident`) — добавить её в `allow` (она пермиссивная) и перезапустить. + +- [ ] **Step 3: Commit** + +```bash +git add deny.toml +git commit -m "chore(license): deny.toml — allow-list + advisories (#12) + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 8: `justfile` (ядро команд) + CI-workflow + +**Files:** +- Create: `justfile` +- Create: `.github/workflows/ci.yml` + +- [ ] **Step 1: Создать `justfile` (ядро; vm-*/run/e2e — в следующих планах)** + +```make +# Штурман — единые dev-команды (расширяется по планам). +set shell := ["bash", "-uc"] + +# список целей +default: + @just --list + +# собрать весь воркспейс +build: + cargo build --workspace + +# тесты (unit + integration) +test: + cargo test --workspace + +# линт: формат + clippy (warnings = ошибки) +lint: + cargo fmt --all --check + cargo clippy --workspace --all-targets -- -D warnings + +# лицензии + advisories +deny: + cargo deny check + +# полный локальный гейт +ci: lint test deny +``` + +- [ ] **Step 2: Создать `.github/workflows/ci.yml`** + +```yaml +name: CI +on: + push: + branches: [main] + pull_request: + +jobs: + lint: + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - run: cargo fmt --all --check + - run: cargo clippy --workspace --all-targets -- -D warnings + + test: + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test --workspace + + license: + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 +``` + +> ⚠️ ARM64-раннеры (`ubuntu-24.04-arm`) — если вне бесплатного тира, фолбэк: `ubuntu-latest` (x86) + +> кросс/`--target` (dev-environment §CI). `prod-build-gate` (`--no-default-features`, §8.3 спеки) добавится +> в Плане 3/4, когда появится фича `dev-mocks`. + +- [ ] **Step 3: Проверить полный гейт локально** + +Run: `just ci` +Expected: `lint` (fmt+clippy чисто) → `test` (все тесты common зелёные) → `deny` (ok). Всё зелёное. + +- [ ] **Step 4: Commit** + +```bash +git add justfile .github/workflows/ci.yml +git commit -m "chore(dev): justfile (ядро) + CI-гейт (lint/test/deny) + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +## Self-review (по спеке) + +- **Покрытие спеки:** Task 1–8 закрывают из §2.1 «Governance» (LICENSE/CONTRIBUTING/DCO/README + deny.toml), + часть «v0.6 dev-харнесс» (justfile-ядро, CI) и фундамент крейтов (`shturman-common` — §4.2: layout `/data`, + durable-write, монотонные часы, tracing→journald). Lima/systemd/сервисы — Планы 3–5 (не forward-ref здесь). +- **Power-safe #5:** durable-write реализован и **доказан unit-тестом** прерывания до `rename` (спека §3/§9.1) — + не reboot. +- **#12:** deny.toml + CI-гейт с дня 1; slint-exception явно отложен в Task (Плана 4), когда slint войдёт в граф. +- **Плейсхолдеры:** код полный; `CONTRIBUTING.md` (Task 6 Step 3) ссылается на §11 спеки как на источник + содержания — развернуть в прозу при исполнении (не оставлять списком). +- **Типы/имена:** `Layout`, `write_atomic`/`tmp_path`, `monotonic_secs`, `init_tracing` — согласованы с + `lib.rs` re-export и будущими потребителями (firstboot/settings/power — План 3). + +## Готово, когда (acceptance Плана 1) + +- [ ] `just ci` зелёный локально (lint + test + deny). +- [ ] `cargo test -p shturman-common` — paths/atomic/clock/log проходят; тест прерывания durable-write зелёный. +- [ ] Governance-файлы на месте (`LICENSE`/`DCO`/`CONTRIBUTING.md`/`README.md`/`deny.toml`). +- [ ] CI-workflow присутствует (зелёный на ARM64-раннере или x86-фолбэке). + +## Дальше + +План 2 — `shturman-ipc` (контракт: Error/имена/энумы/`#[proxy]` Power1+Settings1) + `shturman-sdk` +(`connect`/`SettingsClient`/`PowerClient`/`manifest`). diff --git a/docs/specs/v0.1-v0.6-foundation.md b/docs/specs/v0.1-v0.6-foundation.md new file mode 100644 index 0000000..45f0166 --- /dev/null +++ b/docs/specs/v0.1-v0.6-foundation.md @@ -0,0 +1,672 @@ +# Спека реализации: v0.1 (Образ-болванка) + v0.6 (dev-харнесс) + шагающий скелет + +> **Тип документа:** имплементационная спека вехи (вход в TDD-цикл, не код). +> Фаза реализации (CLAUDE.md): **спека → правки → adversarial-ревью → TDD → verify в VM → коммит.** +> Код, `LICENSE`, `CONTRIBUTING.md`, `Cargo.toml`, крейты — **создаются после утверждения спеки.** +> Содержимое `LICENSE`/`CONTRIBUTING.md` приведено внутри спеки (§10–§11) для ревью. + +Статус: **v1 — применены 17 правок adversarial-ревью + позиция Slint (A); ожидает финального «ок» на TDD-план (2026-06-23).** +Источник правды по дизайну — `docs/`. Эта спека секвенирует доки в код, не противоречит им; при +расхождении — синхронизируем док (двунаправленный шов, см. §13). +Связано: [roadmap §v0](../roadmap.md) · [architecture](../architecture.md) · [tech-stack](../tech-stack.md) · +[dev-environment](../dev-environment.md) · [ipc](../contracts/ipc.md) · [data-model](../contracts/data-model.md) · +[performance](../contracts/performance.md) · [plugin-sdk](../contracts/plugin-sdk.md) · [security-privacy](../contracts/security-privacy.md) · домены [A](../domains/a-base-system.md)/[B](../domains/b-power-lifecycle.md)/[C](../domains/c-shell-ux.md)/[F](../domains/f-plugin-host.md) · [principles](../principles.md) + +> **Changelog v0→v1 (по adversarial-ревью, 17 находок):** every-boot bind `machine-id` (§7.2/§7.6); автомонтирование +> `/data` + ordering `Requires` (§7.1/§7.6); `deny.toml` — Slint=GPL-3.0 exception, позиция A (§3/§12); #5 доказывается +> unit-тестом атомарности, не reboot (§9.1/§9.3); измеримый eMMC-прокси (§7.5/§9.3); `grim`→software-renderer/ +> `weston-screenshooter` (§6); срезы C05/C07/C02 в трассировке (§2.4/§6); шов §6↔C§2 разделён (§2.2); `barrier=1` +> убран (§7.1); zram-generator (§7.4); путь `fake-hwclock`→`/data` (§7.3); гейт `dev-mocks` (§5.2/§8.3); factory-reset +> тест (§9.1); per-unit `is-active` вместо degraded (§9.3); README мини-спека (§11); LGPL гранулярно (§3/§11); указатель +> security-privacy (§1/§3); новый §13 «двунаправленные швы». + +--- + +## 1. Цель и первый артефакт + +**Цель спеки:** заложить фундамент репозитория и dev-цикла так, чтобы получить **первый запускаемый +артефакт** (CLAUDE.md): + +> **boot в Lima-VM → стаб-сервисы (`ru.shturman.Power` / `ru.shturman.Settings`) на D-Bus → первый +> Slint-кадр, читающий состояние с шины.** + +Это **«шагающий скелет» (tracer bullet):** тонкий сквозной поток через все слои архитектуры +(Linux base → trusted core → SDK → first-party app), на стабах. Он доказывает, что плоскости +(D-Bus control-plane, Slint UI), границы крейтов, dev-харнесс и тест-пирамида работают **до** того, +как мы вложимся в реальную логику power-safe (v0.3), Vehicle-Data (v2) и полный shell (v0.5). + +**Архитектурный тезис, который скелет валидирует рано:** «тонкое ядро + всё на SDK» — first-party +Shell общается с ядром (`Power`/`Settings`) **через тот же `shturman-sdk`**, что и будущие сторонние +плагины (architecture §1, principles #9). + +--- + +## 2. Скоуп и границы + +### 2.1 В скоупе (делаем сейчас) + +| Трек | Что | ID каталога | +|------|-----|-------------| +| **v0.1** Образ-болванка (в VM) | layout RO-rootfs + overlay(tmpfs) + журналируемый `/data`; first-boot provisioning; локаль | A02 A06 A17 (+ A01 как dev-Ubuntu) | +| **v0.6 base day-1** | zram/OOM/cgroup-конфиг; journald volatile + критичное в `/data`; eMMC write-min; время (timesyncd + fake-hwclock); reference-«BSP» = Lima-профиль | A09 A10 A11 A07 A16 | +| **v0.6 dev-харнесс** | Lima-VM + provisioning; `justfile`; CI-гейт; vcan/моки-каркас; `shturman-sdk`; валидатор манифеста + scaffolding | F01 F02 (F03/F04/J06 — частично, см. §2.3) | +| **Шагающий скелет** | стаб `ru.shturman.Power` + `ru.shturman.Settings` на шине; минимальный Slint-кадр на SDK; systemd-таргет `shturman.target` | срезы B04 / Settings-core / C03 C04 **C05 C07 C02** / A15 | +| **Governance** | `LICENSE` (MIT) + `CONTRIBUTING.md` + `DCO` + `README.md`; `deny.toml` (cargo-deny, #12) | — | + +### 2.2 Явно НЕ в скоупе (отложено, с указателем «куда») + +Скелет намеренно **стабит**, а не реализует, чтобы не тащить v0.3/v0.5/v2/v4 в первый артефакт: + +| Отложено | Куда | Почему не сейчас | +|----------|------|------------------| +| Полная машина состояний `Power` + graceful shutdown sequencing + abort/PONR | **v0.3** (домен B §2/§4) | стаб лишь публикует интерфейс + эмитит сигналы по dev-триггеру | +| MCU-копилот / supercap, hold-up, thermal-trip, watchdog-арбитраж | **v0.3/v0.4** (B §5/§6, hardware) | требует железа (🟡 B08/B09) | +| smithay-композитор (мульти-клиент) + слот-поверхности (`ru.shturman.shell_slot`) | **v0.5 / с первым surface-апом** (C §2, C06) | v0-shell = одиночное Slint-приложение (де-рисковка C §2) | +| App-Host + Perm-Broker (запуск/песочница/прокси) | **v3 / с первым плагином** | в v0 нет сторонних плагинов и surface-апов; Shell — обычный systemd-сервис на шине | +| Vehicle-Data (`ru.shturman.VehicleData`), CAN/OBD, DTC | **v2** (домен E) | vcan поднимаем в VM, но Vehicle Simulator и сервис — позже | +| Реальный A/B boot-select (U-Boot env), secure boot, at-rest (fscrypt), OTA (RAUC) | **v4** (A §4/§5, hardware) | в VM нет U-Boot; моделируем **layout**, не boot-select | +| Полный home/тайлы/тема день-ночь по design-system, анимации | **v0.5** (C §3, design-system) | сейчас — каркас кадра + нейтральные плейсхолдеры под токены | +| Connectivity, аудио (PipeWire-роли), Location/GPS | **v1** | пакеты ставим для воспроизводимости, но не задействуем | + +> **Два независимых шва v0 (проговариваю явно — это РАЗНЫЕ решения):** +> **(1) Композитор** — по C §2 v0-shell стартует как одиночное Slint-приложение; полноценный smithay-композитор + +> слот-поверхности включаем с первым surface-апом (v0.5). Это **прямая де-рисковка C §2**. +> **(2) App-Host / Perm-Broker** — architecture §6 ставит их в Stage 1 *до* Shell (Shell цепляется через них). C §2 +> это **не** покрывает (там только про композитор). Обоснование их отсутствия в v0 — **другое:** в скелете нет +> песочных апов/плагинов → **нечего брокерить и хостить**, поэтому Shell поднимается обычным systemd-сервисом и +> цепляется к шине напрямую; Broker/App-Host включаются с **первым песочным клиентом** (плагин — v3). Это осознанное +> секвенирование. По двунаправленному шву — добавить ремарку в architecture §6 (см. §13). + +### 2.3 Частично в скоупе (каркас сейчас, тело — позже в v0) + +Честно фиксируем (principle: no silent caps): + +- **F03 Dev-run плагина в VM** — зависит от **App-Host** (нет в v0-скелете). Делаем: `justfile`-таргет + `plugin-dev-run` + каркас, который **корректно сообщает** «App-Host появится в v3». Тело — когда App-Host. +- **F04 Тест-харнесс plugin-host** — делаем **сейчас**: библиотека «плохих манифестов» (`fixtures/manifests/`) + как фикстуры для валидатора (F02). Рантайм fault-injection (crash-loop, mem-hog) — **позже** (нужен App-Host). +- **J06 Dev-симулятор камер** — каркас в `sim/` + `just`-плейсхолдер; тело — в домене J (v2). На критпути + первого артефакта не нужен (камер нет до v2). +- **vcan + Vehicle Simulator** — vcan-модуль и `vcan0` **поднимаем** в provisioning (воспроизводимость + базы); сам симулятор (Python, ELM327-emu) — домен E (v2). `just sim` — плейсхолдер. + +### 2.4 Трассируемость ID → статус в этой спеке + +| ID | Функция | Статус сейчас | Раздел | +|----|---------|---------------|--------| +| A01 | base-образ | **dev = Lima Ubuntu ARM64**; прод-образ (Armbian/Debian vs Yocto 🟡) — v4 | §7.1, §8 | +| A02 | RO-rootfs A/B + overlay(tmpfs) + `/data` | **layout в VM** (boot-select — v4/HW); ФС dev=ext4 (f2fs 🟡 на HW) | §7.1 | +| A06 | First-boot provisioning | **полностью** (`shturman-firstboot`, идемпотентно) | §7.2 | +| A07 | Время (timesyncd + fake-hwclock) | **OS-уровень в provisioning**; B-owned save-on-shutdown — v0.3 | §7.3 | +| A09 | Память (zram/OOM/cgroup) | **конфиг день-1**; числа-бюджеты — на железе | §7.4 | +| A10 | Логи (journald volatile + критичное `/data` + pstore) | **полностью (конфиг)** | §7.5 | +| A11 | eMMC write-min | **дисциплина + измеримая проверка** | §7.5 | +| A15 | systemd-таргеты/оркестрация | **`shturman.target` + юниты + ordering** | §7.6 | +| A16 | reference-BSP | **dev-«BSP» = Lima-профиль**; реальный DT/HAL/DBC — HW | §8.1 | +| A17 | Локаль (ru_RU.UTF-8, tzdata, кириллица, keymap) | **полностью (provisioning)** | §7.1 | +| B04 | `ru.shturman.Power` сервис | **стаб-интерфейс + dev-mock** (срез v0.3) | §5.2 | +| C03/C04 | Shell первый кадр / Slint-shell | **срез** (минимальный кадр) | §6 | +| C05 | Декларативный рендер тайлов | **срез** (плейсхолдер-тайлы); полный — v0.5 | §6 | +| C07 | Статус-бар время+сеть-unknown | **срез** (минимум); полный — v0.5 | §6 | +| C02 | Тема день/ночь | **срез** (по локальному времени); GPS-восход/датчик — v1/later | §6 | +| F01 | `shturman-sdk` | **MVP-крейт** (proxy-обёртки + схема манифеста) | §4, §8.4 | +| F02 | scaffolding + валидатор манифеста | **полностью** | §8.4 | +| F03 | Dev-run плагина | **каркас**, тело — с App-Host (v3) | §2.3, §8.3 | +| F04 | Тест-харнесс plugin-host | **bad-manifest фикстуры**; рантайм — позже | §2.3, §8.4 | +| J06 | Dev-симулятор камер | **каркас**; тело — v2 | §2.3 | +| — | `ru.shturman.Settings` (core-инфра) | **стаб + атомарная запись в `/data`** | §5.3 | + +--- + +## 3. Красные линии, безопасность, лицензии в v0 + +- **CAN только на чтение / никогда не safety-critical** держатся в v0 **тривиально и архитектурно:** + Vehicle-Data не существует, путей к CAN/actuator нет вообще. В `shturman-ipc`/`shturman-sdk` **не + определяется ни одного** write/actuator-метода или capability — «их не существует» (principles #2). +- **Power-safe (#5) с дня 1:** даже стаб `Settings` пишет в `/data` **только** durable-write-контрактом + (`write-temp → fsync → rename → fsync(dir)`, a-base §3). **Доказывается unit-тестом атомарности** (§9.1, + симуляция сбоя между `rename` и `fsync(dir)`), **не** graceful-reboot (reboot флашит кэш и проходит даже + при неатомарной записи — §9.3 шаг 4 переименован в функциональный). Реальный power-cut-тест — v0.3. +- **Лицензионная гигиена (#12):** `deny.toml` + CI-гейт `cargo-deny` с дня 1 — allow MIT/Apache-2.0/BSD/ + ISC/Unicode/Zlib; deny GPL/AGPL (заразный копилефт). **LGPL — гранулярно** (не blanket-deny: динамическая/ + системная линковка допустима), согласовано с principles #12. + - **⚠️ Slint = `GPL-3.0` для embedded.** Royalty-free лицензия Slint **исключает embedded**, а авто-приборка = + embedded (Slint сам называет «car dashboard» примером в FAQ). Бесплатный путь = только GPL-3.0 (шипимый + UI-бинарь — копилефт). **Решение по UI-тулкиту/лицензии — вариант A: отложено к v4** (§12 п.8). На v0 **dev в + Lima-VM не триггерит** копилефт-обязательств распространения (они срабатывают при поставке прод-образа, v4) → + в `deny.toml` явный **exception**: `slint = GPL-3.0, pending v4`. Makepad/Iced (MIT) — известные пермиссивные + запасные. Двунаправленный шов в tech-stack — §13. + - GPL-демоны (NM/MM/BlueZ) — отдельные процессы через D-Bus, не линкуются (вне графа cargo-deny), появляются в v1. +- **Приватность/безопасность (указатель-шов):** по умолчанию **нулевой egress**/телеметрия (security-privacy §7); + at-rest-шифрование и нормативный audit-log — отложены туда (v2–v4); capability-таксономия валидатора (§8.4) — из + security-privacy §3. В v0 нет сети/plugin-host/Vehicle-Data → активной работы здесь нет, только шов. +- **Отзывчивость (#11):** Slint UI-поток не блокируем; D-Bus-вызовы из Shell — async (`zbus`+`tokio`). + Перф-**вердикт** — на RK3588 (performance §2); в VM/CI — функциональные проверки + seed perf-gate. + +--- + +## 4. Раскладка Rust-воркспейса + +### 4.1 Дерево + +``` +shturman/ +├── Cargo.toml # [workspace] + [workspace.dependencies] (единые пины) +├── rust-toolchain.toml # пин тулчейна (stable, профиль) +├── deny.toml # cargo-deny: лицензии (#12) + advisories (+ slint GPL-3.0 exception) +├── justfile # единые dev-команды (§8.2) +├── LICENSE # MIT (§10) +├── CONTRIBUTING.md # governance (§11) +├── DCO # Developer Certificate of Origin 1.1 (§11 п.10) +├── README.md # точка входа репозитория (мини-спека §11 п.13) +├── lima/ +│ └── shturman.yaml # Lima-шаблон VM (§8.1) +├── systemd/ # юниты + политика шины + drop-ins (§7) +│ ├── shturman.target +│ ├── data.mount # постоянный mount loop-/data (power-safe опции, §7.1/§7.6) +│ ├── shturman-firstboot.service # A06 (After/Requires data.mount) +│ ├── shturman-machineid.service # every-boot bind /data/state/machine-id → /etc/machine-id (§7.2/§7.6) +│ ├── shturman-power.service +│ ├── shturman-settings.service +│ ├── shturman-shell.service +│ ├── dbus/ru.shturman.conf # D-Bus policy (own-имена сервисов) +│ ├── dbus/ru.shturman.dev.conf # dev-only drop-in: policy для ru.shturman.dev.* (НЕ в прод-образе) +│ ├── journald-shturman.conf # Storage=volatile + RateLimit (A10) +│ ├── zram-generator.conf # zram через zram-generator, секция [zram0] (A09) +│ └── oomd-shturman.conf # systemd-oomd политика (A09) +├── crates/ +│ ├── shturman-common/ # lib: tracing/journald init, layout `/data`, монотон. часы, durable-write +│ ├── shturman-ipc/ # lib: Error-тип, имена/пути/версии, zbus #[proxy] Power1/Settings1 +│ ├── shturman-sdk/ # lib (F01): client-обёртки, схема манифеста (serde), helpers +│ ├── core/ +│ │ ├── shturman-firstboot/ # bin (A06): идемпотентный init `/data` + генерация machine-id +│ │ ├── shturman-power/ # bin (B04 стаб): сервер Power1 + dev-mock (feature) +│ │ └── shturman-settings/ # bin (стаб): сервер Settings1 + атомарный стор в `/data` +│ ├── apps/ +│ │ └── shturman-shell/ # bin (C03/C04 минимум): Slint-кадр на SDK +│ └── tools/ +│ └── shturman-manifest-validator/ # bin (F02): валидатор manifest.yaml против схемы SDK +├── templates/ +│ └── plugin/ # scaffolding для `just new-plugin` (F02) +├── fixtures/ +│ └── manifests/ # F04: библиотека плохих манифестов + один валидный +├── sim/ # dev-симуляторы (Python): Vehicle Sim (v2), camera mock (v2) — каркас +└── tests/ + └── e2e/ # E2E-харнесс в VM (boot→шина→кадр) — раннер + ассерты +``` + +`[workspace] members = ["crates/*", "crates/core/*", "crates/apps/*", "crates/tools/*"]`. + +### 4.2 Ответственность крейтов и границы + +| Крейт | Тип | Ответственность | Зависит от | +|-------|-----|-----------------|------------| +| `shturman-common` | lib | `tracing`→journald init; layout-константы `/data` (`/data/{apps,settings,state,log}`); монотонные часы (`CLOCK_MONOTONIC`, B §8); helper атомарной записи (durable-write, a-base §3) | — | +| `shturman-ipc` | lib | **Контракт шины**: `ru.shturman.Error.*` (zbus `DBusError`); константы well-known имён/путей/версий интерфейсов; `#[proxy]`-трейты `Power1`, `Settings1`; типы-энумы (`PowerState`, `IgnitionState`, `PowerSource`, `ShutdownReason`) с сериализацией в строки на проводе | `common` | +| `shturman-sdk` | lib (**F01**) | **Публичный API платформы**: `connect()` (бутстрап соединения), ergon-обёртки `SettingsClient`/`PowerClient` над proxy из `ipc`; **схема манифеста** (`manifest::Manifest`, serde, plugin-sdk §2); helpers | `ipc`, `common` | +| `shturman-firstboot` | bin (**A06**) | Идемпотентный first-boot: структура `/data`, **генерация** persistent `machine-id` в `/data/state/`, посев дефолт-настроек, durable-write маркера (привязку machine-id делает отдельный every-boot юнит — §7.2) | `common` | +| `shturman-power` | bin (**B04** стаб) | Сервер `ru.shturman.Power1` (стаб-поведение §5.2) + dev-mock-интерфейс (feature `dev-mocks`) | `ipc`, `common` | +| `shturman-settings` | bin (стаб) | Сервер `ru.shturman.Settings1` (§5.3) + атомарный стор в `/data/settings/` | `ipc`, `common` | +| `shturman-shell` | bin (**C03/C04**) | Slint-приложение: первый кадр; читает `Settings`/`Power` **через `shturman-sdk`** | `sdk`, `common`, `slint` | +| `shturman-manifest-validator` | bin (**F02**) | Валидирует `manifest.yaml` против `shturman_sdk::manifest` (схема + правила plugin-sdk §2/§3) | `sdk` | + +**Границы (ключевое):** +- **Сервисы ядра (`power`/`settings`) зависят от `ipc` (контракт), НЕ от `sdk` (клиент).** Они реализуют + **server-side** (`#[interface]`). SDK — клиентская сторона. Это держит границу «ядро публикует ↔ апы + потребляют» (architecture §3). +- **Апы (`shell`) зависят от `sdk`** — реализуют тезис «first-party на том же SDK» (#1, #9). +- `ipc` — единственный источник имён/версий/ошибок; и сервер, и клиент берут их отсюда (нет рассинхрона + строк имён). Полные XML-сигнатуры фиксируются при реализации (ipc §2). + +### 4.3 Общие зависимости (workspace.dependencies) + +`tokio` (async-рантайм), `zbus` (D-Bus), `serde`/`serde_yaml`/`serde_json` (манифест/стор), `tracing` + +`tracing-subscriber` + `tracing-journald` (логи A10), `slint` (UI; **GPL-3.0**, exception в `deny.toml` — §3/§12), +`anyhow`/`thiserror` (ошибки), `clap` (CLI у bin-ов). Точные версии пинятся в `Cargo.lock` при реализации; +лицензии — через `cargo-deny`. + +--- + +## 5. Контракты D-Bus-стабов + +Соглашения (ipc §2): имя `ru.shturman.`, путь `/ru/shturman/`, интерфейс +`ru.shturman.1` (N = мажор). Всё async. Ошибки — `ru.shturman.Error.`. + +### 5.1 Топология шины (dev) + +- **Прод/VM-E2E:** одна **системная шина устройства** (ipc §1, ✅ зафиксировано 2026-06-23) + policy-файл + `systemd/dbus/ru.shturman.conf` (право `own` на `ru.shturman.Power`/`.Settings`). Policy для `ru.shturman.dev.*` + (dev-mock) — **отдельный dev-only drop-in** `ru.shturman.dev.conf`, **не входящий в прод-образ**. Зеркалит прод. + Decision-журнал ipc.md синхронизируем при реализации (§13). +- **Unit/Integration-тесты:** **приватная сессионная шина** на тест (`dbus-run-session` / встроенный + `dbus-daemon`) — герметично, параллелизуемо, без root (§9). +- Изоляция песочных апов — через прокси (появится с App-Host, v3). + +### 5.2 `ru.shturman.Power` — стаб (B04, домен B §9) + +- **Имя/путь/интерфейс:** `ru.shturman.Power` · `/ru/shturman/Power` · `ru.shturman.Power1`. +- **Методы:** + - `GetPowerState() → s` — текущее состояние, enum-строка ∈ `{off, accessory, running, shutting_down, sleep, battery_cutoff}`. + - `RequestSleep()` — внутренний; **в стабе no-op** (полная sleep/wake — v1/v2, B §7). +- **Сигналы:** + - `AccChanged(b on)` + - `ShutdownImminent(u seconds, s reason)` — `reason ∈ {acc_off, under_voltage, thermal, battery_cutoff}` + - `ShutdownAborted()` — re-power до PONR + - `Sleep()`, `Wake()` — **объявлены в интерфейсе (канон ipc §3), но в v0-стабе НЕ эмитятся** — + зарезервированы до v1/v2 (B §7), как `RequestSleep` и неиспользуемые `Error`-варианты (§5.4). +- **Properties (+ `PropertiesChanged`):** + - `IgnitionState: s` ∈ `{off, accessory, running}` — **канон** (B §1; E зеркалит, не дублирует) + - `Uptime: t` — секунды **монотонных** часов (`CLOCK_MONOTONIC`, B §8) + - `PowerSource: s` ∈ `{vehicle_12v, holdup_cap, sleep_rail, low_battery}` +- **Стаб-поведение (v0):** старт в `running`/`IgnitionState=running`/`PowerSource=vehicle_12v`; + `Uptime` растёт монотонно. **Никаких** методов записи/actuator (#2). Реальная FSM/секвенсинг — v0.3. +- **Dev-mock (feature `dev-mocks`):** доп.интерфейс `ru.shturman.dev.PowerMock1` на том же объекте — + «**fake-ACC**» для тестов и будущего v0.3: + - `SetAcc(b on)` → меняет state/IgnitionState + эмитит `AccChanged` + - `SetIgnition(s state)` + - `TriggerShutdown(u seconds, s reason)` → эмитит `ShutdownImminent` + - `AbortShutdown()` → эмитит `ShutdownAborted` + - **Гейтинг прод:** прод-сборка идёт `--no-default-features` (фича `dev-mocks` выкл.) → `PowerMock1` не + регистрируется; CI имеет job, проверяющий это (§8.3); policy `ru.shturman.dev.*` — dev-only drop-in (§5.1), + не в прод-образе. (Прод-сборки в v0 нет — A01 v4; методы — мок-мутации, не actuator, #2 не нарушают; гейт + заведён заранее, чтобы фича не протекла.) +- **Идентичность/жизненный цикл (ipc §2):** идентичность вызывающего — по соединению (`sender`), не из + аргумента; в v0 (нет песочниц) — без enforcement, **зарезервировано**. Серверное клиент-состояние + (подписки) снимается по `NameOwnerChanged` — даёт `zbus` (тест — §9.2). + +### 5.3 `ru.shturman.Settings` — стаб (core-инфра) + +- **Имя/путь/интерфейс:** `ru.shturman.Settings` · `/ru/shturman/Settings` · `ru.shturman.Settings1`. +- **Методы:** + - `Get(s key) → v value` — `value` как D-Bus variant; неизвестный ключ → `ru.shturman.Error.InvalidArgument`. + - `Set(s key, v value)` — валидировать ключ, записать (атомарно в `/data`), эмитить `Changed`. + - `List(s prefix) → as keys` + - `Reset(s key)` — вернуть дефолт (или удалить), эмитить `Changed`. +- **Сигналы:** `Changed(s key, v value)`. +- **Стор:** `/data/settings/settings.json`; запись — **durable-write** (a-base §3): + `settings.json.tmp` → `fsync(file)` → `rename` → `fsync(dir)`. Загрузка на старте; дефолты сеются + first-boot (§7.2). **Settings — единственный писатель этого поддерева** (architecture §3). +- **Дефолты v0 (seed):** `ui.theme = "auto"` (∈ `{auto, day, night}`), `ui.units = "metric"` (канон + единиц — data-model §2; конвертация на презентации). +- **Namespace-изоляция по `sender→app-id`** (ipc §3) — **отложена** (нет песочных апов в v0); стаб = + плоские ключи. Указатель: enforce появится с App-Host/прокси (v3). + +### 5.4 `ru.shturman.Error` (контракт ошибок, ipc §2) + +Enum в `shturman-ipc` (zbus `DBusError`): `PermissionDenied`, `NotAvailable`, `Stale`, `Timeout`, +`ReadOnly`, `InvalidArgument`, `Unsupported`. В v0 реально используются `InvalidArgument` (Settings) и +зарезервированы остальные (полная семантика — по мере сервисов). + +--- + +## 6. Shell — первый Slint-кадр (срезы C03/C04/C05/C07/C02) + +**Скоуп:** минимальный **первый кадр**, доказывающий сквозной поток, **не** полный v0.5-shell. +**Каталожные срезы (минимум каждого; полные версии — v0.5):** C03 (первый кадр), C04 (Slint-shell), +**C05** (декларативный рендер тайлов — плейсхолдеры), **C07** (статус-бар время + сеть-`«unknown»`), +**C02** (тема день/ночь по локальному времени). + +- **UI (Slint):** окно со **статус-баром** (часы из системного времени; индикатор сети — `«unknown»`, + C §3 деградированный контракт) + **home-грид** из нескольких крупных тайлов-плейсхолдеров + (декларативно) + (dev) индикатор `IgnitionState`. +- **Поток данных (через `shturman-sdk`, не напрямую):** + 1. старт → `sdk::connect()` → `SettingsClient::get("ui.theme")` → применить тему; + 2. подписка `Settings.Changed` → живое обновление темы/единиц; + 3. `PowerClient::power_state()` + подписка `AccChanged`/`PropertiesChanged(IgnitionState)` → отразить. +- **Тема день/ночь:** v0 — **по локальному времени** (RTC/NTP/fake-hwclock, C §6); GPS-восход — v1; + до синка времени — пометка неопределённости (C §3). Визуальные **токены design-system** — каркасом + (нейтральные плейсхолдеры); полный визуальный язык — гейт v0.5. +- **Рендер-бэкенды:** + - *dev интерактивно:* Slint под **weston** (вложенный Wayland) в VM, либо нативно на macOS-хосте. + - *CI/E2E без дисплея (автоматический ассерт кадра):* (a) **Slint software-renderer → RGBA/PNG-буфер** + (без дисплей-сервера и композитора) → ассерт «кадр не пустой» + (b) `slint::testing` — дерево элементов + (root + тайлы + часы) и дата-байндинг темы из Settings. + - ⚠️ **`grim` НЕ годится** (требует `wlr-screencopy`, которого у `weston` нет). Если нужен скриншот именно + под weston — `weston-screenshooter`; но **основной автотест кадра — software-renderer**, композитор не нужен. +- **Границы:** Shell **не** реализует логику апов, **не** трогает CAN/safety (C §1). Композитор (smithay), + слот-поверхности, App-Host-хостинг — **не здесь** (v0.5/v3). + +--- + +## 7. База v0.1 + v0.6 (day-1) + +### 7.1 Образ/layout в VM (A01/A02/A17) + +- **dev-база = Lima Ubuntu ARM64** (нативно к RK3588; A01-прод 🟡 Armbian/Debian vs Yocto — v4). +- **Layout `/data` + overlay (моделируем, не boot-select):** + - `/data` — **отдельный журналируемый носитель**: loopback-образ + **постоянный mount** (`data.mount`/ + fstab, см. §7.6), монтируется **на каждом boot до сервисов** с power-safe-опциями. ФС dev = **ext4**; + **барьеры — дефолт ядра** (legacy `barrier=1` НЕ задаём — он не отображается в `findmnt`), ассертим реально + отображаемые non-default опции (`errors=remount-ro`, явный `commit=N`). **Главная power-safe-гарантия — + durable-write** (§5.3/§9.1), не mount-опция. **f2fs (`fsync_mode=strict`) — на HW** (a-base §3, 🟡 A02; + формулировку a-base §3 про `barrier=1` синхронизируем — §13). + - **overlay upper/work — на tmpfs (volatile)** (a-base §3); персист — только в `/data`. + - RO-rootfs — **дисциплина** (rootfs трактуем read-only; запись только в `/data`/tmpfs). + - **A/B boot-select (U-Boot env) — НЕ в VM** (нет U-Boot): layout документируем, **переключение + слотов — на HW/v4**. Честная граница VM↔HW (a-base §2 — два разных «образа»). +- **Локаль (A17):** `ru_RU.UTF-8` (LANG/LC_*), `tzdata`, **кириллические шрифты до Shell** (консоль/splash), + console keymap — в provisioning. + +### 7.2 First-boot provisioning (A06) — `shturman-firstboot` + +Oneshot-бинарь под `shturman-firstboot.service` (`ConditionPathExists=!/data/.shturman-provisioned`, +`After=`/`Requires=data.mount`), **идемпотентно:** +1. создать структуру `/data/{apps,settings,state,log}`; +2. **сгенерировать** persistent `machine-id` в `/data/state/machine-id` (one-shot; гейт маркером ок). + **Привязку** к `/etc/machine-id` делает **отдельный every-boot юнит** `shturman-machineid.service` (§7.6) — + bind волатилен (overlay tmpfs / RO-rootfs) и не переживает ребут, а firstboot после маркера скипается + (a-base §11: «генерим один раз, биндим каждый boot»); +3. посеять дефолт-настройки (`ui.theme=auto`, `ui.units=metric`) — формат совместим с `Settings`-стором; +4. (плейсхолдер) активировать BSP/vehicle-профиль (в VM — no-op); +5. **последним** шагом — durable-write маркера `/data/.shturman-provisioned` (частичный сбой mid-run → повтор + на next boot довосстанавливает, т.к. маркер пишется атомарно в самом конце). +Идемпотентность, factory-reset (маркер удалён + `/data` очищен → пересоздание), прерывание mid-run — тесты §9.1. + +### 7.3 Время (A07) + +- `systemd-timesyncd` (NTP) + **`fake-hwclock`** с **override пути на `/data`** (`FILE=/data/state/fake-hwclock.data` + через `/etc/default/fake-hwclock` или drop-in) — стоковый путь `/etc/fake-hwclock.data` бьёт по RO-rootfs и + нарушает «персист только в `/data`» (A11). Персист last-known-time (период + on-shutdown) — OS-уровень в + provisioning. GPS-источник UTC — v1 (домен K). +- **Дисциплина монотонности (B §8):** все lifecycle-таймеры/`Uptime` — на `CLOCK_MONOTONIC` (helper в + `shturman-common`), не wall-clock. +- **B-owned save-on-shutdown** (периодика + on-sync + on-shutdown, B §8) — интегрируется в **v0.3** + (graceful shutdown). В v0.6 — базовый OS-fake-hwclock. + +### 7.4 Память (A09) + +- **zram** (lz4/zstd) через **zram-generator** — `/etc/systemd/zram-generator.conf` (секция `[zram0]`); + **только один механизм** (НЕ `zram-tools` — конфликт за `/dev/zram0`). **Swap-на-flash запрещён** (a-base §8). +- **`systemd-oomd`** — `systemd/oomd-shturman.conf`: защищаем Stage-1 critical set (в v0: `power`/`settings`/ + `shell`); первые жертвы — фон (в v0 их нет — задел иерархии, performance §5/§3). +- **cgroup-лимиты** (`MemoryMax`/`MemoryHigh`) — drop-ins юнитов (числа 🟡, профиль на железе). + +### 7.5 Логи (A10) и eMMC write-min (A11) + +- **journald `Storage=volatile`** (журнал в RAM/tmpfs, не на flash) + `RuntimeMaxUse` + `RateLimitIntervalSec`/ + `RateLimitBurst` — `systemd/journald-shturman.conf`. Rust-`tracing` → journald (`tracing-journald`), та же политика. +- **Критичные события переживают power-cut:** size-capped append с `fsync` в `/data/log/` (kernel panic, + итог ACC-off, причина recovery) + `pstore/ramoops` — каркас в `shturman-common` (тело крит-логов — + по мере событий). +- **eMMC write-min (A11):** дисциплина (volatile-логи, tmpfs-транзиент, zram, без спама в `/data`). + **Измеримая проверка (детерминированный VM-прокси):** дельта записанных секторов на loop-устройстве `/data` + (`/proc/diskstats`) за фиксированное окно простоя (напр. 60 c после boot-settle) **ниже порога T** (🟡 + калибруется) + **нет периодических флашей вне allow-list писателей** (`fake-hwclock`, `Settings` on-Set); + всё прочее → fail. Абсолютный байт-бюджет — вердикт на RK3588 (performance §2). + +### 7.6 systemd-оркестрация (A15) + +- **`data.mount`** — постоянный mount loop-`/data` с power-safe-опциями; зависимые юниты несут `RequiresMountsFor=/data`. +- **`shturman-machineid.service`** — every-boot oneshot: bind `/data/state/machine-id` → `/etc/machine-id` + (`After=data.mount`, `Before=dbus.service`/`systemd-machine-id-commit`) — стабильный machine-id до старта шины + (нужен системной шине, §5.1). +- **`shturman.target`** — v0 critical set (Stage-1 срез), порядок: + `data.mount` → `shturman-firstboot.service` → `shturman-machineid.service` → `dbus` → + `shturman-power` + `shturman-settings` → `shturman-shell`. +- **`Requires=` + `After=shturman-firstboot.service`** у power/settings/shell — при провале firstboot сервисы + **не стартуют** (не работают против полу-провиженного `/data`; `Wants=` лишь упорядочивает — недостаточно). +- Юниты: `Restart=on-failure` (изоляция #4); `RuntimeWatchdogSec` — задел (полный watchdog — v0.3, B §6). +- D-Bus policy `ru.shturman.conf` — `own` на имена сервисов; `ru.shturman.dev.*` — **dev-only drop-in** (§5.1). + +--- + +## 8. Dev-харнесс (v0.6) + +### 8.1 Lima-VM (`lima/shturman.yaml`) и как поднимаем + +- **Параметры:** `vmType: vz` (Apple Virtualization, нативно ARM64); `images:` Ubuntu ARM64 LTS; + ресурсы (зафиксировано): `cpus: 4`, `memory: 6GiB`, `disk: 20GiB` (под 16 ГБ хоста, dev-environment + «VM лёгкая»; правится локально); `mounts:` репозиторий **writable** (правим на хосте — собираем в VM). +- **provision (system):** установить пакеты (`systemd`, `dbus`, `pipewire` + WirePlumber [задел v1], + `weston` + `weston-screenshooter`, `can-utils`, `rustup`/toolchain, `python3`+venv, **`systemd-zram-generator`**, + `fake-hwclock`, кириллические шрифты); `modprobe vcan` + `ip link add vcan0`; + создать loopback-`/data` (ext4 + power-safe-опции) и завести **постоянный** `data.mount`/fstab + tmpfs-overlay; + override `fake-hwclock` пути на `/data`; разложить `systemd/`-юниты + journald/zram-generator/oomd drop-ins + + dbus policy (прод `ru.shturman.conf` + dev-only `ru.shturman.dev.conf`); включить `shturman.target`. + *(screenshot кадра в CI — через Slint software-renderer, без пакета grim; см. §6.)* +- **reference-«BSP» (A16):** в dev это **Lima-профиль** (дев-таргет). Реальный reference-BSP (DT overlay + + HAL + DBC) — на HW (a-base §13), вне VM. +- **Подъём:** `just vm-up` → `limactl start --name=shturman lima/shturman.yaml` (создание+provision); + `just vm-shell` → `limactl shell shturman`; `just vm-reset` (сброс «ничего не сломать», dev-environment). + +### 8.2 `justfile` — единые команды + +| Цель | Делает | +|------|--------| +| `vm-up` / `vm-down` / `vm-reset` / `vm-shell` | жизненный цикл Lima-VM | +| `build` | `cargo build --workspace` (в VM/нативно на Linux/CI) | +| `test` | `cargo test --workspace` (unit + integration, §9) | +| `lint` | `cargo fmt --check` + `cargo clippy --workspace -- -D warnings` | +| `deny` | `cargo deny check` (лицензии #12 + advisories) | +| `up` / `down` | старт/стоп сервисов Штурмана на шине (dev) | +| `run` | поднять `shturman.target` локально (Power+Settings+Shell) для ручной проверки | +| `shell-frame` | запустить Shell один раз, отрендерить кадр (software-renderer) → PNG для инспекции | +| `e2e` | сквозной VM-тест приёмки (§9.3): boot → имена на шине → fake-ACC → кадр | +| `new-plugin ` | scaffolding плагина из `templates/plugin/` (F02) | +| `validate-manifest ` | запустить `shturman-manifest-validator` (F02) | +| `plugin-dev-run ` | **каркас** (F03): сообщает «нужен App-Host (v3)» | +| `sim` | **плейсхолдер** Vehicle Simulator (v2, домен E) | +| `ci` | локальный прогон гейта: `lint` + `test` + `deny` | + +### 8.3 CI (GitHub Actions, ARM64-Linux) + +- `.github/workflows/ci.yml`: jobs **lint** (fmt+clippy), **build**, **test** (unit+integration — + раннер **уже Linux**, шину/сервисы/headless-рендер гоним напрямую, **без Lima**), **license** + (`cargo-deny`), **prod-build-gate** (`cargo build --workspace --no-default-features` + ассерт, что + `PowerMock1` не экспортируется без фичи `dev-mocks` — §5.2). Позже — **integration** (vcan+симулятор, v2) + и **build образа** (v4). +- *Фолбэк (dev-environment):* если ARM64-раннеры вне бесплатного тира — x86 + кросс / self-hosted. +- **seed perf-gate (performance §9 ◻️):** замер **time-to-first-frame** в VM/раннере — **функционально** + (smoke), с явной пометкой «**не перф-вердикт**» (вердикт — RK3588, performance §2). Состав авто-гейта + (boot/кадр/ввод/голос) расширяем по фазам. + +### 8.4 SDK (F01) + dev-tools (F02) + харнесс (F04) + +- **`shturman-sdk` (F01):** `connect()`; `SettingsClient`/`PowerClient` (обёртки proxy из `ipc`); + `manifest::Manifest` (serde-схема YAML, plugin-sdk §2) — **single source** схемы для валидатора и + будущего рантайма. semver. +- **`shturman-manifest-validator` (F02):** schema-валидация + правила (capabilities из таксономии; + `len(tiles) ≤ ui_tiles`; `vehicle_read` только из каталога data-model; `ru` обязателен; манифестный + `id` `ru.shturman.*` **закрыт** для сторонних — F §3). Коды ошибок — человекочитаемые. +- **scaffolding (F02):** `templates/plugin/` (валидный `manifest.yaml` + минимальный бинарь-каркас на + `shturman-sdk` + `locales/ru.yaml`); `just new-plugin foo` генерит плагин, **проходящий** валидатор. +- **F04 (частично):** `fixtures/manifests/` — библиотека **плохих** манифестов (коллизия id, + неподдерж. `shturman_api`, `tiles>ui_tiles`, capability вне таксономии, `vehicle_read` вне каталога, + попытка `ru.shturman.*`) + один валидный → фикстуры тестов валидатора. Рантайм-fault-injection + (crash-loop/mem-hog) — **позже** (нужен App-Host). + +--- + +## 9. План тестирования и приёмка + +Пирамида (dev-environment): **unit → integration → E2E → HW-in-the-loop**. HW (RK3588) — **отложено** +(нет железа); перф-**вердикт** там (performance §2). + +### 9.1 Unit (хост/VM/CI, по крейтам) + +- `shturman-ipc`: сериализация enum↔строка (`PowerState`/`IgnitionState`/`PowerSource`/`ShutdownReason`); + маппинг `Error` ↔ D-Bus-имя. +- `shturman-common`: **тест power-safe #5** — атомарная запись (temp→fsync→rename→fsync(dir)) + **симуляция + сбоя между `rename` и `fsync(dir)` / прерывания**: читается прошлая ИЛИ новая версия, нет частично + записанного/битого файла (это и есть доказательство #5, не reboot); монотонные часы. +- `shturman-settings`: load/seed/round-trip Set→Get; `Reset` к дефолту; неизвестный ключ → `InvalidArgument`; + персист переживает перезапуск процесса (тест на tmp-`/data`). +- `shturman-firstboot`: **идемпотентность** — (а) маркер на месте → no-op; (б) **factory-reset**: маркер + удалён + `/data` очищен → пересоздаёт `/data/{apps,settings,state,log}`, повторно сеет дефолты, ставит маркер, + machine-id перегенерён; (в) **прерывание mid-run** (kill до маркера) → повторный прогон довосстанавливает. +- `shturman-sdk`: парс манифеста (валидный/битый). +- `shturman-manifest-validator`: каждый bad-фикстур → ожидаемый код; good → pass. +- `shturman-shell`: `slint::testing` — дерево элементов строится, тема байндится из мок-Settings. +- Команда: `just test` (или `cargo test -p `). + +### 9.2 Integration (сервисы на **приватной** шине) + +- Харнесс поднимает `dbus-run-session` → стартует `shturman-settings` + `shturman-power` → тест-клиент + **через `shturman-sdk`**: + - `Settings.Set("ui.theme","night")` → `Get` = `night`; приходит `Changed`; + - `Power.GetPowerState()` = `running`; `IgnitionState` читается; + - `dev-mocks`: `PowerMock1.SetAcc(false)` → подписчик получил `AccChanged(false)`; + - **снятие клиент-состояния:** подписчик исчезает → серверная подписка снимается по `NameOwnerChanged` (ipc §2). +- Команда: `just test` (integration-тесты в `crates/*/tests/`, фича `dev-mocks`). + +### 9.3 E2E (полный поток в Lima-VM) — `just e2e` + +В VM: `shturman.target` поднят → раннер `tests/e2e/`: +1. **boot/init + оркестрация:** `findmnt /data` смонтирован **до сервисов** и показывает реально отображаемые + non-default опции (напр. `errors=remount-ro`); overlay-tmpfs присутствует; **per-unit** `systemctl is-active` + = active для `shturman-power`/`-settings`/`-shell` и inactive(dead, Result=success) для firstboot/machineid — + **не** довольствуемся `is-system-running ∈ {running,degraded}` (degraded маскирует упавший юнит). +2. **first-boot (A06):** маркер есть; повторный запуск `shturman-firstboot` — no-op. +3. **шина:** `busctl --system list` содержит `ru.shturman.Power` и `ru.shturman.Settings` (own). +4. **персист через перезапуск (функц., НЕ power-safe):** `Settings.Set` → **reboot VM** → значение сохранилось; + **`machine-id` стабилен после reboot** (every-boot bind работает). *(Настоящий #5 — unit-тест атомарности §9.1; + реальный power-cut — v0.3.)* +5. **fake-ACC:** `PowerMock1.SetAcc(...)` → `AccChanged` наблюдаем. +6. **первый кадр:** Shell → **software-renderer → PNG не пустой** + (`slint::testing`) root+тайлы+часы; + тема соответствует `ui.theme`. +7. **base-бюджеты (функц.):** journald `Storage=volatile` активен; `zramctl` показывает **одно** zram-устройство; + `fake-hwclock`-данные лежат в `/data` (не в `/etc`); eMMC-прокси (§7.5) — дельта секторов loop-`/data` ниже + порога за окно простоя, вне allow-list писателей записей нет. +8. **seed perf:** замер time-to-first-frame (логируется, не блокирует — не вердикт). + +### 9.4 Критерии приёмки (acceptance) + +**v0.1** (roadmap: «init поднялся; RO-rootfs A/B + overlay + `/data` ок»): +- [ ] VM грузится; per-unit critical set active; `/data` смонтирован **до сервисов** с реальными power-safe-опциями; overlay(tmpfs) есть. +- [ ] First-boot создал структуру `/data` **идемпотентно**; `machine-id` стабилен **после reboot** (every-boot bind). +- [ ] RO-rootfs-дисциплина соблюдена (запись только в `/data`/tmpfs). *(A/B boot-select — HW/v4.)* + +**v0.6** (roadmap: «dev-итерация без железа работает; память/лог/eMMC в бюджете»): +- [ ] `just vm-up && just build && just test && just lint && just deny` — зелено **без железа**. +- [ ] journald `Storage=volatile`; одно zram-устройство; oomd-политика загружена; eMMC-прокси в пороге (§7.5). +- [ ] `shturman-sdk` собирается; `just validate-manifest` корректно принимает good / отвергает bad; + `just new-plugin foo` генерит плагин, **проходящий** валидатор. +- [ ] Governance: `LICENSE`, `CONTRIBUTING.md`, `DCO`, `README.md` (мини-спека §11) присутствуют; prod-build-gate (§8.3) зелёный. + +**Шагающий скелет** (CLAUDE.md): +- [ ] `ru.shturman.Power` + `ru.shturman.Settings` владеют именами на шине. +- [ ] Settings round-trip + персист через reboot; fake-ACC `AccChanged` наблюдаем. +- [ ] **Первый Slint-кадр** рендерится (PNG не пустой), отражает `ui.theme` из Settings и состояние `Power`. + +--- + +## 10. LICENSE (MIT) — содержимое файла `LICENSE` + +``` +MIT License + +Copyright (c) 2026 K9 Shturman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +> **Примечание (лицензионный слой UI):** наш код — MIT. Но `slint` для embedded доступен бесплатно только под +> **GPL-3.0**, поэтому **шипимый UI-бинарь** (Shell и будущие first-party-апы, линкующие Slint) при распространении +> прод-образа (v4) будет под GPL-3.0. Это **отложенное решение (вариант A, §12 п.8)**; до v4 dev-использование +> обязательств не создаёт. MIT GPL-совместима (односторонне). Если копилеф­т неудобен — миграция на Makepad/Iced (MIT) +> или commercial-лицензия Slint. + +--- + +## 11. `CONTRIBUTING.md` — предлагаемое содержимое + +Закрывает **governance-пробел**. Структура: + +1. **Добро пожаловать + философия:** open-source companion-ОС для авто (RK3588), MIT; язык общения — + русский, код-идентификаторы — как есть. +2. **Красные линии (нерушимы, principles #1–#2):** PR, добавляющий CAN-write/actuator/Mode-04/UDS-write + или safety-critical-интеграцию (двигатель/тормоза/ABS/ESP/руль/подушки), **отклоняется** — + допустимы только OBD-read (Mode 01/03/07/09/0A). Граница — `docs/contracts/safety.md`. +3. **Источник правды — `docs/`:** не противоречить дизайн-докам; при расхождении реальности и дока — + **синхронизировать док** (двунаправленный шов), не «молча обойти». +4. **Лицензионная гигиена (#12):** зависимости — MIT/Apache-2.0/BSD/ISC-совместимые; GPL/AGPL — + избегать или изолировать отдельным процессом; **LGPL — гранулярно** (динамическая/системная линковка + допустима, не blanket-запрет); **`cargo deny check` — обязателен** (CI-гейт). Исключения (напр. `slint` + GPL-3.0, §3) — явные и задокументированные. +5. **Рабочий цикл (фаза реализации):** roadmap ведёт; цикл на веху — **спека → TDD → реализация → + verify в Lima-VM → коммит**; **код не пишем до утверждённой спеки**. Скиллы: TDD, + writing/executing-plans, verification-before-completion, systematic-debugging, requesting-code-review. +6. **Стиль кода:** `rustfmt` + `clippy -D warnings` (через `just lint`); Rust везде в проде + (Python — только dev/симуляторы, tech-stack). +7. **Тесты:** пирамида unit/integration/E2E; «тестируемо без машины» (#13) — каждая фича со своим + симулятором/моком; `just test` зелёный до PR. +8. **Коммиты/ветки:** `feat/fix/chore(): …`; ветка от `main` (в `main` без явного «ок» не коммитим); + в конце коммита — `Co-Authored-By: Claude Opus 4.8 `. +9. **PR-чеклист:** прошёл `just ci`; не нарушает красные линии; доки синхронизированы; добавлены тесты. +10. **DCO sign-off (обязателен):** `git commit -s` (Developer Certificate of Origin) — без CLA. + Файл `DCO` (текст DCO 1.1) — в корне. +11. **Поведение/споры:** Code of Conduct (указатель; добавить `CODE_OF_CONDUCT.md` — рек. Contributor Covenant). +12. **Где обсуждать:** GitHub Issues/Discussions; 🟡-решения — в roadmap «Риск-реестр». +13. **`README.md` (мини-спека, корень репо):** название + абзац-описание; красные линии (CAN read-only / не + safety-critical); ссылки на `CLAUDE.md` и `docs/`; быстрый старт (`just vm-up` / `just run`); лицензия + MIT + DCO + примечание о GPL-слое UI (§10). Галочка приёмки — §9.4. + +--- + +## 12. Решения (зафиксированы) и отложенное (не блокеры) + +**✅ Зафиксировано (ревью 2026-06-23):** +1. **Контрибьюция — DCO** (`git commit -s`, Developer Certificate of Origin); CLA не вводим. Файл `DCO` (1.1) в корень. +2. **Правообладатель в LICENSE — `K9 Shturman`** (§10). +3. **Топология шины в dev — системная шина устройства + policy** (`systemd/dbus/ru.shturman.conf`), + зеркало прода (совпадает с рек. ipc §1); decision-журнал ipc.md синхронизируем при реализации (§13). +4. **ФС `/data` в dev-VM — ext4** (барьеры — дефолт ядра, `barrier=1` не задаём); **f2fs (`fsync_mode=strict`) — на HW** + (a-base §3; прод-ФС финализируется на прод-образе, 🟡 A02). +5. **Ресурсы Lima-VM — 4 CPU / 6 GiB / 20 GiB** (под 16 ГБ-хост; правится локально). +6. **Место спек — `docs/specs/`.** +7. **Глубина «первого кадра» — минимум:** статус-бар (часы + сеть «unknown») + грид-плейсхолдеры + + тема из Settings; полный home/тема/анимации — v0.5. +8. **UI-тулкит / лицензия Slint — вариант A: отложено к v4.** На v0 берём Slint; dev в VM не триггерит + копилеф­т-обязательств распространения. Royalty-free неприменим (embedded исключён) → бесплатный путь = + GPL-3.0; `deny.toml` — exception `slint = GPL-3.0, pending v4`. Makepad (primary) / Iced (fallback) — пермиссивные + запасные на случай HW-спайка. Финал (GPL-карв-аут #12 vs миграция vs commercial) — до прод-образа v4. Подробно: §3, §10-примечание. + +**Отложенное (CLAUDE.md «всплывут по ходу — НЕ блокеры старта»):** +- `A01` Armbian/Debian vs Yocto — прод-образ v4 (dev = Lima Ubuntu). +- `A02` f2fs vs ext4 — финал на прод-образе (dev = ext4, п.4). +- `B08/B09` MCU vs supercap-only — v0.4, вероятно нужна аппаратная проверка. +- **UI-тулкит/лицензия** (Slint GPL vs пермиссивный Makepad/Iced vs commercial) — финал к v4; HW-спайк запасных при случае. + +--- + +## 13. Двунаправленные швы (синхронизировать при реализации) + +По правилу проекта (реальность ↔ док). Сейчас не правим (ещё спека) — фиксируем точечные правки на момент кода: + +- **`ipc.md`** decision-журнал: топология шины 🟡 → ✅ «системная шина устройства» (§5.1). +- **`a-base §3`**: `barrier=1` — дефолт ядра (legacy-алиас, не отображается); привести формулировку к «барьеры по + умолчанию; durable-write — главная гарантия power-safe» (§7.1). +- **`architecture §6`**: ремарка «Stage-1-минимум для v0-скелета: Perm-Broker/App-Host включаются с первым + песочным клиентом (плагин/surface-ап), Shell в v0 — обычный сервис на шине» (§2.2). +- **`tech-stack.md`**: лицензия Slint — `GPL-3.0` для embedded (royalty-free неприменим); решение по тулкиту + отложено к v4 (§12 п.8); Makepad/Iced — пермиссивные запасные. +- **`principles #12`**: уточнить LGPL — гранулярно (динамическая/системная линковка допустима), а не blanket; + согласовать с `deny.toml`/§3. + +--- + +## 14. Дальше по ритму + +1. ✅ Спека → правки → **adversarial-ревью пройдено** (17 находок применены в этой v1) → ждём твоё финальное «ок». +2. По «ок» — **TDD-план** (бите-сайз по writing-plans): порядок крейтов + (`common` → `ipc` → `sdk` → `firstboot`/`settings`/`power` → `shell` → `tools`) + dev-харнесс + governance-файлы. +3. **Реализация (TDD)** → **verify в Lima-VM** (§9) → синхронизация швов §13 → **коммит** + (ветка от `main`, после твоего «ок»). +``` +