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:
@@ -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`).
|
||||
@@ -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.<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`, после твоего «ок»).
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user