d443fb479b
Спека после adversarial-ревью (17 находок) + позиция Slint (вариант A, финал отложен к v4). План 1 — воркспейс + governance + shturman-common (TDD). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
748 lines
26 KiB
Markdown
748 lines
26 KiB
Markdown
# План 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`).
|