docs(specs): спека реализации v0.1/v0.6 (v1) + План 1

Спека после adversarial-ревью (17 находок) + позиция Slint (вариант A, финал отложен к v4).
План 1 — воркспейс + governance + shturman-common (TDD).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 11:47:49 +03:00
parent 25703751dc
commit d443fb479b
2 changed files with 1419 additions and 0 deletions
+747
View File
@@ -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 <noreply@anthropic.com>`.
---
### 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 <noreply@anthropic.com>"
```
---
### 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<PathBuf>) -> 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 <noreply@anthropic.com>"
```
---
### 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 <noreply@anthropic.com>"
```
---
### 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<Instant> = 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 <noreply@anthropic.com>"
```
---
### 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 <noreply@anthropic.com>"
```
---
### 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 <noreply@anthropic.com>"
```
---
### 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 <noreply@anthropic.com>"
```
---
### 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 <noreply@anthropic.com>"
```
---
## 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`).
+672
View File
@@ -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 — отложены туда (v2v4); 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.<Service>`, путь `/ru/shturman/<Service>`, интерфейс
`ru.shturman.<Service>1` (N = мажор). Всё async. Ошибки — `ru.shturman.Error.<Name>`.
### 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 <name>` | scaffolding плагина из `templates/plugin/` (F02) |
| `validate-manifest <path>` | запустить `shturman-manifest-validator` (F02) |
| `plugin-dev-run <path>` | **каркас** (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 <crate>`).
### 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(<area>): …`; ветка от `main``main` без явного «ок» не коммитим);
в конце коммита — `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`.
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`, после твоего «ок»).
```