docs(specs): План 2 — shturman-ipc (контракт) + shturman-sdk (клиент)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
2026-06-24 12:05:30 +03:00
parent 54f17cbf4e
commit df553790ae
+132
View File
@@ -0,0 +1,132 @@
# План 2 — `shturman-ipc` (контракт шины) + `shturman-sdk` (клиент)
> REQUIRED SUB-SKILL: `superpowers:executing-plans` + TDD. Полный код пишется в `src` по ходу (inline-исполнение);
> здесь — дизайн, задачи, ключевые сигнатуры, тест-кейсы, acceptance.
**Goal:** зафиксировать контракт D-Bus в коде (`shturman-ipc`: ошибки, имена/пути/версии, enum-типы, `#[proxy]`
Power1/Settings1) и публичный клиентский слой (`shturman-sdk`: `connect`, `SettingsClient`/`PowerClient`,
схема манифеста). Граница: ядро-сервисы будут зависеть от `ipc`; апы/плагины — от `sdk` (architecture §3, #1).
**Architecture:** `ipc``common`; `sdk``ipc` + `common`. Enum-типы сериализуются на провод как **строки**
(spec §5.2): D-Bus-сигнатуры используют `String`/`Variant`, Rust-сторона конвертит через `as_str`/`FromStr`.
Bus-зависимые обёртки (clients) компилируются здесь, **проверяются на живой шине в Плане 3**.
**Tech Stack:** `zbus` 4 (`#[proxy]`, `zbus::DBusError`, `zvariant`), `serde`/`serde_yaml` (манифест), `thiserror`.
**Контекст:** [спека §4–§5](../v0.1-v0.6-foundation.md), [ipc.md](../../contracts/ipc.md), [data-model §2](../../contracts/data-model.md),
[plugin-sdk §2](../../contracts/plugin-sdk.md). Ветка `feat/v0-foundation`, коммиты `-s` + Co-Authored-By.
---
### Task 1: `shturman-ipc` — крейт + `Error` + `names`
**Files:** `crates/shturman-ipc/Cargo.toml`, `src/lib.rs`, `src/error.rs`, `src/names.rs`; `Cargo.toml` (members).
- Добавить `crates/shturman-ipc` в `members`. Deps: `zbus`, `serde`, `thiserror`, `shturman-common`.
- `error.rs``ru.shturman.Error.*` (ipc §2):
```rust
#[derive(Debug, thiserror::Error, zbus::DBusError)]
#[zbus(prefix = "ru.shturman.Error")]
pub enum Error {
#[zbus(error)]
ZBus(zbus::Error), // catch-all для транспортных ошибок
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("not available: {0}")]
NotAvailable(String),
#[error("stale: {0}")]
Stale(String),
#[error("timeout: {0}")]
Timeout(String),
#[error("read only: {0}")]
ReadOnly(String),
#[error("invalid argument: {0}")]
InvalidArgument(String),
#[error("unsupported: {0}")]
Unsupported(String),
}
```
- `names.rs` — well-known имена/пути (ipc §2):
```rust
pub mod power {
pub const NAME: &str = "ru.shturman.Power";
pub const PATH: &str = "/ru/shturman/Power";
pub const IFACE: &str = "ru.shturman.Power1";
}
pub mod settings {
pub const NAME: &str = "ru.shturman.Settings";
pub const PATH: &str = "/ru/shturman/Settings";
pub const IFACE: &str = "ru.shturman.Settings1";
}
```
- **TDD-тест (error):** `Error::InvalidArgument("x").to_string()` содержит `"invalid argument"`; компиляция
`zbus::DBusError` подтверждает маппинг имён. **Тест (names):** консты соответствуют конвенции
`ru.shturman.*` / `/ru/shturman/*` (sanity).
- Build + `cargo test -p shturman-ipc` зелёный → commit `feat(ipc): Error + well-known имена`.
### Task 2: `ipc::types` — enum-типы (TDD round-trip строк)
**Files:** `crates/shturman-ipc/src/types.rs`.
- Enums (data-model/spec §5.2): `PowerState {off, accessory, running, shutting_down, sleep, battery_cutoff}`,
`IgnitionState {off, accessory, running}`, `PowerSource {vehicle_12v, holdup_cap, sleep_rail, low_battery}`,
`ShutdownReason {acc_off, under_voltage, thermal, battery_cutoff}`. Каждому — `as_str(&self) -> &'static str`
и `FromStr` (snake_case на проводе).
- **TDD-тесты:** для каждого enum — `parse(as_str(v)) == v` по всем вариантам; неизвестная строка → `Err`.
- Падающий тест → реализация → зелёно → commit `feat(ipc): enum-типы Power/Ignition/Source/Reason (string round-trip)`.
### Task 3: `ipc::proxy` — `Power1`/`Settings1` (`#[proxy]`)
**Files:** `crates/shturman-ipc/src/proxy.rs`, обновить `lib.rs` (re-export).
- `#[proxy(interface="ru.shturman.Power1", default_service="ru.shturman.Power", default_path="/ru/shturman/Power")]`
трейт `Power1`: метод `get_power_state() -> zbus::Result<String>`, `request_sleep()`; `#[zbus(property)]`
`ignition_state()->String`, `uptime()->u64`, `power_source()->String`; `#[zbus(signal)]` `acc_changed(on: bool)`,
`shutdown_imminent(seconds: u32, reason: String)`, `shutdown_aborted()`, `sleep()`, `wake()`.
- `Settings1` трейт: `get(key)->OwnedValue`, `set(key, value: &Value)`, `list(prefix)->Vec<String>`,
`reset(key)`; `#[zbus(signal)]` `changed(key: String, value: OwnedValue)`.
- Проверка — **компиляция** (zbus-макро генерит клиент). Build зелёный → commit `feat(ipc): zbus-proxy Power1/Settings1`.
### Task 4: `shturman-sdk` — крейт + `manifest` (TDD)
**Files:** `crates/shturman-sdk/Cargo.toml`, `src/lib.rs`, `src/manifest.rs`; `Cargo.toml` (members).
- Добавить `crates/shturman-sdk` в `members`. Deps: `zbus`, `serde`, `serde_yaml`, `tokio`, `shturman-ipc`, `shturman-common`.
- `manifest.rs` — serde-схема (plugin-sdk §2): `Manifest{ plugin: Plugin, capabilities: Capabilities,
extension_points: Option<ExtensionPoints> }`; `Plugin{id,name,version,description,author,shturman_api}`;
`Capabilities{ vehicle_read: Vec<String>, assistant_intents: …, ui_tiles: u32, ui_screens: u32,
storage: bool, network: Option<…> }` (поля `#[serde(default)]` где опц.).
- **TDD-тесты:** распарсить валидный YAML (пример `fuel-tracker`, plugin-sdk §8) → поля верны; битый YAML → `Err`;
отсутствие обязательного `plugin.id` → `Err`.
- Падающий тест → реализация → зелёно → commit `feat(sdk): схема манифеста (plugin-sdk §2)`.
### Task 5: `sdk` — `connect` + `PowerClient` + `SettingsClient` (build; bus — План 3)
**Files:** `crates/shturman-sdk/src/{connect.rs,power.rs,settings.rs}`, `lib.rs` (re-export).
- `connect()` — `zbus::Connection` к **системной шине** (`Connection::system()`); env `SHTURMAN_BUS=session`
для тестов/dev → `Connection::session()`. (Вызов на живой шине — План 3.)
- `PowerClient{ proxy: Power1Proxy }`: `power_state()->Result<PowerState>` (парс строки в enum через `ipc::types`),
`ignition_state()->Result<IgnitionState>`, `uptime()->Result<u64>`; подписки на сигналы (stream).
- `SettingsClient{ proxy: Settings1Proxy }`: `get_string(key)`, `set(key, value)`, `list(prefix)`, `reset(key)`,
поток `changed`.
- Проверка — **компиляция** + `cargo test --workspace` (unit, без шины) + `just ci`. Re-exports в `lib.rs`.
- commit `feat(sdk): connect + Power/Settings клиенты (bus-тест — План 3)`.
---
## Acceptance (Плана 2)
- [ ] `cargo build --workspace` зелёный (ipc + sdk компилируются, proxy-макро ок).
- [ ] `cargo test --workspace` — round-trip enum (ipc) + парс манифеста (sdk) зелёные; common-тесты не сломаны.
- [ ] `just ci` зелёный (fmt + clippy -D + test + deny). Новые транзитивные лицензии (zbus и пр.) — в allow или добавить.
- [ ] Границы: `ipc` не зависит от `sdk`; сервисы (План 3) будут импортировать `ipc`; `sdk` — для апов.
## Дальше
План 3 — стаб-сервисы `firstboot`/`settings`/`power` + интеграция на **живой шине** (здесь всплывёт вопрос
D-Bus на macOS-хосте: `brew install dbus` + session-bus, либо перенести Lima-настройку раньше).