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`).
|
||||
Reference in New Issue
Block a user