Files
shturman/docs/specs/plans/01-workspace-and-common.md
kk0t9 d443fb479b 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>
2026-06-24 11:47:49 +03:00

748 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План 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`).