refactor: restructure docs as submodule, add dev-docs/ and AGENTS.md
- Move ASPICE docs to chanoraapp/docs submodule at docs/ - Move development docs to dev-docs/ (superpowers, offline-knowledge, impl-mapping) - Add AGENTS.md with project conventions for AI agents - Add impl-mapping.md (SAD component → source file mapping) - Archive completed plans to dev-docs/superpowers/plans/_archived/ - Remove AGENTS.md from .gitignore (now tracked)
This commit is contained in:
@@ -0,0 +1,689 @@
|
||||
# Chanora Server Prefetch Crate Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Move server-resolution prefetch policy from `chanora_core` into a focused crate named `chanora_prefetch` without changing Flutter APIs, resolver behavior, protocol dialing behavior, or Android connect UX.
|
||||
|
||||
**Architecture:** Add `crates/chanora_prefetch` as a workspace member. The new crate owns normalization, single-entry TTL cache, generation rejection, resolver-backed async warming, and fresh exact-match lookup. `chanora_core` keeps the trust boundary for `ConnectConfig.resolved_address`, using the prefetcher only to populate one-time dial config while keeping stored/reconnect config sanitized.
|
||||
|
||||
**Tech Stack:** Rust workspace, `tokio`, `tracing`, `thiserror`, `chanora_resolver`, `chanora_core`, `chanora_protocol`, Flutter Android smoke via ADB.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create `crates/chanora_prefetch/Cargo.toml`: package metadata and dependencies.
|
||||
- Create `crates/chanora_prefetch/src/lib.rs`: public `ServerPrefetcher`, public `ServerPrefetchError`, internal cache entry/state, resolver-backed prefetch logic, and unit tests.
|
||||
- Modify `Cargo.toml`: add `crates/chanora_prefetch` to workspace members and update the workspace layout comment.
|
||||
- Modify `core/chanora_core/Cargo.toml`: replace the direct `chanora_resolver` dependency with `chanora_prefetch`.
|
||||
- Modify `core/chanora_core/src/lib.rs`: remove private prefetch cache/resolver helpers, add `ServerPrefetcher`, delegate `prefetch_server_resolution`, and keep connect config sanitization tests.
|
||||
- Modify `Cargo.lock`: generated by `cargo test`/`cargo check` after adding the crate.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add `chanora_prefetch` Crate With Cache Policy Tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `Cargo.toml`
|
||||
- Create: `crates/chanora_prefetch/Cargo.toml`
|
||||
- Create: `crates/chanora_prefetch/src/lib.rs`
|
||||
|
||||
- [ ] **Step 1: Add the workspace member and package files**
|
||||
|
||||
In top-level `Cargo.toml`, update the layout comment and members:
|
||||
|
||||
```toml
|
||||
# crates/chanora_prefetch — server-resolution prefetch cache/policy
|
||||
# crates/chanora_bridge/ — Flutter/Rust typed DTOs + glue
|
||||
```
|
||||
|
||||
```toml
|
||||
members = [
|
||||
"core/chanora_core",
|
||||
"crates/chanora_protocol",
|
||||
"crates/chanora_state",
|
||||
"crates/chanora_audio",
|
||||
"crates/chanora_storage",
|
||||
"crates/chanora_diagnostics",
|
||||
"crates/chanora_prefetch",
|
||||
"crates/chanora_bridge",
|
||||
"crates/chanora_resolver",
|
||||
]
|
||||
```
|
||||
|
||||
Create `crates/chanora_prefetch/Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "chanora_prefetch"
|
||||
description = "Chanora — server-address prefetch cache and policy built on chanora_resolver."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
chanora_resolver = { path = "../chanora_resolver" }
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
tokio = { version = "1", features = ["sync", "rt", "macros"] }
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
```
|
||||
|
||||
Create `crates/chanora_prefetch/src/lib.rs` with the initial crate implementation and tests:
|
||||
|
||||
```rust
|
||||
//! Server-address prefetch cache and policy for Chanora.
|
||||
//!
|
||||
//! This crate owns speculative server-resolution warming. It does not
|
||||
//! decide whether a connection should use a prefetched address; callers
|
||||
//! must still apply their own trust boundary before dialing.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use thiserror::Error;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{info, warn};
|
||||
|
||||
const SERVER_PREFETCH_TTL: Duration = Duration::from_secs(120);
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ServerPrefetchError {
|
||||
#[error("resolver initialization failed: {0}")]
|
||||
ResolverInit(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ServerPrefetchEntry {
|
||||
normalized_host: String,
|
||||
resolved_address: SocketAddr,
|
||||
completed_at: Instant,
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ServerPrefetchCache {
|
||||
latest_generation: u64,
|
||||
entry: Option<ServerPrefetchEntry>,
|
||||
last_failure: Option<String>,
|
||||
}
|
||||
|
||||
impl ServerPrefetchCache {
|
||||
fn begin(&mut self, host: &str) -> u64 {
|
||||
if normalize_host(host).is_empty() {
|
||||
return self.latest_generation;
|
||||
}
|
||||
self.latest_generation = self.latest_generation.saturating_add(1);
|
||||
self.last_failure = None;
|
||||
self.latest_generation
|
||||
}
|
||||
|
||||
fn store_success(
|
||||
&mut self,
|
||||
generation: u64,
|
||||
host: &str,
|
||||
resolved_address: SocketAddr,
|
||||
completed_at: Instant,
|
||||
) {
|
||||
if generation != self.latest_generation {
|
||||
return;
|
||||
}
|
||||
self.entry = Some(ServerPrefetchEntry {
|
||||
normalized_host: normalize_host(host),
|
||||
resolved_address,
|
||||
completed_at,
|
||||
generation,
|
||||
});
|
||||
self.last_failure = None;
|
||||
}
|
||||
|
||||
fn store_failure(&mut self, generation: u64, error: String) {
|
||||
if generation != self.latest_generation {
|
||||
return;
|
||||
}
|
||||
self.last_failure = Some(error);
|
||||
}
|
||||
|
||||
fn fresh_match(&self, host: &str, now: Instant) -> Option<SocketAddr> {
|
||||
let normalized = normalize_host(host);
|
||||
let entry = self.entry.as_ref()?;
|
||||
if entry.normalized_host != normalized {
|
||||
return None;
|
||||
}
|
||||
if entry.generation != self.latest_generation {
|
||||
return None;
|
||||
}
|
||||
if now.duration_since(entry.completed_at) > SERVER_PREFETCH_TTL {
|
||||
return None;
|
||||
}
|
||||
Some(entry.resolved_address)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ServerPrefetcher {
|
||||
cache: Arc<Mutex<ServerPrefetchCache>>,
|
||||
}
|
||||
|
||||
impl ServerPrefetcher {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub async fn prefetch(&self, host: String) -> Result<(), ServerPrefetchError> {
|
||||
let normalized = normalize_host(&host);
|
||||
if normalized.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let generation = {
|
||||
let mut cache = self.cache.lock().await;
|
||||
cache.begin(&normalized)
|
||||
};
|
||||
let cache = self.cache.clone();
|
||||
tokio::spawn(async move {
|
||||
info!(target: "chanora_prefetch", host = %normalized, "resolution prefetch started");
|
||||
let result = resolve_socket(&normalized).await;
|
||||
let mut guard = cache.lock().await;
|
||||
match result {
|
||||
Ok(addr) => {
|
||||
info!(
|
||||
target: "chanora_prefetch",
|
||||
host = %normalized,
|
||||
resolved = %addr,
|
||||
"resolution prefetch result"
|
||||
);
|
||||
guard.store_success(generation, &normalized, addr, Instant::now());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
target: "chanora_prefetch",
|
||||
host = %normalized,
|
||||
error = %err,
|
||||
"resolution prefetch failed"
|
||||
);
|
||||
guard.store_failure(generation, err.to_string());
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fresh_match(&self, host: &str) -> Option<SocketAddr> {
|
||||
let resolved = {
|
||||
let cache = self.cache.lock().await;
|
||||
cache.fresh_match(host, Instant::now())
|
||||
};
|
||||
match resolved {
|
||||
Some(addr) => {
|
||||
info!(
|
||||
target: "chanora_prefetch",
|
||||
host = %host,
|
||||
resolved = %addr,
|
||||
"connect using prefetched resolution"
|
||||
);
|
||||
Some(addr)
|
||||
}
|
||||
None => {
|
||||
info!(target: "chanora_prefetch", host = %host, "connect prefetch miss or stale");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn begin_for_test(&self, host: &str) -> u64 {
|
||||
let mut cache = self.cache.lock().await;
|
||||
cache.begin(host)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn store_success_for_test(
|
||||
&self,
|
||||
generation: u64,
|
||||
host: &str,
|
||||
resolved_address: SocketAddr,
|
||||
completed_at: Instant,
|
||||
) {
|
||||
let mut cache = self.cache.lock().await;
|
||||
cache.store_success(generation, host, resolved_address, completed_at);
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn latest_generation_for_test(&self) -> u64 {
|
||||
let cache = self.cache.lock().await;
|
||||
cache.latest_generation
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_host(host: &str) -> String {
|
||||
host.trim().to_lowercase()
|
||||
}
|
||||
|
||||
async fn resolve_socket(host: &str) -> Result<SocketAddr, ServerPrefetchError> {
|
||||
let resolver = chanora_resolver::ChanoraResolver::new()
|
||||
.map_err(|err| ServerPrefetchError::ResolverInit(err.to_string()))?;
|
||||
let resolved = resolver
|
||||
.resolve_client_address(host)
|
||||
.await
|
||||
.map_err(|err| ServerPrefetchError::ResolverInit(err.to_string()))?;
|
||||
Ok(resolved.connection_addr())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn fresh_exact_match_returns_socket_address() {
|
||||
let prefetcher = ServerPrefetcher::new();
|
||||
let generation = prefetcher.begin_for_test(" Example.COM ").await;
|
||||
let addr = "127.0.0.1:9987".parse().unwrap();
|
||||
prefetcher
|
||||
.store_success_for_test(generation, "example.com", addr, Instant::now())
|
||||
.await;
|
||||
|
||||
assert_eq!(prefetcher.fresh_match("example.com").await, Some(addr));
|
||||
assert_eq!(prefetcher.fresh_match(" EXAMPLE.com ").await, Some(addr));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stale_entries_are_ignored() {
|
||||
let prefetcher = ServerPrefetcher::new();
|
||||
let generation = prefetcher.begin_for_test("example.com").await;
|
||||
let addr = "127.0.0.1:9987".parse().unwrap();
|
||||
prefetcher
|
||||
.store_success_for_test(
|
||||
generation,
|
||||
"example.com",
|
||||
addr,
|
||||
Instant::now() - SERVER_PREFETCH_TTL - Duration::from_secs(1),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(prefetcher.fresh_match("example.com").await, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn different_hosts_are_ignored() {
|
||||
let prefetcher = ServerPrefetcher::new();
|
||||
let generation = prefetcher.begin_for_test("example.com").await;
|
||||
let addr = "127.0.0.1:9987".parse().unwrap();
|
||||
prefetcher
|
||||
.store_success_for_test(generation, "example.com", addr, Instant::now())
|
||||
.await;
|
||||
|
||||
assert_eq!(prefetcher.fresh_match("other.example.com").await, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stale_generation_completions_are_ignored() {
|
||||
let prefetcher = ServerPrefetcher::new();
|
||||
let old_generation = prefetcher.begin_for_test("old.example.com").await;
|
||||
let _new_generation = prefetcher.begin_for_test("new.example.com").await;
|
||||
let old_addr = "127.0.0.1:9987".parse().unwrap();
|
||||
prefetcher
|
||||
.store_success_for_test(old_generation, "old.example.com", old_addr, Instant::now())
|
||||
.await;
|
||||
|
||||
assert_eq!(prefetcher.fresh_match("old.example.com").await, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn blank_hosts_do_not_update_generation() {
|
||||
let prefetcher = ServerPrefetcher::new();
|
||||
let before = prefetcher.latest_generation_for_test().await;
|
||||
|
||||
prefetcher.prefetch(" ".to_string()).await.unwrap();
|
||||
|
||||
assert_eq!(prefetcher.latest_generation_for_test().await, before);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run new crate tests**
|
||||
|
||||
Run: `cargo test -p chanora_prefetch --lib`
|
||||
|
||||
Expected: the new crate compiles and all 5 tests pass.
|
||||
|
||||
- [ ] **Step 3: Commit Task 1**
|
||||
|
||||
```bash
|
||||
git add Cargo.toml Cargo.lock crates/chanora_prefetch
|
||||
git commit -m "feat(prefetch): add server prefetch crate"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Wire Core to `ServerPrefetcher`
|
||||
|
||||
**Files:**
|
||||
- Modify: `core/chanora_core/Cargo.toml`
|
||||
- Modify: `core/chanora_core/src/lib.rs`
|
||||
|
||||
- [ ] **Step 1: Replace the core dependency**
|
||||
|
||||
In `core/chanora_core/Cargo.toml`, replace:
|
||||
|
||||
```toml
|
||||
chanora_resolver = { path = "../../crates/chanora_resolver" }
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```toml
|
||||
chanora_prefetch = { path = "../../crates/chanora_prefetch" }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Remove private prefetch cache implementation from core**
|
||||
|
||||
In `core/chanora_core/src/lib.rs`, remove these private items:
|
||||
|
||||
```rust
|
||||
const RESOLUTION_PREFETCH_TTL: Duration = Duration::from_secs(120);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ResolutionPrefetchEntry { ... }
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ResolutionPrefetchCache { ... }
|
||||
|
||||
impl ResolutionPrefetchCache { ... }
|
||||
|
||||
fn normalize_prefetch_host(host: &str) -> String { ... }
|
||||
|
||||
async fn resolve_prefetch_socket(host: &str) -> Result<std::net::SocketAddr, CoreError> { ... }
|
||||
```
|
||||
|
||||
Add this import near the other crate imports:
|
||||
|
||||
```rust
|
||||
use chanora_prefetch::ServerPrefetcher;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Replace the session field and constructor initialization**
|
||||
|
||||
Change the `ChanoraSession` field from:
|
||||
|
||||
```rust
|
||||
resolution_prefetch: Arc<Mutex<ResolutionPrefetchCache>>,
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
server_prefetch: ServerPrefetcher,
|
||||
```
|
||||
|
||||
Change the constructor initialization from:
|
||||
|
||||
```rust
|
||||
resolution_prefetch: Arc::new(Mutex::new(ResolutionPrefetchCache::default())),
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
server_prefetch: ServerPrefetcher::new(),
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Delegate prefetch and fresh-match lookup**
|
||||
|
||||
Replace `prefetch_server_resolution` with:
|
||||
|
||||
```rust
|
||||
pub async fn prefetch_server_resolution(&self, host: String) -> Result<(), CoreError> {
|
||||
self.server_prefetch
|
||||
.prefetch(host)
|
||||
.await
|
||||
.map_err(|err| CoreError::Protocol(chanora_protocol::ProtocolError::DnsFailed {
|
||||
host: "prefetch".to_string(),
|
||||
reason: err.to_string(),
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
Replace `apply_prefetched_resolution` with:
|
||||
|
||||
```rust
|
||||
async fn apply_prefetched_resolution(&self, cfg: &mut ConnectConfig) {
|
||||
cfg.resolved_address = None;
|
||||
let host = cfg.address.trim();
|
||||
if host.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(addr) = self.server_prefetch.fresh_match(host).await {
|
||||
cfg.resolved_address = Some(addr);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep `prepare_connect_configs` unchanged except that it calls the updated `apply_prefetched_resolution`.
|
||||
|
||||
- [ ] **Step 5: Update core tests to use the prefetcher seam**
|
||||
|
||||
Keep these existing core tests:
|
||||
|
||||
```rust
|
||||
session_clears_untrusted_prefetched_resolution_on_cache_miss
|
||||
connect_config_without_prefetched_resolution_clears_only_resolved_address
|
||||
connect_config_preparation_keeps_prefetched_address_out_of_stored_config
|
||||
```
|
||||
|
||||
Replace any direct access to `session.resolution_prefetch` with calls to public test-support methods on `ServerPrefetcher`. In `crates/chanora_prefetch/src/lib.rs`, keep these methods gated with `#[cfg(any(test, feature = "test-support"))]`:
|
||||
|
||||
```rust
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn store_success_for_test(
|
||||
&self,
|
||||
generation: u64,
|
||||
host: &str,
|
||||
resolved_address: SocketAddr,
|
||||
completed_at: Instant,
|
||||
)
|
||||
```
|
||||
|
||||
and:
|
||||
|
||||
```rust
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn begin_for_test(&self, host: &str) -> u64
|
||||
```
|
||||
|
||||
Enable the feature for `chanora_core` tests by adding this dev-dependency in `core/chanora_core/Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dev-dependencies]
|
||||
chanora_prefetch = { path = "../../crates/chanora_prefetch", features = ["test-support"] }
|
||||
```
|
||||
|
||||
Then core test setup should look like:
|
||||
|
||||
```rust
|
||||
let generation = session.server_prefetch.begin_for_test("example.com").await;
|
||||
session
|
||||
.server_prefetch
|
||||
.store_success_for_test(generation, "example.com", cached, std::time::Instant::now())
|
||||
.await;
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run core tests and fix compile errors**
|
||||
|
||||
Run: `cargo test -p chanora_core --lib`
|
||||
|
||||
Expected: all core lib tests pass.
|
||||
|
||||
- [ ] **Step 7: Commit Task 2**
|
||||
|
||||
```bash
|
||||
git add Cargo.toml Cargo.lock core/chanora_core/Cargo.toml core/chanora_core/src/lib.rs crates/chanora_prefetch
|
||||
git commit -m "refactor(core): use server prefetch crate"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Remove Duplicated Core Cache Tests and Verify Protocol Is Unchanged
|
||||
|
||||
**Files:**
|
||||
- Modify: `core/chanora_core/src/lib.rs`
|
||||
- Inspect only: `crates/chanora_protocol/src/adapter.rs`
|
||||
|
||||
- [ ] **Step 1: Remove core tests moved to the prefetch crate**
|
||||
|
||||
Delete these tests from `core/chanora_core/src/lib.rs` because they now belong in `chanora_prefetch`:
|
||||
|
||||
```rust
|
||||
resolution_prefetch_cache_returns_fresh_exact_match
|
||||
resolution_prefetch_cache_ignores_stale_entries
|
||||
resolution_prefetch_cache_ignores_different_hosts
|
||||
resolution_prefetch_cache_ignores_stale_generation_completion
|
||||
```
|
||||
|
||||
Do not delete core trust-boundary tests:
|
||||
|
||||
```rust
|
||||
session_uses_fresh_prefetched_resolution_for_connect_config
|
||||
session_clears_untrusted_prefetched_resolution_on_cache_miss
|
||||
connect_config_without_prefetched_resolution_clears_only_resolved_address
|
||||
connect_config_preparation_keeps_prefetched_address_out_of_stored_config
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Confirm protocol source is untouched**
|
||||
|
||||
Run: `git diff -- crates/chanora_protocol/src/adapter.rs`
|
||||
|
||||
Expected: no diff. If there is a diff, revert only accidental protocol edits by manually restoring the changed lines from `HEAD`; do not use destructive git checkout/reset.
|
||||
|
||||
- [ ] **Step 3: Run combined Rust tests**
|
||||
|
||||
Run: `cargo test -p chanora_prefetch -p chanora_core -p chanora_protocol --lib`
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 4: Commit Task 3**
|
||||
|
||||
```bash
|
||||
git add core/chanora_core/src/lib.rs Cargo.lock
|
||||
git commit -m "test(core): keep prefetch tests at crate boundary"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Final Verification and Android Smoke
|
||||
|
||||
**Files:**
|
||||
- No source edits expected.
|
||||
|
||||
- [ ] **Step 1: Run full focused Rust verification**
|
||||
|
||||
Run: `cargo test -p chanora_resolver -p chanora_prefetch -p chanora_protocol -p chanora_core --lib`
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 2: Run focused Flutter tests**
|
||||
|
||||
Run from `apps/chanora_flutter`:
|
||||
|
||||
```bash
|
||||
flutter test test/services/server_resolution_prefetch_scheduler_test.dart test/services/connection_phase_state_test.dart test/services/ts3_server_link_test.dart
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 3: Build Android APK**
|
||||
|
||||
Run from `apps/chanora_flutter`:
|
||||
|
||||
```bash
|
||||
flutter build apk --debug
|
||||
```
|
||||
|
||||
Expected: build succeeds and produces `build/app/outputs/flutter-apk/app-debug.apk`.
|
||||
|
||||
- [ ] **Step 4: Run fresh-launch Android prefetch/connect smoke**
|
||||
|
||||
Run from `apps/chanora_flutter` with an emulator/device attached:
|
||||
|
||||
```bash
|
||||
adb install -r "build/app/outputs/flutter-apk/app-debug.apk"
|
||||
adb shell am force-stop app.chanora.chanora_flutter
|
||||
adb logcat -c
|
||||
adb shell monkey -p app.chanora.chanora_flutter -c android.intent.category.LAUNCHER 1
|
||||
sleep 5
|
||||
adb shell input tap 354 1363
|
||||
sleep 8
|
||||
adb logcat -d -v time | rg "resolution prefetch|prefetched|client request resolution|server address resolved|FATAL|AndroidRuntime|ANR"
|
||||
adb shell uiautomator dump /sdcard/window.xml
|
||||
adb exec-out cat /sdcard/window.xml
|
||||
```
|
||||
|
||||
Expected logs include:
|
||||
|
||||
```text
|
||||
resolution prefetch started
|
||||
resolution prefetch result
|
||||
connect using prefetched resolution
|
||||
using prefetched server address
|
||||
```
|
||||
|
||||
Expected UI hierarchy includes:
|
||||
|
||||
```text
|
||||
Vigorous Pro
|
||||
Leave server
|
||||
Default Channel
|
||||
ChanoraBeta
|
||||
```
|
||||
|
||||
No app `FATAL`, app `AndroidRuntime` crash, or `ANR` lines should appear. `AndroidRuntime` lines from `monkey` or `uiautomator` are not app crashes.
|
||||
|
||||
- [ ] **Step 5: Run one channel switch smoke**
|
||||
|
||||
With the app still connected, run:
|
||||
|
||||
```bash
|
||||
adb logcat -c
|
||||
adb shell input tap 300 1705
|
||||
sleep 2
|
||||
adb shell input tap 300 1465
|
||||
sleep 2
|
||||
adb logcat -d -v time | rg "voice_join|client_move|FATAL|AndroidRuntime|ANR"
|
||||
adb shell uiautomator dump /sdcard/window.xml
|
||||
adb exec-out cat /sdcard/window.xml
|
||||
```
|
||||
|
||||
Expected logs include successful moves to channel IDs similar to:
|
||||
|
||||
```text
|
||||
voice_join accepted by server
|
||||
client_move resolved by authoritative self channel change
|
||||
```
|
||||
|
||||
Expected final UI has `ChanoraBeta` under `Default Channel`.
|
||||
|
||||
- [ ] **Step 6: Check final git state**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git log --oneline -5
|
||||
```
|
||||
|
||||
Expected: only unrelated untracked `config.json` remains, unless the user has added other unrelated work. The latest commits should be the prefetch crate split commits.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage: the plan adds `chanora_prefetch`, moves cache policy and resolver-backed warming there, keeps core as trust boundary, preserves Flutter/protocol behavior, and includes Android connect smoke verification.
|
||||
- Placeholder scan: no `TBD`, `TODO`, or unspecified edge handling remains.
|
||||
- Type consistency: the plan uses `ServerPrefetcher`, `ServerPrefetchError`, `prefetch`, and `fresh_match` consistently across crate and core tasks.
|
||||
@@ -0,0 +1,899 @@
|
||||
# Server Resolution Prefetch Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add invisible server-address resolution prefetch so the active host field can warm Rust resolver state before Connect without changing connection semantics.
|
||||
|
||||
**Architecture:** Flutter schedules debounced prefetch calls for the active host field only. Rust owns resolution prefetch state, TTL, exact-match validation, and connect-time reuse. The protocol layer accepts an optional already-resolved socket address so Connect can skip resolver work only when the core cache says it is safe.
|
||||
|
||||
**Tech Stack:** Flutter/Dart, flutter_rust_bridge generated bindings, Rust async Tokio, `chanora_core`, `chanora_protocol`, `chanora_resolver`, Flutter widget/service tests, Rust unit tests.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify `core/chanora_core/src/lib.rs`: add a session-owned prefetch cache, prefetch API, connect-time cache lookup, and unit tests for TTL/exact-match/generation behavior.
|
||||
- Modify `crates/chanora_protocol/src/adapter.rs`: add `resolved_address: Option<SocketAddr>` to `ConnectConfig` and skip `resolve_server_socket()` when present.
|
||||
- Modify `crates/chanora_bridge/src/api.rs`: add `prefetch_server_resolution(host: String)` bridge function and pass cache-aware connects through `ChanoraSession`.
|
||||
- Regenerate `crates/chanora_bridge/src/frb_generated.rs`: generated Rust bridge bindings.
|
||||
- Regenerate `apps/chanora_flutter/lib/src/rust/api.dart`, `api.freezed.dart`, and `frb_generated.dart`: generated Dart bridge bindings.
|
||||
- Create `apps/chanora_flutter/lib/services/server_resolution_prefetch_scheduler.dart`: small testable debounce helper for host-field prefetch scheduling.
|
||||
- Modify `apps/chanora_flutter/lib/main.dart`: wire `_hostCtl` listener, settings-loaded prefetch, and disposal.
|
||||
- Create `apps/chanora_flutter/test/services/server_resolution_prefetch_scheduler_test.dart`: Flutter/Dart tests for debounce and empty-host behavior.
|
||||
|
||||
Keep all prefetch UI invisible. Do not prefetch bookmarks in bulk.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Protocol Config Accepts Prefetched Socket
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/chanora_protocol/src/adapter.rs`
|
||||
|
||||
- [ ] **Step 1: Add a failing protocol config test**
|
||||
|
||||
Add this test inside the existing `#[cfg(test)] mod tests` in `crates/chanora_protocol/src/adapter.rs`:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn connect_config_can_carry_prefetched_socket_address() {
|
||||
let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap();
|
||||
let cfg = ConnectConfig {
|
||||
address: "example.com".to_string(),
|
||||
nickname: "Tester".to_string(),
|
||||
password: None,
|
||||
identity: None,
|
||||
ready_timeout: Duration::from_secs(1),
|
||||
resolved_address: Some(addr),
|
||||
};
|
||||
|
||||
assert_eq!(cfg.resolved_address, Some(addr));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `cargo test -p chanora_protocol connect_config_can_carry_prefetched_socket_address`
|
||||
|
||||
Expected: FAIL to compile with a message like `struct ConnectConfig has no field named resolved_address`.
|
||||
|
||||
- [ ] **Step 3: Add the minimal config field**
|
||||
|
||||
Update `ConnectConfig` in `crates/chanora_protocol/src/adapter.rs`:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConnectConfig {
|
||||
/// Server address: `hostname[:port]` or TSDNS name.
|
||||
pub address: String,
|
||||
/// Optional already-resolved socket address from core's invisible
|
||||
/// prefetch cache. When present, the protocol layer skips address
|
||||
/// resolution but still opens a normal TS3 connection only after
|
||||
/// the user requested Connect.
|
||||
pub resolved_address: Option<std::net::SocketAddr>,
|
||||
/// Nickname to use on the server.
|
||||
pub nickname: String,
|
||||
/// Optional server password.
|
||||
pub password: Option<String>,
|
||||
/// Optional pre-existing identity (base64 string accepted by
|
||||
/// `tsclientlib::Identity::new_from_str`). If `None`, a fresh
|
||||
/// identity is generated and **not persisted** — production callers
|
||||
/// should provide one from secure identity storage.
|
||||
pub identity: Option<String>,
|
||||
/// How long to wait for the initial state snapshot before
|
||||
/// returning `ProtocolError::Timeout`.
|
||||
pub ready_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for ConnectConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
address: String::new(),
|
||||
resolved_address: None,
|
||||
nickname: "Chanora".to_string(),
|
||||
password: None,
|
||||
identity: None,
|
||||
ready_timeout: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the focused test**
|
||||
|
||||
Run: `cargo test -p chanora_protocol connect_config_can_carry_prefetched_socket_address`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/chanora_protocol/src/adapter.rs
|
||||
git commit -m "feat(protocol): accept prefetched server address"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Protocol Connect Skips Resolver On Prefetch Hit
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/chanora_protocol/src/adapter.rs`
|
||||
|
||||
- [ ] **Step 1: Add a failing helper test**
|
||||
|
||||
Add a private helper next to `resolve_server_socket()` only after this failing test is added. First add this test in `#[cfg(test)] mod tests`:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn server_socket_from_config_prefers_prefetched_address() {
|
||||
let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap();
|
||||
let cfg = ConnectConfig {
|
||||
address: "example.com".to_string(),
|
||||
resolved_address: Some(addr),
|
||||
nickname: "Tester".to_string(),
|
||||
password: None,
|
||||
identity: None,
|
||||
ready_timeout: Duration::from_secs(1),
|
||||
};
|
||||
|
||||
assert_eq!(server_socket_from_config(&cfg), Some(addr));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `cargo test -p chanora_protocol server_socket_from_config_prefers_prefetched_address`
|
||||
|
||||
Expected: FAIL to compile with `cannot find function server_socket_from_config`.
|
||||
|
||||
- [ ] **Step 3: Add the helper and wire connection task**
|
||||
|
||||
Add this helper near `resolve_server_socket()`:
|
||||
|
||||
```rust
|
||||
fn server_socket_from_config(cfg: &ConnectConfig) -> Option<SocketAddr> {
|
||||
cfg.resolved_address
|
||||
}
|
||||
```
|
||||
|
||||
Find the call in `connection_task` that currently resolves the address, shaped like:
|
||||
|
||||
```rust
|
||||
let server = resolve_server_socket(&cfg.address).await?;
|
||||
```
|
||||
|
||||
Replace it with:
|
||||
|
||||
```rust
|
||||
let server = match server_socket_from_config(&cfg) {
|
||||
Some(addr) => {
|
||||
info!(
|
||||
target: "chanora_protocol",
|
||||
input = %cfg.address,
|
||||
resolved = %addr,
|
||||
"using prefetched server address"
|
||||
);
|
||||
addr
|
||||
}
|
||||
None => resolve_server_socket(&cfg.address).await?,
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run protocol tests**
|
||||
|
||||
Run: `cargo test -p chanora_protocol server_socket_from_config_prefers_prefetched_address`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Run broader protocol tests**
|
||||
|
||||
Run: `cargo test -p chanora_protocol`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/chanora_protocol/src/adapter.rs
|
||||
git commit -m "fix(protocol): reuse prefetched server address"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Core Prefetch Cache Data Model
|
||||
|
||||
**Files:**
|
||||
- Modify: `core/chanora_core/src/lib.rs`
|
||||
|
||||
- [ ] **Step 1: Write failing cache tests**
|
||||
|
||||
Add these tests inside `#[cfg(test)] mod tests` in `core/chanora_core/src/lib.rs`:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn resolution_prefetch_cache_returns_fresh_exact_match() {
|
||||
let mut cache = ResolutionPrefetchCache::default();
|
||||
let now = std::time::Instant::now();
|
||||
let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap();
|
||||
|
||||
let generation = cache.begin(" Example.COM ");
|
||||
cache.store_success(generation, " Example.COM ", addr, now);
|
||||
|
||||
assert_eq!(cache.fresh_match("example.com", now), Some(addr));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_prefetch_cache_ignores_stale_entries() {
|
||||
let mut cache = ResolutionPrefetchCache::default();
|
||||
let now = std::time::Instant::now();
|
||||
let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap();
|
||||
|
||||
let generation = cache.begin("example.com");
|
||||
cache.store_success(generation, "example.com", addr, now - RESOLUTION_PREFETCH_TTL - std::time::Duration::from_secs(1));
|
||||
|
||||
assert_eq!(cache.fresh_match("example.com", now), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_prefetch_cache_ignores_different_hosts() {
|
||||
let mut cache = ResolutionPrefetchCache::default();
|
||||
let now = std::time::Instant::now();
|
||||
let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap();
|
||||
|
||||
let generation = cache.begin("example.com");
|
||||
cache.store_success(generation, "example.com", addr, now);
|
||||
|
||||
assert_eq!(cache.fresh_match("other.example.com", now), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_prefetch_cache_ignores_stale_generation_completion() {
|
||||
let mut cache = ResolutionPrefetchCache::default();
|
||||
let now = std::time::Instant::now();
|
||||
let first: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap();
|
||||
let second: std::net::SocketAddr = "127.0.0.2:9987".parse().unwrap();
|
||||
|
||||
let old_generation = cache.begin("example.com");
|
||||
let new_generation = cache.begin("example.com");
|
||||
cache.store_success(new_generation, "example.com", second, now);
|
||||
cache.store_success(old_generation, "example.com", first, now);
|
||||
|
||||
assert_eq!(cache.fresh_match("example.com", now), Some(second));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cargo test -p chanora_core resolution_prefetch_cache --lib`
|
||||
|
||||
Expected: FAIL to compile with missing `ResolutionPrefetchCache` and `RESOLUTION_PREFETCH_TTL`.
|
||||
|
||||
- [ ] **Step 3: Add the cache model**
|
||||
|
||||
Add near `NetworkDiagnostics` in `core/chanora_core/src/lib.rs`:
|
||||
|
||||
```rust
|
||||
const RESOLUTION_PREFETCH_TTL: Duration = Duration::from_secs(120);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ResolutionPrefetchEntry {
|
||||
normalized_host: String,
|
||||
resolved_address: std::net::SocketAddr,
|
||||
completed_at: std::time::Instant,
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ResolutionPrefetchCache {
|
||||
latest_generation: u64,
|
||||
entry: Option<ResolutionPrefetchEntry>,
|
||||
last_failure: Option<String>,
|
||||
}
|
||||
|
||||
impl ResolutionPrefetchCache {
|
||||
fn begin(&mut self, host: &str) -> u64 {
|
||||
self.latest_generation = self.latest_generation.saturating_add(1);
|
||||
self.last_failure = None;
|
||||
let _ = normalize_prefetch_host(host);
|
||||
self.latest_generation
|
||||
}
|
||||
|
||||
fn store_success(
|
||||
&mut self,
|
||||
generation: u64,
|
||||
host: &str,
|
||||
resolved_address: std::net::SocketAddr,
|
||||
completed_at: std::time::Instant,
|
||||
) {
|
||||
if generation != self.latest_generation {
|
||||
return;
|
||||
}
|
||||
self.entry = Some(ResolutionPrefetchEntry {
|
||||
normalized_host: normalize_prefetch_host(host),
|
||||
resolved_address,
|
||||
completed_at,
|
||||
generation,
|
||||
});
|
||||
self.last_failure = None;
|
||||
}
|
||||
|
||||
fn store_failure(&mut self, generation: u64, error: String) {
|
||||
if generation != self.latest_generation {
|
||||
return;
|
||||
}
|
||||
self.last_failure = Some(error);
|
||||
}
|
||||
|
||||
fn fresh_match(&self, host: &str, now: std::time::Instant) -> Option<std::net::SocketAddr> {
|
||||
let normalized = normalize_prefetch_host(host);
|
||||
let entry = self.entry.as_ref()?;
|
||||
if entry.normalized_host != normalized {
|
||||
return None;
|
||||
}
|
||||
if now.duration_since(entry.completed_at) > RESOLUTION_PREFETCH_TTL {
|
||||
return None;
|
||||
}
|
||||
Some(entry.resolved_address)
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_prefetch_host(host: &str) -> String {
|
||||
host.trim().to_lowercase()
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run focused core tests**
|
||||
|
||||
Run: `cargo test -p chanora_core resolution_prefetch_cache --lib`
|
||||
|
||||
Expected: all four tests PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add core/chanora_core/src/lib.rs
|
||||
git commit -m "feat(core): add resolution prefetch cache"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Core Prefetch API And Connect Cache Lookup
|
||||
|
||||
**Files:**
|
||||
- Modify: `core/chanora_core/src/lib.rs`
|
||||
|
||||
- [ ] **Step 1: Write failing session tests**
|
||||
|
||||
Add this test in `#[cfg(test)] mod tests` in `core/chanora_core/src/lib.rs`:
|
||||
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn session_uses_fresh_prefetched_resolution_for_connect_config() {
|
||||
let session = ChanoraSession::new();
|
||||
let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap();
|
||||
|
||||
{
|
||||
let mut cache = session.resolution_prefetch.lock().await;
|
||||
let generation = cache.begin("example.com");
|
||||
cache.store_success(generation, "example.com", addr, std::time::Instant::now());
|
||||
}
|
||||
|
||||
let mut cfg = ConnectConfig::default();
|
||||
cfg.address = " example.com ".to_string();
|
||||
session.apply_prefetched_resolution(&mut cfg).await;
|
||||
|
||||
assert_eq!(cfg.resolved_address, Some(addr));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `cargo test -p chanora_core session_uses_fresh_prefetched_resolution_for_connect_config --lib`
|
||||
|
||||
Expected: FAIL to compile with missing `resolution_prefetch` and `apply_prefetched_resolution`.
|
||||
|
||||
- [ ] **Step 3: Add session field and constructor initialization**
|
||||
|
||||
Add to `ChanoraSession`:
|
||||
|
||||
```rust
|
||||
/// Invisible server-address prefetch cache. Warmed by Flutter typing
|
||||
/// but validated by Rust before Connect can reuse it.
|
||||
resolution_prefetch: Arc<Mutex<ResolutionPrefetchCache>>,
|
||||
```
|
||||
|
||||
Initialize in `ChanoraSession::new()`:
|
||||
|
||||
```rust
|
||||
resolution_prefetch: Arc::new(Mutex::new(ResolutionPrefetchCache::default())),
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add connect-time cache lookup helper**
|
||||
|
||||
Add methods inside `impl ChanoraSession` before `connect()`:
|
||||
|
||||
```rust
|
||||
async fn apply_prefetched_resolution(&self, cfg: &mut ConnectConfig) {
|
||||
let host = cfg.address.trim();
|
||||
if host.is_empty() {
|
||||
return;
|
||||
}
|
||||
let resolved = {
|
||||
let cache = self.resolution_prefetch.lock().await;
|
||||
cache.fresh_match(host, std::time::Instant::now())
|
||||
};
|
||||
match resolved {
|
||||
Some(addr) => {
|
||||
info!(
|
||||
target: "chanora_core",
|
||||
host = %host,
|
||||
resolved = %addr,
|
||||
"connect using prefetched resolution"
|
||||
);
|
||||
cfg.resolved_address = Some(addr);
|
||||
}
|
||||
None => {
|
||||
info!(target: "chanora_core", host = %host, "connect prefetch miss or stale");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In `connect()`, after identity resolution and before `ProtocolClient::connect(cfg.clone())`, add:
|
||||
|
||||
```rust
|
||||
self.apply_prefetched_resolution(&mut cfg).await;
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add prefetch scheduling API in core**
|
||||
|
||||
Add this method in `impl ChanoraSession`:
|
||||
|
||||
```rust
|
||||
pub async fn prefetch_server_resolution(&self, host: String) -> Result<(), CoreError> {
|
||||
let normalized = normalize_prefetch_host(&host);
|
||||
if normalized.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let generation = {
|
||||
let mut cache = self.resolution_prefetch.lock().await;
|
||||
cache.begin(&normalized)
|
||||
};
|
||||
let cache = self.resolution_prefetch.clone();
|
||||
tokio::spawn(async move {
|
||||
info!(target: "chanora_core", host = %normalized, "resolution prefetch started");
|
||||
let result = resolve_prefetch_socket(&normalized).await;
|
||||
let mut guard = cache.lock().await;
|
||||
match result {
|
||||
Ok(addr) => {
|
||||
info!(
|
||||
target: "chanora_core",
|
||||
host = %normalized,
|
||||
resolved = %addr,
|
||||
"resolution prefetch result"
|
||||
);
|
||||
guard.store_success(generation, &normalized, addr, std::time::Instant::now());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
target: "chanora_core",
|
||||
host = %normalized,
|
||||
error = %err,
|
||||
"resolution prefetch failed"
|
||||
);
|
||||
guard.store_failure(generation, err.to_string());
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Add this helper outside `impl ChanoraSession`:
|
||||
|
||||
```rust
|
||||
async fn resolve_prefetch_socket(host: &str) -> Result<std::net::SocketAddr, CoreError> {
|
||||
let resolver = chanora_resolver::ChanoraResolver::new()
|
||||
.map_err(|err| CoreError::Protocol(chanora_protocol::ProtocolError::DnsFailed {
|
||||
host: host.to_string(),
|
||||
reason: format!("resolver initialization failed: {err}"),
|
||||
}))?;
|
||||
let resolved = resolver
|
||||
.resolve_client_address(host)
|
||||
.await
|
||||
.map_err(|err| CoreError::Protocol(chanora_protocol::ProtocolError::DnsFailed {
|
||||
host: host.to_string(),
|
||||
reason: err.to_string(),
|
||||
}))?;
|
||||
resolved
|
||||
.parse::<std::net::SocketAddr>()
|
||||
.map_err(|err| CoreError::Protocol(chanora_protocol::ProtocolError::DnsFailed {
|
||||
host: host.to_string(),
|
||||
reason: format!("resolver returned invalid socket address '{resolved}': {err}"),
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run focused core tests**
|
||||
|
||||
Run: `cargo test -p chanora_core session_uses_fresh_prefetched_resolution_for_connect_config --lib`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Run core tests**
|
||||
|
||||
Run: `cargo test -p chanora_core --lib`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add core/chanora_core/src/lib.rs
|
||||
git commit -m "feat(core): prefetch server resolution"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Bridge API Exposes Prefetch
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/chanora_bridge/src/api.rs`
|
||||
- Regenerate: `crates/chanora_bridge/src/frb_generated.rs`
|
||||
- Regenerate: `apps/chanora_flutter/lib/src/rust/api.dart`
|
||||
- Regenerate: `apps/chanora_flutter/lib/src/rust/api.freezed.dart`
|
||||
- Regenerate: `apps/chanora_flutter/lib/src/rust/frb_generated.dart`
|
||||
|
||||
- [ ] **Step 1: Add bridge function**
|
||||
|
||||
Add after `connect()` in `crates/chanora_bridge/src/api.rs`:
|
||||
|
||||
```rust
|
||||
/// Warm server address resolution for the active host field. This is
|
||||
/// intentionally fire-and-forget from the UI perspective: it schedules
|
||||
/// Rust-side prefetch work and never opens a TS3 session.
|
||||
pub async fn prefetch_server_resolution(host: String) -> Result<(), BridgeError> {
|
||||
runtime()
|
||||
.spawn(async move { session().prefetch_server_resolution(host).await })
|
||||
.await
|
||||
.map_err(|e| task_join_error("prefetch_server_resolution", e))??;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Regenerate flutter_rust_bridge bindings**
|
||||
|
||||
Run from repo root:
|
||||
|
||||
```bash
|
||||
flutter_rust_bridge_codegen generate
|
||||
```
|
||||
|
||||
Expected: generated files update and `apps/chanora_flutter/lib/src/rust/api.dart` contains:
|
||||
|
||||
```dart
|
||||
Future<void> prefetchServerResolution({required String host}) => RustLib
|
||||
.instance
|
||||
.api
|
||||
.crateApiPrefetchServerResolution(host: host);
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run bridge/core compile check**
|
||||
|
||||
Run: `cargo test -p chanora_bridge --lib`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Run Flutter analyzer smoke check**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter analyze`
|
||||
|
||||
Expected: no new errors related to generated bindings.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/chanora_bridge/src/api.rs crates/chanora_bridge/src/frb_generated.rs apps/chanora_flutter/lib/src/rust/api.dart apps/chanora_flutter/lib/src/rust/api.freezed.dart apps/chanora_flutter/lib/src/rust/frb_generated.dart
|
||||
git commit -m "feat(bridge): expose resolution prefetch"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Flutter Prefetch Scheduler Helper
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/chanora_flutter/lib/services/server_resolution_prefetch_scheduler.dart`
|
||||
- Create: `apps/chanora_flutter/test/services/server_resolution_prefetch_scheduler_test.dart`
|
||||
|
||||
- [ ] **Step 1: Write failing scheduler tests**
|
||||
|
||||
Create `apps/chanora_flutter/test/services/server_resolution_prefetch_scheduler_test.dart`:
|
||||
|
||||
```dart
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:chanora_flutter/services/server_resolution_prefetch_scheduler.dart';
|
||||
|
||||
void main() {
|
||||
test('debounces host edits and prefetches latest trimmed host', () async {
|
||||
final calls = <String>[];
|
||||
final scheduler = ServerResolutionPrefetchScheduler(
|
||||
delay: const Duration(milliseconds: 20),
|
||||
prefetch: (host) async => calls.add(host),
|
||||
);
|
||||
|
||||
scheduler.schedule(' first.example.com ');
|
||||
scheduler.schedule(' second.example.com ');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 35));
|
||||
|
||||
expect(calls, ['second.example.com']);
|
||||
scheduler.dispose();
|
||||
});
|
||||
|
||||
test('skips empty hosts', () async {
|
||||
final calls = <String>[];
|
||||
final scheduler = ServerResolutionPrefetchScheduler(
|
||||
delay: const Duration(milliseconds: 10),
|
||||
prefetch: (host) async => calls.add(host),
|
||||
);
|
||||
|
||||
scheduler.schedule(' ');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 25));
|
||||
|
||||
expect(calls, isEmpty);
|
||||
scheduler.dispose();
|
||||
});
|
||||
|
||||
test('dispose cancels pending prefetch', () async {
|
||||
final calls = <String>[];
|
||||
final scheduler = ServerResolutionPrefetchScheduler(
|
||||
delay: const Duration(milliseconds: 30),
|
||||
prefetch: (host) async => calls.add(host),
|
||||
);
|
||||
|
||||
scheduler.schedule('example.com');
|
||||
scheduler.dispose();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 45));
|
||||
|
||||
expect(calls, isEmpty);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter test test/services/server_resolution_prefetch_scheduler_test.dart`
|
||||
|
||||
Expected: FAIL to compile with missing `server_resolution_prefetch_scheduler.dart`.
|
||||
|
||||
- [ ] **Step 3: Implement scheduler**
|
||||
|
||||
Create `apps/chanora_flutter/lib/services/server_resolution_prefetch_scheduler.dart`:
|
||||
|
||||
```dart
|
||||
import 'dart:async';
|
||||
|
||||
typedef ServerResolutionPrefetch = Future<void> Function(String host);
|
||||
|
||||
class ServerResolutionPrefetchScheduler {
|
||||
ServerResolutionPrefetchScheduler({
|
||||
required this.prefetch,
|
||||
this.delay = const Duration(milliseconds: 700),
|
||||
});
|
||||
|
||||
final ServerResolutionPrefetch prefetch;
|
||||
final Duration delay;
|
||||
|
||||
Timer? _timer;
|
||||
bool _disposed = false;
|
||||
|
||||
void schedule(String rawHost) {
|
||||
if (_disposed) return;
|
||||
_timer?.cancel();
|
||||
final host = rawHost.trim();
|
||||
if (host.isEmpty) return;
|
||||
_timer = Timer(delay, () {
|
||||
if (_disposed) return;
|
||||
unawaited(prefetch(host));
|
||||
});
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run scheduler tests**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter test test/services/server_resolution_prefetch_scheduler_test.dart`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Format Dart files**
|
||||
|
||||
Run: `cd apps/chanora_flutter && dart format lib/services/server_resolution_prefetch_scheduler.dart test/services/server_resolution_prefetch_scheduler_test.dart`
|
||||
|
||||
Expected: files formatted, no errors.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/chanora_flutter/lib/services/server_resolution_prefetch_scheduler.dart apps/chanora_flutter/test/services/server_resolution_prefetch_scheduler_test.dart
|
||||
git commit -m "feat(ui): add resolution prefetch scheduler"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Wire Prefetch Scheduler Into Home UI
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/chanora_flutter/lib/main.dart`
|
||||
|
||||
- [ ] **Step 1: Add imports and state fields**
|
||||
|
||||
In `apps/chanora_flutter/lib/main.dart`, add:
|
||||
|
||||
```dart
|
||||
import 'services/server_resolution_prefetch_scheduler.dart';
|
||||
```
|
||||
|
||||
Inside `_BetaHomeState`, add:
|
||||
|
||||
```dart
|
||||
late final ServerResolutionPrefetchScheduler _resolutionPrefetch;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Initialize scheduler and listener**
|
||||
|
||||
In `initState()`, before `_eventsSub = rust.eventsStream().listen(_onEvent);`, add:
|
||||
|
||||
```dart
|
||||
_resolutionPrefetch = ServerResolutionPrefetchScheduler(
|
||||
prefetch: (host) => rust.prefetchServerResolution(host: host),
|
||||
);
|
||||
_hostCtl.addListener(_onHostEdited);
|
||||
```
|
||||
|
||||
Add method in `_BetaHomeState`:
|
||||
|
||||
```dart
|
||||
void _onHostEdited() {
|
||||
_resolutionPrefetch.schedule(_hostCtl.text);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Schedule prefetch after settings load**
|
||||
|
||||
In `_loadUiSettings()`, after setting host/nickname from settings and before the `catch`, add:
|
||||
|
||||
```dart
|
||||
if (settings.host.isNotEmpty) {
|
||||
_resolutionPrefetch.schedule(settings.host);
|
||||
}
|
||||
```
|
||||
|
||||
Ensure this is not duplicated if an existing `_hostCtl.text = settings.host;` listener already schedules it. If both paths fire, keep only the explicit `_resolutionPrefetch.schedule(settings.host);` and temporarily remove/re-add the listener around `_hostCtl.text = settings.host`, or accept the duplicate because scheduler debounce collapses it. Prefer accepting the duplicate for minimal change.
|
||||
|
||||
- [ ] **Step 4: Dispose scheduler and listener**
|
||||
|
||||
In `dispose()`, before `_hostCtl.dispose();`, add:
|
||||
|
||||
```dart
|
||||
_hostCtl.removeListener(_onHostEdited);
|
||||
_resolutionPrefetch.dispose();
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run focused Flutter tests**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter test test/services/server_resolution_prefetch_scheduler_test.dart test/services/connection_phase_state_test.dart`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Run analyzer**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter analyze`
|
||||
|
||||
Expected: no new analyzer errors.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/chanora_flutter/lib/main.dart
|
||||
git commit -m "feat(ui): prefetch active server address"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: End-To-End Verification On Android
|
||||
|
||||
**Files:**
|
||||
- No source changes expected unless verification finds a bug.
|
||||
|
||||
- [ ] **Step 1: Run focused Rust tests**
|
||||
|
||||
Run: `cargo test -p chanora_resolver -p chanora_protocol -p chanora_core --lib`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run focused Flutter tests**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter test test/services/server_resolution_prefetch_scheduler_test.dart test/services/connection_phase_state_test.dart test/services/ts3_server_link_test.dart`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Build Android debug APK**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter build apk --debug`
|
||||
|
||||
Expected: `✓ Built build/app/outputs/flutter-apk/app-debug.apk`.
|
||||
|
||||
- [ ] **Step 4: Install and launch through ADB**
|
||||
|
||||
Run from `apps/chanora_flutter`:
|
||||
|
||||
```bash
|
||||
adb install -r "build/app/outputs/flutter-apk/app-debug.apk"
|
||||
adb shell am force-stop app.chanora.chanora_flutter
|
||||
adb logcat -c
|
||||
adb shell monkey -p app.chanora.chanora_flutter -c android.intent.category.LAUNCHER 1
|
||||
```
|
||||
|
||||
Expected: install success and app launches.
|
||||
|
||||
- [ ] **Step 5: Allow prefetch, connect, and inspect logs**
|
||||
|
||||
Run from repo root:
|
||||
|
||||
```bash
|
||||
sleep 2
|
||||
adb shell input tap 354 1363
|
||||
sleep 8
|
||||
adb logcat -d -v time | rg "resolution prefetch|prefetched|client request resolution|server address resolved|FATAL|AndroidRuntime"
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- No app `FATAL` lines.
|
||||
- Logs include `resolution prefetch started` and either `resolution prefetch result` or safe failure.
|
||||
- If prefetch completed before tap, logs include `connect using prefetched resolution` and protocol logs include `using prefetched server address`.
|
||||
- If prefetch did not complete before tap, connect still succeeds through normal resolver path.
|
||||
|
||||
- [ ] **Step 6: Inspect UI hierarchy**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
adb shell uiautomator dump /sdcard/window.xml
|
||||
adb exec-out cat /sdcard/window.xml
|
||||
```
|
||||
|
||||
Expected: connected server view with channel count, not stuck on `Synchronizing...`.
|
||||
|
||||
- [ ] **Step 7: Final status check**
|
||||
|
||||
Run: `git status --short`
|
||||
|
||||
Expected: no unintended source changes. Unrelated existing `config.json` may remain untracked and must not be committed.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
Spec coverage:
|
||||
|
||||
- Active-field-only prefetch: Tasks 6 and 7.
|
||||
- Rust-owned resolver cache and exact-match validation: Tasks 3 and 4.
|
||||
- 2-minute TTL: Task 3.
|
||||
- Invisible UI: Tasks 6 and 7 avoid any UI status changes.
|
||||
- No bookmark fan-out: Task 7 wires only `_hostCtl` and settings-loaded host.
|
||||
- No TS3 session before Connect: Task 4 only resolves address; Task 2 only uses resolved address during actual protocol connect.
|
||||
- Diagnostics: Tasks 2 and 4 add logs for prefetch and cache hit/miss.
|
||||
- Testing and Android smoke: Task 8.
|
||||
|
||||
Completeness scan: no incomplete markers are used. Each code-changing task includes concrete code snippets and commands.
|
||||
|
||||
Type consistency: `resolved_address` is added to protocol `ConnectConfig`, core uses `cfg.resolved_address`, bridge exposes `prefetch_server_resolution`, and Dart uses generated `prefetchServerResolution`.
|
||||
@@ -0,0 +1,642 @@
|
||||
# Chat Panel Switching Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Enable in-place conversation switching in the expanded 3-panel layout, add per-conversation draft persistence, and improve unread awareness — matching industry-standard UX patterns from Discord/Slack/Telegram/Element.
|
||||
|
||||
**Architecture:** The expanded layout (≥1024dp) shows voice controls | channel tree | inline chat panel. Currently the chat panel locks to one conversation with no way to switch from the channel tree. The fix adds channel→chat triggers, in-place target swapping, per-target draft storage, and preserves chat state across switches. The state machine (`_inlineChatTarget` + `_chatOpen`) already supports switching — we just need UI affordances and draft persistence.
|
||||
|
||||
**Tech Stack:** Flutter/Dart, existing `BridgeMessageTarget` sealed class, existing `ChatDetailView` / `ChatPanel` / `SnapshotView` widgets.
|
||||
|
||||
**Design research basis:** Discord (in-place swap, dot/badge unread hierarchy), Telegram Desktop (adaptive 3-tier layout, per-conversation drafts + scroll anchoring), Element (per-room panel state, toggleable right panel), Slack (bold sidebar for unread, split view). All apps treat DMs and channels identically for switching behavior.
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
| UX Element | Status |
|
||||
|---|---|
|
||||
| Unread indicator | Single global badge count on app bar chat button |
|
||||
| Channel → chat trigger | **None.** Channel tiles only join voice |
|
||||
| Client → DM trigger | Works (right-click → "Direct Message") |
|
||||
| Header chat button when panel open | Idempotent — re-uses same `_inlineChatTarget` |
|
||||
| Draft persistence | None — single `TextEditingController`, lost on switch |
|
||||
| Scroll position memory | None — always auto-scrolls to bottom |
|
||||
| Per-conversation unread | None |
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope (this plan):**
|
||||
- Channel → chat switching in expanded layout
|
||||
- Channel right-click → "Chat" option
|
||||
- Header chat button → switch to current voice channel chat when panel already open
|
||||
- Per-target draft persistence (in-memory `Map`)
|
||||
- Close = dismiss (remember last target and draft)
|
||||
- Unread dot indicator on channels in `SnapshotView`
|
||||
|
||||
**Out of scope (future):**
|
||||
- Scroll position memory per target
|
||||
- "New messages" divider
|
||||
- Per-target unread counts / badge numbers
|
||||
- Notification tiering (dot/badge/mention)
|
||||
- Split view (Slack power-user feature)
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
| Tier | Width | Chat Mode | Changes in this plan |
|
||||
|---|---|---|---|
|
||||
| **Expanded** | ≥1024dp | Inline `ChatPanel` (right column) | ✅ All changes apply here |
|
||||
| **Medium** | 600–1023dp | Full-screen `ChatPage` route | No changes needed (already works) |
|
||||
| **Compact** | <600dp | Full-screen `ChatPage` route | No changes needed (already works) |
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|---|---|---|
|
||||
| `apps/chanora_flutter/lib/widgets/snapshot_view.dart` | **Modify** | Add `onOpenChannelChat` callback, channel context menu with "Chat" option, unread dot on channels |
|
||||
| `apps/chanora_flutter/lib/main.dart` | **Modify** | Add `_chatDrafts` map, wire `onOpenChannelChat`, fix header chat button to switch to current channel, fix `_closeInlineChat` to preserve last target |
|
||||
| `apps/chanora_flutter/lib/widgets/chat_panel.dart` | **Modify** | Accept `onSwitchTarget` callback, pass draft state through |
|
||||
| `apps/chanora_flutter/lib/widgets/chat_views.dart` | **Modify** | `ChatDetailView` accepts external draft text, exposes draft text on target change |
|
||||
| `apps/chanora_flutter/lib/design/breakpoints.dart` | **No changes** | Breakpoints unchanged |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add `onOpenChannelChat` callback to `SnapshotView`
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/chanora_flutter/lib/widgets/snapshot_view.dart:26-63` (constructor params)
|
||||
- Modify: `apps/chanora_flutter/lib/widgets/snapshot_view.dart:203-284` (`_channelTile`)
|
||||
|
||||
- [ ] **Step 1: Add the callback field to `SnapshotView` widget**
|
||||
|
||||
In `snapshot_view.dart`, add a new optional callback field after `onOpenClientPoke` (around line 71):
|
||||
|
||||
```dart
|
||||
/// Open chat for a channel.
|
||||
final ValueChanged<rust.BridgeChannel>? onOpenChannelChat;
|
||||
```
|
||||
|
||||
Update the constructor to include it (around line 32, after `onOpenClientPoke`):
|
||||
|
||||
```dart
|
||||
this.onOpenChannelChat,
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add a right-click/long-press context menu to `_channelTile`**
|
||||
|
||||
Replace the `InkWell` in `_channelTile` (lines 243-284) with a context menu wrapper. The channel tile should support:
|
||||
- **Tap**: join voice (existing behavior, unchanged)
|
||||
- **Right-click / long-press**: show a popup menu with "Open chat" option
|
||||
|
||||
```dart
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: widget.onOpenChannelChat != null
|
||||
? () => widget.onOpenChannelChat!(channel)
|
||||
: null,
|
||||
child: PopupMenuButton<String>(
|
||||
position: PopupMenuPosition.under,
|
||||
enabled: widget.onOpenChannelChat != null,
|
||||
onSelected: (value) {
|
||||
if (value == 'chat') {
|
||||
widget.onOpenChannelChat?.call(channel);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'chat',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.chat_bubble_outline, size: 18),
|
||||
const SizedBox(width: 12),
|
||||
Text(AppLocalizations.of(context)!.chatAction),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 40),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: channelIndent),
|
||||
_expandButton(
|
||||
theme,
|
||||
hasVisibleChildren: hasVisibleChildren,
|
||||
expanded: expanded,
|
||||
onPressed: onToggleExpanded,
|
||||
),
|
||||
SizedBox(
|
||||
width: _channelIconColumnWidth,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Icon(
|
||||
Icons.tag,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _channelTextGap),
|
||||
Expanded(
|
||||
child: Text(
|
||||
channel.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (channel.hasPassword) ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.lock_outline,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
Note: The `PopupMenuButton` wraps the existing content as its `child`, so the tile looks identical until right-clicked. The `onTap` on `InkWell` continues to handle voice join.
|
||||
|
||||
- [ ] **Step 3: Run `flutter analyze`**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter analyze`
|
||||
Expected: No new errors (the callback is optional, so existing call sites compile without changes)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/chanora_flutter/lib/widgets/snapshot_view.dart
|
||||
git commit -m "feat(chat): add onOpenChannelChat callback with context menu to channel tiles"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Wire `onOpenChannelChat` in `main.dart` and add per-target draft storage
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/chanora_flutter/lib/main.dart:370-374` (state fields)
|
||||
- Modify: `apps/chanora_flutter/lib/main.dart:2521-2540` (SnapshotView constructor)
|
||||
|
||||
- [ ] **Step 1: Add draft storage map**
|
||||
|
||||
Add a new state field near line 374 (after `_inlineChatCollapseNoticeShown`):
|
||||
|
||||
```dart
|
||||
/// Per-target draft text. Populated when switching away from a conversation
|
||||
/// so the user's unfinished message is preserved.
|
||||
final Map<String, String> _chatDrafts = {};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `_lastDismissedTarget` field**
|
||||
|
||||
Add a new state field to remember the last dismissed target so reopening returns to it:
|
||||
|
||||
```dart
|
||||
/// The last chat target before the panel was closed. Used to restore the
|
||||
/// previous conversation when the user reopens chat.
|
||||
rust.BridgeMessageTarget? _lastDismissedTarget;
|
||||
String _lastDismissedClientName = '';
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Wire `onOpenChannelChat` in `SnapshotView` constructor**
|
||||
|
||||
In the `SnapshotView(...)` constructor around line 2512, add the new callback:
|
||||
|
||||
```dart
|
||||
onOpenChannelChat: (channel) => unawaited(
|
||||
_onOpenChat(
|
||||
target: rust.BridgeMessageTarget.channel(channel.id),
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run `flutter analyze`**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter analyze`
|
||||
Expected: No new errors
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/chanora_flutter/lib/main.dart
|
||||
git commit -m "feat(chat): add per-target draft storage and wire onOpenChannelChat"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Fix `_onOpenChat` to support switching and draft save/restore
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/chanora_flutter/lib/main.dart:1628-1685` (`_onOpenChat` and `_closeInlineChat`)
|
||||
|
||||
- [ ] **Step 1: Update `_onOpenChat` to save current draft and restore new target's draft**
|
||||
|
||||
Replace the `_onOpenChat` method (lines 1628-1676) with logic that:
|
||||
1. Saves the current `_inlineChatTarget` draft before switching
|
||||
2. Restores the new target's draft (if any)
|
||||
3. When called with no explicit target and panel is already open, switches to current voice channel's chat
|
||||
|
||||
```dart
|
||||
Future<void> _onOpenChat({
|
||||
rust.BridgeMessageTarget? target,
|
||||
String clientName = '',
|
||||
}) async {
|
||||
final initialSnapshot = _snapshot!;
|
||||
|
||||
// Resolve the new target.
|
||||
// If no target passed and panel is already open, switch to current voice channel.
|
||||
// If no target passed and panel is closed, resolve from history or default.
|
||||
rust.BridgeMessageTarget newTarget;
|
||||
if (target != null) {
|
||||
newTarget = target;
|
||||
} else if (_chatOpen && _currentVoiceChannelId != null) {
|
||||
newTarget = rust.BridgeMessageTarget.channel(_currentVoiceChannelId!);
|
||||
} else if (_inlineChatTarget != null) {
|
||||
newTarget = _inlineChatTarget!;
|
||||
} else {
|
||||
newTarget = resolveInitialChatTarget(
|
||||
messages: _chatMessages,
|
||||
currentVoiceChannelId: _currentVoiceChannelId,
|
||||
) ?? const rust.BridgeMessageTarget.server();
|
||||
}
|
||||
|
||||
final newClientName = clientName.isNotEmpty
|
||||
? clientName
|
||||
: (newTarget == _inlineChatTarget) ? _inlineChatClientName : '';
|
||||
|
||||
final isExpanded =
|
||||
layoutClassFromWidth(MediaQuery.sizeOf(context).width) ==
|
||||
LayoutClass.expanded;
|
||||
if (isExpanded) {
|
||||
setState(() {
|
||||
// Save draft for the current target before switching.
|
||||
_saveCurrentDraft();
|
||||
_chatUnread = 0;
|
||||
_chatOpen = true;
|
||||
_inlineChatTarget = newTarget;
|
||||
_inlineChatClientName = newClientName;
|
||||
_inlineChatCollapseNoticeShown = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_chatUnread = 0;
|
||||
_chatOpen = true;
|
||||
});
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ChatPage(
|
||||
messages: _chatMessages,
|
||||
snapshot: initialSnapshot,
|
||||
messagesSource: () => _chatMessages,
|
||||
snapshotSource: () => _snapshot ?? initialSnapshot,
|
||||
refreshListenable: _chatFeedRevision,
|
||||
initialTarget: newTarget,
|
||||
initialClientName: newClientName,
|
||||
onTs3ServerLink: _onTs3ServerLink,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (mounted) setState(() => _chatOpen = false);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `_saveCurrentDraft` and `_draftKeyForTarget` helper methods**
|
||||
|
||||
Add these near `_onOpenChat`:
|
||||
|
||||
```dart
|
||||
/// Converts a [rust.BridgeMessageTarget] to a stable string key for draft storage.
|
||||
String _draftKeyForTarget(rust.BridgeMessageTarget target) {
|
||||
return switch (target) {
|
||||
rust.BridgeMessageTarget_Server() => 'server',
|
||||
rust.BridgeMessageTarget_Channel(:final id) => 'channel:$id',
|
||||
rust.BridgeMessageTarget_Client(:final id) => 'client:$id',
|
||||
rust.BridgeMessageTarget_Poke(:final id) => 'poke:$id',
|
||||
};
|
||||
}
|
||||
|
||||
/// Saves the current draft text (if any) for the current inline chat target.
|
||||
/// Called before switching targets or closing the panel.
|
||||
void _saveCurrentDraft() {
|
||||
// Note: The actual draft text is read from ChatDetailView's
|
||||
// TextEditingController via a callback. This is wired in Task 4.
|
||||
}
|
||||
```
|
||||
|
||||
Note: `_saveCurrentDraft` will be completed in Task 4 when we wire the draft callback from `ChatDetailView`.
|
||||
|
||||
- [ ] **Step 3: Update `_closeInlineChat` to preserve last target instead of nulling it**
|
||||
|
||||
Replace `_closeInlineChat` (lines 1678-1685):
|
||||
|
||||
```dart
|
||||
void _closeInlineChat() {
|
||||
setState(() {
|
||||
// Save draft before closing.
|
||||
_saveCurrentDraft();
|
||||
// Remember the last target so reopening returns to it.
|
||||
_lastDismissedTarget = _inlineChatTarget;
|
||||
_lastDismissedClientName = _inlineChatClientName;
|
||||
_chatOpen = false;
|
||||
// Do NOT null _inlineChatTarget — we want to remember it for reopen.
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run `flutter analyze`**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter analyze`
|
||||
Expected: No new errors
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/chanora_flutter/lib/main.dart
|
||||
git commit -m "feat(chat): switch chat target on channel click, save draft before switching, preserve target on close"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add draft save/restore callback to `ChatDetailView` and `ChatPanel`
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/chanora_flutter/lib/widgets/chat_views.dart:1054-1098` (`ChatDetailView` constructor + state)
|
||||
- Modify: `apps/chanora_flutter/lib/widgets/chat_panel.dart:12-75` (`ChatPanel` constructor + build)
|
||||
|
||||
- [ ] **Step 1: Add draft callbacks to `ChatDetailView`**
|
||||
|
||||
Add two new optional callbacks to `ChatDetailView` (after `messageMaxWidth` around line 1066):
|
||||
|
||||
```dart
|
||||
/// External draft text to restore when the widget initializes or the target changes.
|
||||
final String? restoredDraft;
|
||||
|
||||
/// Called with the current draft text whenever the target changes or the widget is disposed.
|
||||
final ValueChanged<String>? onDraftChanged;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Implement draft restore in `_ChatDetailViewState`**
|
||||
|
||||
In `_ChatDetailViewState` (line 1100), add `initState` and `didUpdateWidget` to handle drafts:
|
||||
|
||||
```dart
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.restoredDraft != null && widget.restoredDraft!.isNotEmpty) {
|
||||
_textCtl.text = widget.restoredDraft!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ChatDetailView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.target != widget.target) {
|
||||
// Save draft for old target before switching.
|
||||
if (oldWidget.onDraftChanged != null && _textCtl.text.isNotEmpty) {
|
||||
oldWidget.onDraftChanged!(_textCtl.text);
|
||||
}
|
||||
// Restore draft for new target.
|
||||
_textCtl.text = widget.restoredDraft ?? '';
|
||||
_lastRenderedTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Emit the current draft so the parent can save it.
|
||||
if (widget.onDraftChanged != null && _textCtl.text.isNotEmpty) {
|
||||
widget.onDraftChanged!(_textCtl.text);
|
||||
}
|
||||
_textCtl.dispose();
|
||||
_scrollCtl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
Remove the existing `dispose` method (lines 1127-1132) — it's replaced by the new one above.
|
||||
|
||||
- [ ] **Step 3: Thread draft callbacks through `ChatPanel`**
|
||||
|
||||
Update `ChatPanel` to accept and pass through the new callbacks. Add fields:
|
||||
|
||||
```dart
|
||||
/// External draft text to restore in the chat detail view.
|
||||
final String? restoredDraft;
|
||||
|
||||
/// Called when the draft text changes.
|
||||
final ValueChanged<String>? onDraftChanged;
|
||||
```
|
||||
|
||||
Pass them through in `build()` where `ChatDetailView` is constructed (line 58):
|
||||
|
||||
```dart
|
||||
child: ChatDetailView(
|
||||
messages: messages,
|
||||
snapshot: snapshot,
|
||||
target: target,
|
||||
clientName: clientName,
|
||||
currentChannelId: currentChannelId,
|
||||
channelName: channelName,
|
||||
onTs3ServerLink: onTs3ServerLink,
|
||||
restoredDraft: restoredDraft,
|
||||
onDraftChanged: onDraftChanged,
|
||||
messageMaxWidth: 500,
|
||||
headerTrailing: IconButton(
|
||||
tooltip: 'Close chat',
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: onClose,
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Wire draft callbacks in `main.dart`**
|
||||
|
||||
In the `ChatPanel(...)` constructor around line 2567, add the draft callbacks:
|
||||
|
||||
```dart
|
||||
ChatPanel(
|
||||
messages: _chatMessages,
|
||||
snapshot: _snapshot!,
|
||||
target: inlineChatTarget,
|
||||
clientName: _inlineChatClientName,
|
||||
onTs3ServerLink: _onTs3ServerLink,
|
||||
restoredDraft: _chatDrafts[_draftKeyForTarget(inlineChatTarget)],
|
||||
onDraftChanged: (text) {
|
||||
_chatDrafts[_draftKeyForTarget(_inlineChatTarget!)] = text;
|
||||
},
|
||||
onClose: _closeInlineChat,
|
||||
),
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Complete `_saveCurrentDraft` in `main.dart`**
|
||||
|
||||
The `_saveCurrentDraft` method is called from `_onOpenChat` (before switching) and `_closeInlineChat`. Since `ChatDetailView` emits drafts via `onDraftChanged` and `dispose`, the parent always has the latest draft in `_chatDrafts`. The method body stays as a no-op safety net:
|
||||
|
||||
```dart
|
||||
void _saveCurrentDraft() {
|
||||
// Drafts are continuously saved via onDraftChanged callback.
|
||||
// This method exists as an explicit save point for any future
|
||||
// snapshot-based draft capture.
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run `flutter analyze`**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter analyze`
|
||||
Expected: No new errors
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/chanora_flutter/lib/widgets/chat_views.dart apps/chanora_flutter/lib/widgets/chat_panel.dart apps/chanora_flutter/lib/main.dart
|
||||
git commit -m "feat(chat): per-target draft persistence with save/restore on switch"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Add unread dot indicator to channel tiles in `SnapshotView`
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/chanora_flutter/lib/widgets/snapshot_view.dart` (add unread indicator)
|
||||
- Modify: `apps/chanora_flutter/lib/main.dart` (pass unread channel set)
|
||||
|
||||
- [ ] **Step 1: Add unread channel IDs parameter to `SnapshotView`**
|
||||
|
||||
Add a new required field to `SnapshotView` (after `canJoinVoiceChannel` around line 57):
|
||||
|
||||
```dart
|
||||
/// Set of channel IDs that have unread chat messages.
|
||||
final Set<BigInt> unreadChannelIds;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add unread dot to `_channelTile`**
|
||||
|
||||
In `_channelTile`, inside the `Row` children (after the channel name `Expanded` widget, around line 273), add an unread dot:
|
||||
|
||||
```dart
|
||||
// Unread indicator.
|
||||
if (widget.unreadChannelIds.contains(channel.id)) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
```
|
||||
|
||||
This must come before the password lock icon check (line 274).
|
||||
|
||||
- [ ] **Step 3: Compute unread channel set in `main.dart`**
|
||||
|
||||
Add a getter in `_BetaHomeState` that computes which channels have unread messages:
|
||||
|
||||
```dart
|
||||
/// Channel IDs that have unread chat messages (used for dot indicators).
|
||||
Set<BigInt> get _unreadChannelIds {
|
||||
if (_chatOpen) return const {};
|
||||
final ids = <BigInt>{};
|
||||
for (final entry in _chatMessages) {
|
||||
if (!entry.countsTowardUnread || entry.isSelf) continue;
|
||||
if (entry.target case rust.BridgeMessageTarget_Channel(:final id)) {
|
||||
ids.add(id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Pass unread channel set to `SnapshotView`**
|
||||
|
||||
In the `SnapshotView(...)` constructor around line 2512, add:
|
||||
|
||||
```dart
|
||||
unreadChannelIds: _unreadChannelIds,
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run `flutter analyze`**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter analyze`
|
||||
Expected: No new errors
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/chanora_flutter/lib/widgets/snapshot_view.dart apps/chanora_flutter/lib/main.dart
|
||||
git commit -m "feat(chat): unread dot indicator on channel tiles with unread messages"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: End-to-end verification
|
||||
|
||||
**Files:** All modified files.
|
||||
|
||||
- [ ] **Step 1: Run `flutter analyze` on the full project**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter analyze`
|
||||
Expected: Zero issues
|
||||
|
||||
- [ ] **Step 2: Run `flutter test`**
|
||||
|
||||
Run: `cd apps/chanora_flutter && flutter test`
|
||||
Expected: All tests pass (same baseline as before — 180 passed, 2 skipped)
|
||||
|
||||
- [ ] **Step 3: Build macOS release**
|
||||
|
||||
Run: `bash tools/build-macos.sh`
|
||||
Expected: Successful build producing `chanora-v0.2.0-beta.1-macos-aarch64.zip`
|
||||
|
||||
- [ ] **Step 4: Manual QA checklist**
|
||||
|
||||
Launch the app and verify:
|
||||
|
||||
1. **Channel → chat switching**: With window ≥1024dp and chat panel open showing Server chat, click a channel in the tree. The chat panel should switch to that channel's chat (messages filter to that channel). Voice join should also happen.
|
||||
|
||||
2. **Channel right-click → Chat**: Right-click a channel → "Open chat". Chat panel should switch to that channel's chat without joining voice.
|
||||
|
||||
3. **Header chat button toggle**: With chat panel open showing a DM, click the header chat button. It should switch to the current voice channel's chat.
|
||||
|
||||
4. **Draft persistence**: Type "hello" in chat input but don't send. Click a different channel. Type "world" in that channel's chat. Switch back to the first channel. The input should show "hello".
|
||||
|
||||
5. **Close and reopen**: Close the chat panel. Click the header chat button. It should reopen to the last conversation with the draft intact.
|
||||
|
||||
6. **Unread dots**: Close the chat panel. Have someone send a message to a specific channel. That channel in the tree should show a blue dot.
|
||||
|
||||
7. **Medium/compact unchanged**: Narrow the window below 1024dp. Open chat. It should still push a full-screen route as before. No regressions.
|
||||
|
||||
8. **DM switching still works**: Right-click a client → "Direct Message". Chat panel should switch to that DM. Right-click another client → "Direct Message". Should switch again.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
### Spec coverage
|
||||
|
||||
| Requirement | Task |
|
||||
|---|---|
|
||||
| Channel → chat switching (tap) | Task 2 (wiring) + Task 3 (target resolution) |
|
||||
| Channel → chat (context menu) | Task 1 |
|
||||
| Header button switches when open | Task 3 |
|
||||
| Per-target draft persistence | Task 4 |
|
||||
| Close = dismiss (remember state) | Task 3 |
|
||||
| Unread dot on channels | Task 5 |
|
||||
| Medium/compact unchanged | No changes to those paths |
|
||||
|
||||
### Placeholder scan
|
||||
No TBD, TODO, or placeholder steps found. All code blocks contain complete implementations.
|
||||
|
||||
### Type consistency
|
||||
- `BridgeMessageTarget.channel(id)` uses `BigInt` — matches `channel.id` type
|
||||
- `_chatDrafts` uses `String` keys from `_draftKeyForTarget` — consistent
|
||||
- `onOpenChannelChat` callback type `ValueChanged<rust.BridgeChannel>?` — matches widget pattern
|
||||
- `unreadChannelIds` uses `Set<BigInt>` — matches channel ID type
|
||||
@@ -0,0 +1,95 @@
|
||||
# Core Internal Split Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Improve `chanora_core` maintainability by moving stable event DTOs and network diagnostic state out of the oversized `lib.rs` while preserving the public crate Interface.
|
||||
|
||||
**Architecture:** Keep `lib.rs` as the public Interface and session orchestration entry point. Move event-facing DTOs to `events.rs` and private network diagnostic ring-buffer state to `network_diagnostics.rs`; re-export public event types from `lib.rs` so downstream callers keep using `chanora_core::SessionEvent` and related names unchanged.
|
||||
|
||||
**Tech Stack:** Rust 2021, Tokio broadcast/watch channels, `thiserror`, existing Cargo workspace tests.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Move Core Event DTOs
|
||||
|
||||
**Files:**
|
||||
- Create: `core/chanora_core/src/events.rs`
|
||||
- Modify: `core/chanora_core/src/lib.rs`
|
||||
- Verify: `cargo test -p chanora_core`
|
||||
|
||||
- [ ] **Step 1: Preserve current public Interface with tests**
|
||||
|
||||
Run: `cargo test -p chanora_core`
|
||||
Expected: PASS. Existing bridge-facing tests and compile checks prove the current public event names are valid.
|
||||
|
||||
- [ ] **Step 2: Create `events.rs` with the moved public event DTOs**
|
||||
|
||||
Move these exact public types from `lib.rs` to `events.rs`:
|
||||
- `PttDescriptorSnapshot`
|
||||
- `PersistedPttBinding`
|
||||
- `SessionEvent`
|
||||
- `VoiceJoinSyncState`
|
||||
- `VoiceJoinErrorCode`
|
||||
- `NetworkState`
|
||||
|
||||
Use local imports in `events.rs` for `chanora_audio::{AudioRoute, PttBackendDescriptor}` and `chanora_protocol::MessageTarget`.
|
||||
|
||||
- [ ] **Step 3: Re-export moved types from `lib.rs`**
|
||||
|
||||
Add `mod events;` and `pub use events::{...};` for all moved public types. Remove the original definitions from `lib.rs`.
|
||||
|
||||
- [ ] **Step 4: Run tests**
|
||||
|
||||
Run: `cargo test -p chanora_core`
|
||||
Expected: PASS with no public Interface break.
|
||||
|
||||
### Task 2: Move Core Network Diagnostics State
|
||||
|
||||
**Files:**
|
||||
- Create: `core/chanora_core/src/network_diagnostics.rs`
|
||||
- Modify: `core/chanora_core/src/lib.rs`
|
||||
- Verify: `cargo test -p chanora_core network_diagnostics`
|
||||
|
||||
- [ ] **Step 1: Move `NetworkDiagnostics` into a private module**
|
||||
|
||||
Move the private `NetworkDiagnostics` struct and its methods from `lib.rs` into `network_diagnostics.rs`. Keep methods `pub(crate)` because `ChanoraSession` records connection/loss events and exports summaries.
|
||||
|
||||
- [ ] **Step 2: Move the regression test with the module**
|
||||
|
||||
Move `network_diagnostics_keeps_last_eight_loss_reasons` from the `lib.rs` test module into `network_diagnostics.rs` so the behaviour test lives next to the Implementation it protects.
|
||||
|
||||
- [ ] **Step 3: Import the private module from `lib.rs`**
|
||||
|
||||
Add `mod network_diagnostics;` and `use network_diagnostics::NetworkDiagnostics;`. Remove `VecDeque` from the `lib.rs` imports.
|
||||
|
||||
- [ ] **Step 4: Run targeted and workspace verification**
|
||||
|
||||
Run: `cargo test -p chanora_core network_diagnostics`
|
||||
Expected: PASS.
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: PASS.
|
||||
|
||||
### Task 3: Format and Check Workspace
|
||||
|
||||
**Files:**
|
||||
- Modify: Rust files touched above only, except existing formatter-only churn may remain from prior `cargo fmt --all`.
|
||||
|
||||
- [ ] **Step 1: Format Rust code**
|
||||
|
||||
Run: `cargo fmt --all`
|
||||
Expected: no command output.
|
||||
|
||||
- [ ] **Step 2: Compile workspace**
|
||||
|
||||
Run: `cargo check --workspace`
|
||||
Expected: finishes successfully.
|
||||
|
||||
- [ ] **Step 3: Inspect diff**
|
||||
|
||||
Run: `git diff --stat`
|
||||
Expected: new `events.rs` and `network_diagnostics.rs`; smaller `core/chanora_core/src/lib.rs`; no public API renames.
|
||||
|
||||
---
|
||||
|
||||
Self-review: This plan covers the recommended Core split first slice, avoids public Interface changes, has no placeholders, and keeps testing tied to the moved Implementations.
|
||||
@@ -0,0 +1,608 @@
|
||||
# Maintainability Continuation Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Continue the current maintainability review with safe simplifications, full code-review remediation, current documentation, explicit fail-safe evidence, and Android runtime verification status.
|
||||
|
||||
**Current status:** Task 0 and the audio-realtime portion of Task 0.1 have landed in commits `d835394` and `8606eb4`. Task 0.2 is a documentation/status alignment slice only; it must not claim Android or iOS runtime success.
|
||||
|
||||
**Architecture:** Treat the existing uncommitted maintainability changes as the baseline slice. Fix safety and hidden-bug findings before broad Module splits. Preserve public Rust Core, Bridge, Protocol, Audio, and Flutter responsibilities while applying only tested simplifications and risk-reducing refactors. Record larger seam decisions as follow-up findings unless a huge Module must be split to make a safety fix testable.
|
||||
|
||||
**Tech Stack:** Rust 2021 Cargo workspace, Flutter/Dart 3.11, Flutter Rust Bridge 2.12, Android ADB, Markdown governance and verification documents.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
Implementation should keep the following responsibilities stable:
|
||||
|
||||
- `core/chanora_core/src/lib.rs`: public Core API, session orchestration, public re-exports, and integration-facing methods.
|
||||
- `core/chanora_core/src/events.rs`: Core public event DTOs re-exported by `lib.rs`.
|
||||
- `core/chanora_core/src/network_diagnostics.rs`: private bounded network diagnostic history and its local regression tests.
|
||||
- `crates/chanora_audio/src/voice_render.rs`: shared render/downmix helpers.
|
||||
- `crates/chanora_audio/src/ios_raw_unit.rs`, `ios_voice_unit.rs`, `android_voice_unit.rs`, `engine.rs`: platform/audio backends; avoid broad rewrites without device evidence.
|
||||
- `crates/chanora_audio/src/ptt_backends/mod.rs`: PTT backend descriptor and error definitions.
|
||||
- `crates/chanora_state/src/lib.rs`: snapshot reducers and state deltas.
|
||||
- `crates/chanora_protocol/src/adapter.rs`: protocol adapter event ordering and DTO projection.
|
||||
- `crates/chanora_bridge/src/api.rs`: bridge-facing API and DTO mapping; generated files are not manually edited.
|
||||
- `crates/chanora_diagnostics/src/lib.rs`: bounded diagnostics, redaction, and export data.
|
||||
- `docs/governance/maintainability-review-2026-06-08.md`: working review record and fail-safe gap log.
|
||||
- `docs/governance/document-index.md`: navigation index for review records.
|
||||
- `docs/architecture/sad.md`, `docs/architecture/sdd.md`: architecture updates for seams and implementation boundaries.
|
||||
- `docs/implementation-status-2026-05-28.md`: implementation status updates.
|
||||
- `docs/verification/swe4-unit-verification-plan.md`, `docs/verification/swe5-software-integration-verification-plan.md`: verification evidence and requirements updates.
|
||||
|
||||
## Review Findings To Remediate First
|
||||
|
||||
The full review added these priority fixes before the original maintainability cleanup sequence:
|
||||
|
||||
- Flutter privacy fail-safe: talk-power recovery must not clear a user/manual mute. Fixed in commit `d835394`.
|
||||
- Flutter stuck-transmit fail-safe: touch PTT must release when disposed while held. Fixed in commit `d835394`.
|
||||
- Flutter/iOS fail-safe: iOS audio-session activation errors must be caught, and activation should be moved before Rust VoiceProcessingIO startup where the current app flow allows. Missing-plugin/error hardening fixed in commit `d835394`; iOS device runtime verification remains required.
|
||||
- Rust realtime safety: Android and iOS raw render-reference buffers must not use unsynchronized mutable aliasing. Callback-path hardening fixed in commit `8606eb4`; the full lock-free `AudioHandler` / config / debug-recorder redesign remains a follow-up.
|
||||
- Rust realtime safety: Android input callback must not block on a mutex, and non-48 kHz capture should not allocate/clone per callback. Focused callback-path hardening fixed in commit `8606eb4`; Android target compilation and runtime verification remain blocked locally until the missing NDK compiler and an authorized ADB target are available.
|
||||
- Rust control-plane safety: disconnect and protocol control requests must remain bounded under broken transport or sustained voice traffic.
|
||||
- Governance correctness: README/release/VAD/Android API/product-decision docs must not contradict code or verification status.
|
||||
|
||||
## Task 0: Fix Flutter Privacy and Stuck-Transmit Fail-Safes
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/chanora_flutter/lib/main.dart`
|
||||
- Modify: `apps/chanora_flutter/lib/widgets/voice_compact.dart`
|
||||
- Modify: `apps/chanora_flutter/lib/services/ios_audio_session_controller.dart`
|
||||
- Test: `apps/chanora_flutter/test/widgets/voice_compact_test.dart`
|
||||
- Test: existing Flutter tests under `apps/chanora_flutter/test/`
|
||||
|
||||
- [ ] **Step 1: Add failing touch PTT disposal regression test**
|
||||
|
||||
Create or update `apps/chanora_flutter/test/widgets/voice_compact_test.dart` with a widget test that presses the touch PTT button, replaces the widget without sending pointer-up, and expects the callback sequence `[true, false]`.
|
||||
|
||||
- [ ] **Step 2: Run touch PTT test and verify RED**
|
||||
|
||||
Run from `apps/chanora_flutter`: `flutter test test/widgets/voice_compact_test.dart`
|
||||
|
||||
Expected before production fix: FAIL because disposal does not emit `false`.
|
||||
|
||||
- [ ] **Step 3: Implement touch PTT release-on-dispose**
|
||||
|
||||
Add `dispose()` to the touch PTT button state so an active press calls `widget.onHeldChanged(false)` exactly once before disposal.
|
||||
|
||||
- [ ] **Step 4: Run touch PTT test and verify GREEN**
|
||||
|
||||
Run from `apps/chanora_flutter`: `flutter test test/widgets/voice_compact_test.dart`
|
||||
|
||||
Expected after fix: PASS.
|
||||
|
||||
- [ ] **Step 5: Add or preserve mute-owner regression coverage**
|
||||
|
||||
If an existing pure reducer seam is available, add a failing test for manual mute true -> talk power blocked -> talk power restored. If no testable seam exists, first extract the smallest voice mute owner helper from `main.dart` and test it directly.
|
||||
|
||||
- [ ] **Step 6: Implement independent mute owners**
|
||||
|
||||
Ensure talk-power recovery clears only the talk-power owner and does not clear manual/user mute or permission mute. Effective hard mute is the OR of manual, permission, and talk-power owners.
|
||||
|
||||
- [ ] **Step 7: Harden iOS audio-session controller errors**
|
||||
|
||||
Add tests for `MissingPluginException` in `ios_audio_session_controller_test.dart`, then catch `MissingPluginException` or `Object` so activation/deactivation failures do not become unhandled async errors.
|
||||
|
||||
- [ ] **Step 8: Run focused Flutter verification**
|
||||
|
||||
Run from `apps/chanora_flutter`: `flutter test test/widgets/voice_compact_test.dart test/services/ios_audio_session_controller_test.dart && flutter analyze`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 9: Commit Flutter fail-safe slice**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add apps/chanora_flutter/lib/main.dart apps/chanora_flutter/lib/widgets/voice_compact.dart apps/chanora_flutter/lib/services/ios_audio_session_controller.dart apps/chanora_flutter/test/widgets/voice_compact_test.dart apps/chanora_flutter/test/services/ios_audio_session_controller_test.dart
|
||||
git commit -m "fix(voice): preserve mute owners and release touch ptt"
|
||||
```
|
||||
|
||||
Expected: one commit containing only Flutter fail-safe fixes and tests.
|
||||
|
||||
## Task 0.1: Fix Rust Realtime and Control-Plane Safety Findings
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/chanora_audio/src/android_voice_unit.rs`
|
||||
- Modify: `crates/chanora_audio/src/ios_raw_unit.rs`
|
||||
- Modify: `crates/chanora_audio/src/engine.rs`
|
||||
- Modify: `crates/chanora_protocol/src/adapter.rs`
|
||||
- Modify: `core/chanora_core/src/lib.rs`
|
||||
- Test: Rust tests in affected crates
|
||||
|
||||
- [ ] **Step 1: Add failing bounded-buffer regression for render-reference handoff**
|
||||
|
||||
Add host-testable unit coverage around the render-reference buffer behavior so a writer can publish a frame and a reader can read a complete latest frame without unsynchronized mutation.
|
||||
|
||||
- [ ] **Step 2: Replace unsafe shared mutable render-reference buffers**
|
||||
|
||||
Replace unsynchronized mutable aliasing in Android and iOS raw render-reference buffers with a realtime-safe handoff such as an `ArrayQueue` of complete frames or a documented atomic double-buffer. Do not add mutex locking to realtime callbacks.
|
||||
|
||||
- [ ] **Step 3: Add failing protocol progress regression where feasible**
|
||||
|
||||
Add or isolate a test proving control requests are not starved by sustained voice packet drain.
|
||||
|
||||
- [ ] **Step 4: Bound voice draining and disconnect shutdown**
|
||||
|
||||
Cap voice packet draining per protocol loop and make disconnect/shutdown bounded so UI/Core locks are not held across unbounded transport waits.
|
||||
|
||||
- [ ] **Step 5: Run focused Rust verification**
|
||||
|
||||
Run: `cargo test -p chanora_audio -p chanora_protocol -p chanora_core`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit Rust safety slice**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add crates/chanora_audio/src/android_voice_unit.rs crates/chanora_audio/src/ios_raw_unit.rs crates/chanora_audio/src/engine.rs crates/chanora_protocol/src/adapter.rs core/chanora_core/src/lib.rs
|
||||
git commit -m "fix(audio): harden realtime and protocol fail-safes"
|
||||
```
|
||||
|
||||
Expected: one commit containing only Rust safety fixes and tests.
|
||||
|
||||
## Task 0.2: Align Review Findings With Specs, Plans, and Governance Docs
|
||||
|
||||
**Status:** In progress / documentation-only alignment. Do not commit from this task unless explicitly requested.
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/specs/2026-06-08-maintainability-continuation-design.md`
|
||||
- Modify: `docs/superpowers/plans/2026-06-08-maintainability-continuation.md`
|
||||
- Modify: `docs/governance/maintainability-review-2026-06-08.md`
|
||||
- Modify: `README.md`
|
||||
- Modify if needed: `CHANGELOG.md`
|
||||
- Modify if needed: `docs/governance/product-decision-register.md`
|
||||
- Modify if needed: `docs/release/release-readiness-go-nogo-record.md`
|
||||
- Modify if needed: `docs/release/dv-waiver-register.md`
|
||||
- Modify if needed: `docs/verification/verification-master-plan.md`
|
||||
- Modify if needed: `docs/verification/sys4-system-integration-verification-plan.md`
|
||||
|
||||
- [x] **Step 1: Record full-review findings in maintainability review**
|
||||
|
||||
Update `docs/governance/maintainability-review-2026-06-08.md` with the full code-review findings, fixed items, blocked items, and follow-up Module split candidates.
|
||||
|
||||
- [x] **Step 2: Fix stale platform/release claims**
|
||||
|
||||
Update Android minimum runtime claims to API 28 where code and requirements require it. Update release metadata so Flutter app version/build and Rust workspace version are clearly distinguished.
|
||||
|
||||
- [x] **Step 3: Clarify VAD/VoiceActivity status**
|
||||
|
||||
Document the difference between VAD scaffolding/assets/tests and product-enabled VoiceActivity behavior. Do not claim runtime VoiceActivity is shipped unless verified.
|
||||
|
||||
- [x] **Step 4: Promote Android runtime verification blocker**
|
||||
|
||||
Add Android ADB/build/install/smoke as a blocker or waiver in governing release/verification docs when no authorized target is connected.
|
||||
|
||||
- [ ] **Step 5: Commit plan/spec/governance alignment slice**
|
||||
|
||||
Skipped in this subagent run because the instruction for Task 0.2 explicitly says not to commit.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/specs/2026-06-08-maintainability-continuation-design.md docs/superpowers/plans/2026-06-08-maintainability-continuation.md docs/governance/maintainability-review-2026-06-08.md README.md CHANGELOG.md docs/governance/product-decision-register.md docs/release/release-readiness-go-nogo-record.md docs/release/dv-waiver-register.md docs/verification/verification-master-plan.md docs/verification/sys4-system-integration-verification-plan.md
|
||||
git commit -m "docs: align review findings and verification gates"
|
||||
```
|
||||
|
||||
Expected: one documentation/governance commit, with unavailable optional files omitted only if unchanged.
|
||||
|
||||
## Task 1: Verify Current Branch Baseline
|
||||
|
||||
**Files:**
|
||||
- Read: `docs/governance/maintainability-review-2026-06-08.md`
|
||||
- Read: `docs/superpowers/plans/2026-06-08-core-internal-split.md`
|
||||
- Inspect: all currently modified files from `git status --short`
|
||||
- Modify: none unless verification exposes a small mechanical fix
|
||||
|
||||
- [ ] **Step 1: Inspect current status**
|
||||
|
||||
Run: `git status --short`
|
||||
|
||||
Expected: output includes the existing maintainability branch changes and no staged files from unrelated work.
|
||||
|
||||
- [ ] **Step 2: Inspect current diff summary**
|
||||
|
||||
Run: `git diff --stat`
|
||||
|
||||
Expected: diff remains focused on Core split, Audio simplifications, State/Protocol/Bridge/Diagnostics cleanup, and documentation updates.
|
||||
|
||||
- [ ] **Step 3: Verify Rust formatting**
|
||||
|
||||
Run: `cargo fmt --all --check`
|
||||
|
||||
Expected: PASS with no output. If it fails, run `cargo fmt --all`, inspect the resulting diff, and include formatter-only changes in the smallest relevant commit.
|
||||
|
||||
- [ ] **Step 4: Verify Rust compilation**
|
||||
|
||||
Run: `cargo check --workspace`
|
||||
|
||||
Expected: PASS for the full workspace.
|
||||
|
||||
- [ ] **Step 5: Verify Rust tests**
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
|
||||
Expected: PASS for the full workspace.
|
||||
|
||||
- [ ] **Step 6: Verify Flutter analysis**
|
||||
|
||||
Run from `apps/chanora_flutter`: `flutter analyze`
|
||||
|
||||
Expected: PASS with no new analyzer errors.
|
||||
|
||||
- [ ] **Step 7: Verify Flutter tests**
|
||||
|
||||
Run from `apps/chanora_flutter`: `flutter test --exclude-tags e2e`
|
||||
|
||||
Expected: PASS for non-e2e Flutter tests.
|
||||
|
||||
- [ ] **Step 8: Check Android device availability**
|
||||
|
||||
Run: `adb devices -l`
|
||||
|
||||
Expected if a target is connected: at least one `device` row. Expected if no target is connected: only the header and no `device` row; record Android runtime verification as blocked in `docs/governance/maintainability-review-2026-06-08.md`.
|
||||
|
||||
- [ ] **Step 9: Commit verified existing slice**
|
||||
|
||||
Only after Steps 1-8 have been completed or any blocked Android status has been documented, stage the smallest coherent existing slice.
|
||||
|
||||
Recommended first slice if tests pass:
|
||||
|
||||
```bash
|
||||
git add core/chanora_core/src/lib.rs core/chanora_core/src/events.rs core/chanora_core/src/network_diagnostics.rs docs/superpowers/plans/2026-06-08-core-internal-split.md
|
||||
git commit -m "refactor(core): split event and diagnostics internals"
|
||||
```
|
||||
|
||||
Expected: one commit containing only the Core split and its plan.
|
||||
|
||||
## Task 2: Commit Existing Built-In and Helper Reuse Simplifications
|
||||
|
||||
**Files:**
|
||||
- Modify or stage: `crates/chanora_audio/src/ptt_backends/mod.rs`
|
||||
- Modify or stage: `crates/chanora_audio/src/voice_render.rs`
|
||||
- Modify or stage: `crates/chanora_audio/src/ios_raw_unit.rs`
|
||||
- Modify or stage: `crates/chanora_diagnostics/src/lib.rs`
|
||||
- Modify or stage: `crates/chanora_state/src/lib.rs`
|
||||
- Modify or stage: `crates/chanora_resolver/Cargo.toml`
|
||||
- Modify or stage: `Cargo.lock`
|
||||
|
||||
- [ ] **Step 1: Inspect simplification diffs**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff -- crates/chanora_audio/src/ptt_backends/mod.rs crates/chanora_audio/src/voice_render.rs crates/chanora_audio/src/ios_raw_unit.rs crates/chanora_diagnostics/src/lib.rs crates/chanora_state/src/lib.rs crates/chanora_resolver/Cargo.toml Cargo.lock
|
||||
```
|
||||
|
||||
Expected: diffs show built-in/helper reuse only: `thiserror::Error`, `VecDeque`, render helper reuse, reducer reuse, and workspace metadata inheritance.
|
||||
|
||||
- [ ] **Step 2: Run focused Rust tests for changed areas**
|
||||
|
||||
Run: `cargo test -p chanora_audio -p chanora_diagnostics -p chanora_state -p chanora_resolver`
|
||||
|
||||
Expected: PASS for all listed crates.
|
||||
|
||||
- [ ] **Step 3: Run workspace Rust verification**
|
||||
|
||||
Run: `cargo check --workspace && cargo test --workspace`
|
||||
|
||||
Expected: PASS for workspace compile and tests.
|
||||
|
||||
- [ ] **Step 4: Commit built-in/helper reuse slice**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add crates/chanora_audio/src/ptt_backends/mod.rs crates/chanora_audio/src/voice_render.rs crates/chanora_audio/src/ios_raw_unit.rs crates/chanora_diagnostics/src/lib.rs crates/chanora_state/src/lib.rs crates/chanora_resolver/Cargo.toml Cargo.lock
|
||||
git commit -m "refactor: reuse built-ins and shared helpers"
|
||||
```
|
||||
|
||||
Expected: one commit containing only the built-in/helper reuse simplifications.
|
||||
|
||||
## Task 3: Review Remaining Audio Platform Diffs Before Committing
|
||||
|
||||
**Files:**
|
||||
- Inspect: `crates/chanora_audio/src/android_voice_unit.rs`
|
||||
- Inspect: `crates/chanora_audio/src/engine.rs`
|
||||
- Inspect: `crates/chanora_audio/src/ios_voice_unit.rs`
|
||||
- Modify: `docs/governance/maintainability-review-2026-06-08.md` if Android runtime verification is blocked or audio fail-safe evidence changes
|
||||
|
||||
- [ ] **Step 1: Inspect platform-audio diffs**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff -- crates/chanora_audio/src/android_voice_unit.rs crates/chanora_audio/src/engine.rs crates/chanora_audio/src/ios_voice_unit.rs
|
||||
```
|
||||
|
||||
Expected: diffs are understandable as local simplification or fail-safe improvements. If a diff changes platform runtime behavior and no Android/iOS device evidence is available, keep it separate from non-platform commits.
|
||||
|
||||
- [ ] **Step 2: Run audio crate tests**
|
||||
|
||||
Run: `cargo test -p chanora_audio`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Check Android runtime availability**
|
||||
|
||||
Run: `adb devices -l`
|
||||
|
||||
Expected if connected: at least one target row with `device`. Expected if blocked: no target row.
|
||||
|
||||
- [ ] **Step 4: Run Android build when a target is available**
|
||||
|
||||
Run from `apps/chanora_flutter`: `flutter build apk --debug`
|
||||
|
||||
Expected: PASS and a debug APK is produced.
|
||||
|
||||
- [ ] **Step 5: Run Android install and smoke when a target is available**
|
||||
|
||||
Run from `apps/chanora_flutter`: `flutter install`
|
||||
|
||||
Expected: PASS and app installs on the connected target.
|
||||
|
||||
Manual smoke expectations:
|
||||
|
||||
- App launches without crash.
|
||||
- Permission UI can be reached.
|
||||
- Audio device/permission screen does not crash.
|
||||
- Voice controls remain responsive.
|
||||
|
||||
- [ ] **Step 6: Record blocked Android evidence if no target is available**
|
||||
|
||||
Modify `docs/governance/maintainability-review-2026-06-08.md` Section 6 so it includes the exact `adb devices -l` result and states Android runtime verification is blocked until a device or emulator is connected.
|
||||
|
||||
- [ ] **Step 7: Commit platform-audio slice**
|
||||
|
||||
If Android runtime was verified, run:
|
||||
|
||||
```bash
|
||||
git add crates/chanora_audio/src/android_voice_unit.rs crates/chanora_audio/src/engine.rs crates/chanora_audio/src/ios_voice_unit.rs docs/governance/maintainability-review-2026-06-08.md
|
||||
git commit -m "refactor(audio): simplify platform voice internals"
|
||||
```
|
||||
|
||||
If Android runtime was blocked, run:
|
||||
|
||||
```bash
|
||||
git add crates/chanora_audio/src/android_voice_unit.rs crates/chanora_audio/src/engine.rs crates/chanora_audio/src/ios_voice_unit.rs docs/governance/maintainability-review-2026-06-08.md
|
||||
git commit -m "refactor(audio): simplify platform voice internals"
|
||||
```
|
||||
|
||||
Expected: commit message is the same, but the maintainability review explicitly records the blocked Android runtime evidence.
|
||||
|
||||
## Task 4: Review Protocol and Bridge Diffs as One Boundary Slice
|
||||
|
||||
**Files:**
|
||||
- Inspect: `crates/chanora_protocol/src/adapter.rs`
|
||||
- Inspect: `crates/chanora_bridge/src/api.rs`
|
||||
- Modify: `docs/architecture/sad.md`
|
||||
- Modify: `docs/architecture/sdd.md`
|
||||
- Modify: `docs/governance/maintainability-review-2026-06-08.md`
|
||||
|
||||
- [ ] **Step 1: Inspect protocol/bridge diffs**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff -- crates/chanora_protocol/src/adapter.rs crates/chanora_bridge/src/api.rs
|
||||
```
|
||||
|
||||
Expected: diffs preserve protocol isolation and bridge DTO shape unless bridge generation and Flutter tests are included.
|
||||
|
||||
- [ ] **Step 2: Verify protocol tests**
|
||||
|
||||
Run: `cargo test -p chanora_protocol`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Verify bridge tests and compile**
|
||||
|
||||
Run: `cargo test -p chanora_bridge`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Verify Flutter after bridge/API changes**
|
||||
|
||||
Run from `apps/chanora_flutter`: `flutter analyze && flutter test --exclude-tags e2e`
|
||||
|
||||
Expected: PASS for analyzer and non-e2e tests.
|
||||
|
||||
- [ ] **Step 5: Document seam findings**
|
||||
|
||||
Update `docs/governance/maintainability-review-2026-06-08.md` so remaining bridge DTO drift and protocol voice packet seam risks are listed under fail-safe gaps or follow-up opportunities.
|
||||
|
||||
- [ ] **Step 6: Update architecture docs if seam wording changed**
|
||||
|
||||
If the code diff clarifies protocol/bridge boundaries, update `docs/architecture/sad.md` and `docs/architecture/sdd.md` with one concise note each. The note should state whether protocol voice packet handling is an intentional exception and whether bridge DTO mirrors remain required by Flutter Rust Bridge.
|
||||
|
||||
- [ ] **Step 7: Commit protocol/bridge slice**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add crates/chanora_protocol/src/adapter.rs crates/chanora_bridge/src/api.rs docs/architecture/sad.md docs/architecture/sdd.md docs/governance/maintainability-review-2026-06-08.md
|
||||
git commit -m "refactor: clarify protocol bridge boundaries"
|
||||
```
|
||||
|
||||
Expected: one commit for protocol/bridge boundary cleanup plus matching architecture documentation.
|
||||
|
||||
## Task 5: Run a Second-Pass Simplification Search
|
||||
|
||||
**Files:**
|
||||
- Inspect: Rust and Dart source files only
|
||||
- Modify: only if the simplification is mechanical, local, and covered by tests
|
||||
- Modify: `docs/governance/maintainability-review-2026-06-08.md`
|
||||
|
||||
- [ ] **Step 1: Search for custom queue front removal**
|
||||
|
||||
Run: `rg "remove\(0\)|removeAt\(0\)" core crates apps/chanora_flutter/lib apps/chanora_flutter/test`
|
||||
|
||||
Expected: no results. If results exist, replace with `VecDeque` in Rust or a clearer Dart queue structure only when behavior is covered by a local test.
|
||||
|
||||
- [ ] **Step 2: Search for manual Rust error formatting**
|
||||
|
||||
Run: `rg "impl (std::fmt::)?Display for .*Error|impl std::error::Error for" core crates`
|
||||
|
||||
Expected: only intentional manual implementations remain. For simple enum error types, replace with `thiserror::Error` and add or preserve tests for user-facing strings.
|
||||
|
||||
- [ ] **Step 3: Search for duplicate mono downmix loops**
|
||||
|
||||
Run: `rg "chunks_exact\(2\)|downmix|mono" crates/chanora_audio/src`
|
||||
|
||||
Expected: duplicate i16/f32 mono downmix code is either absent or justified. If a duplicate remains, route it through an existing helper and run `cargo test -p chanora_audio`.
|
||||
|
||||
- [ ] **Step 4: Search for shallow modules worth documenting**
|
||||
|
||||
Run: `rg "^pub struct|^pub enum|^pub fn|^fn" crates/chanora_audio/src/processor crates/chanora_prefetch/src crates/chanora_resolver/src core/chanora_core/src`
|
||||
|
||||
Expected: identify candidates, but do not merge modules in this task. Record speculative merges in `docs/governance/maintainability-review-2026-06-08.md` unless a candidate is trivial and already covered by tests.
|
||||
|
||||
- [ ] **Step 5: Commit second-pass mechanical simplifications if any**
|
||||
|
||||
If code changed, run the relevant focused tests plus `cargo check --workspace && cargo test --workspace`, then inspect the changed files:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
|
||||
Stage only the files changed by the second-pass mechanical simplification. Example for a Rust-only diagnostics simplification:
|
||||
|
||||
```bash
|
||||
git add crates/chanora_diagnostics/src/lib.rs docs/governance/maintainability-review-2026-06-08.md
|
||||
git commit -m "refactor: apply second-pass mechanical simplifications"
|
||||
```
|
||||
|
||||
Expected: commit contains only local mechanical simplifications and the review record.
|
||||
|
||||
- [ ] **Step 6: Commit review-only findings if no code changed**
|
||||
|
||||
If no code changed and only findings were added, run:
|
||||
|
||||
```bash
|
||||
git add docs/governance/maintainability-review-2026-06-08.md
|
||||
git commit -m "docs: record maintainability follow-up findings"
|
||||
```
|
||||
|
||||
Expected: documentation-only commit.
|
||||
|
||||
## Task 6: Final Documentation Alignment
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/governance/maintainability-review-2026-06-08.md`
|
||||
- Modify: `docs/governance/document-index.md`
|
||||
- Modify: `docs/implementation-status-2026-05-28.md`
|
||||
- Modify: `docs/verification/swe4-unit-verification-plan.md`
|
||||
- Modify: `docs/verification/swe5-software-integration-verification-plan.md`
|
||||
- Modify if needed: `docs/architecture/sad.md`
|
||||
- Modify if needed: `docs/architecture/sdd.md`
|
||||
|
||||
- [ ] **Step 1: Update maintainability review completion status**
|
||||
|
||||
Edit `docs/governance/maintainability-review-2026-06-08.md` so these sections are current:
|
||||
|
||||
- Changes applied
|
||||
- Remaining simplification opportunities
|
||||
- Fail-safe gaps that need evidence
|
||||
- Verification policy
|
||||
- Android ADB status
|
||||
- Git policy
|
||||
|
||||
- [ ] **Step 2: Update document index**
|
||||
|
||||
Ensure `docs/governance/document-index.md` includes `docs/governance/maintainability-review-2026-06-08.md` and this plan/spec if the repository convention indexes superpowers documents.
|
||||
|
||||
- [ ] **Step 3: Update implementation status**
|
||||
|
||||
Ensure `docs/implementation-status-2026-05-28.md` describes maintainability review results without claiming production readiness.
|
||||
|
||||
- [ ] **Step 4: Update SWE.4 verification plan**
|
||||
|
||||
Ensure `docs/verification/swe4-unit-verification-plan.md` lists Rust unit verification expectations for Core, Audio, State, Protocol, Bridge, Diagnostics, Resolver, and Prefetch when those crates are touched.
|
||||
|
||||
- [ ] **Step 5: Update SWE.5 verification plan**
|
||||
|
||||
Ensure `docs/verification/swe5-software-integration-verification-plan.md` lists integration expectations for Bridge DTO drift, protocol event folding, Flutter analyze/test, and Android runtime smoke evidence.
|
||||
|
||||
- [ ] **Step 6: Run documentation cross-link search**
|
||||
|
||||
Run: `rg "maintainability-review-2026-06-08|core-internal-split|maintainability-continuation" docs README.md`
|
||||
|
||||
Expected: references point to existing files and no stale path is introduced.
|
||||
|
||||
- [ ] **Step 7: Commit final documentation alignment**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add docs/governance/maintainability-review-2026-06-08.md docs/governance/document-index.md docs/implementation-status-2026-05-28.md docs/verification/swe4-unit-verification-plan.md docs/verification/swe5-software-integration-verification-plan.md docs/architecture/sad.md docs/architecture/sdd.md
|
||||
git commit -m "docs: align maintainability verification records"
|
||||
```
|
||||
|
||||
Expected: one documentation-focused commit.
|
||||
|
||||
## Task 7: Final Verification and Android Evidence
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/governance/maintainability-review-2026-06-08.md` only if final verification status changes
|
||||
|
||||
- [ ] **Step 1: Run full Rust verification**
|
||||
|
||||
Run: `cargo fmt --all --check && cargo check --workspace && cargo test --workspace`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run full Flutter verification**
|
||||
|
||||
Run from `apps/chanora_flutter`: `flutter analyze && flutter test --exclude-tags e2e`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Run ADB check**
|
||||
|
||||
Run: `adb devices -l`
|
||||
|
||||
Expected if connected: at least one target row with `device`. Expected if blocked: no target row and the maintainability review states Android runtime verification is blocked.
|
||||
|
||||
- [ ] **Step 4: Run Android build/install/smoke when connected**
|
||||
|
||||
Run from `apps/chanora_flutter`: `flutter build apk --debug && flutter install`
|
||||
|
||||
Expected: PASS. Manually verify app launch, permission screen access, audio settings access, and voice control responsiveness.
|
||||
|
||||
- [ ] **Step 5: Commit final verification evidence if docs changed**
|
||||
|
||||
If the maintainability review was updated with final verification evidence, run:
|
||||
|
||||
```bash
|
||||
git add docs/governance/maintainability-review-2026-06-08.md
|
||||
git commit -m "docs: record maintainability verification evidence"
|
||||
```
|
||||
|
||||
Expected: one small evidence-only documentation commit.
|
||||
|
||||
- [ ] **Step 6: Inspect final history and status**
|
||||
|
||||
Run: `git status --short && git log --oneline -10`
|
||||
|
||||
Expected: no unexpected unstaged changes related to this work; recent commits are small and logically separated.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
Spec coverage:
|
||||
|
||||
- Safe simplifications are covered by Tasks 1, 2, 3, 4, and 5.
|
||||
- Built-in replacement opportunities are covered by Tasks 2 and 5.
|
||||
- Fail-safe gaps are covered by Tasks 3, 4, 6, and 7.
|
||||
- Rust, Flutter, and Android verification are covered by Tasks 1 and 7, with focused verification in Tasks 2 through 4.
|
||||
- Documentation updates are covered by Tasks 4, 6, and 7.
|
||||
- Small commit policy is covered by every task's dedicated commit step.
|
||||
|
||||
Placeholder scan: The plan contains no open placeholders. Task 5 uses `git status --short` before staging because the exact second-pass files are only known after the search runs; the example command shows the required staging style.
|
||||
|
||||
Type consistency: The plan does not introduce new APIs or types. It preserves current crate and file boundaries from the approved design.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,114 @@
|
||||
# DV Evidence Pack Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Create a document set that lets a DV meeting review Chanora's current verification posture, traceability, release blockers, waivers, and evidence without implying incomplete work is complete.
|
||||
|
||||
**Architecture:** The pack is documentation-only. Verification plans live under `docs/verification/`; release decision evidence lives under `docs/release/`; cross-document traceability lives under `docs/governance/`; security, privacy, and legal gate summaries live in their existing README-advertised folders.
|
||||
|
||||
**Tech Stack:** Markdown, existing SysRS/SysDes/SRS baselines, implementation status report, CI workflow definitions.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create Verification Plan Set
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/verification/verification-master-plan.md`
|
||||
- Create: `docs/verification/swe4-unit-verification-plan.md`
|
||||
- Create: `docs/verification/swe5-software-integration-verification-plan.md`
|
||||
- Create: `docs/verification/swe6-software-verification-plan.md`
|
||||
- Create: `docs/verification/sys4-system-integration-verification-plan.md`
|
||||
|
||||
- [x] **Step 1: Write master plan**
|
||||
|
||||
Create `docs/verification/verification-master-plan.md` with lifecycle scope, evidence rules, entry/exit criteria, current evidence sources, open gates, and reviewer decision framing.
|
||||
|
||||
- [x] **Step 2: Write SWE.4 plan**
|
||||
|
||||
Create `docs/verification/swe4-unit-verification-plan.md` with unit verification scope for Rust crates, Flutter services/widgets, diagnostics, storage, state reducers, audio DSP, and known unit-test gaps.
|
||||
|
||||
- [x] **Step 3: Write SWE.5 plan**
|
||||
|
||||
Create `docs/verification/swe5-software-integration-verification-plan.md` with cross-component integration scope for Flutter-bridge-core, protocol-state, audio-platform, secure storage, diagnostics export, resolver prefetch, and packaging hooks.
|
||||
|
||||
- [x] **Step 4: Write SWE.6 plan**
|
||||
|
||||
Create `docs/verification/swe6-software-verification-plan.md` with SRS-level acceptance scope and the SysRS-241 through SysRS-257 MVP acceptance matrix.
|
||||
|
||||
- [x] **Step 5: Write SYS.4 plan**
|
||||
|
||||
Create `docs/verification/sys4-system-integration-verification-plan.md` with system-level integration scope for external compatible servers, OS services, hardware, network, app stores, diagnostics, and release evidence.
|
||||
|
||||
### Task 2: Create Release and DV Decision Records
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/release/release-readiness-go-nogo-record.md`
|
||||
- Create: `docs/release/dv-waiver-register.md`
|
||||
|
||||
- [x] **Step 1: Write release readiness record**
|
||||
|
||||
Create `docs/release/release-readiness-go-nogo-record.md` with current candidate metadata, decision state, platform readiness, verification status, legal/security/privacy gates, blockers, and meeting recommendation.
|
||||
|
||||
- [x] **Step 2: Write waiver register**
|
||||
|
||||
Create `docs/release/dv-waiver-register.md` with explicit waivers for DEC-012, Android Keystore DEK, iOS voice-processing mode, source-build-only desktop/iOS artifacts, state reducer coverage, and VAD deferral.
|
||||
|
||||
### Task 3: Create Traceability and Gate Summaries
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/governance/traceability-matrix.md`
|
||||
- Create: `docs/security/security-privacy-legal-guideline.md`
|
||||
- Create: `docs/security/dependency-and-supply-chain-report.md`
|
||||
- Create: `docs/privacy/privacy-policy.md`
|
||||
- Create: `docs/legal/trademark-and-attribution-review.md`
|
||||
|
||||
- [x] **Step 1: Write traceability matrix**
|
||||
|
||||
Create `docs/governance/traceability-matrix.md` summarizing SysRS to SysDes to SRS to verification coverage, including the MVP acceptance and verification handoff items.
|
||||
|
||||
- [x] **Step 2: Write security/privacy/legal guideline**
|
||||
|
||||
Create `docs/security/security-privacy-legal-guideline.md` summarizing release gates and evidence expectations for secure storage, diagnostics redaction, dependency review, privacy policy, and affiliation wording.
|
||||
|
||||
- [x] **Step 3: Write dependency report**
|
||||
|
||||
Create `docs/security/dependency-and-supply-chain-report.md` summarizing current CI checks and open evidence gaps without claiming DEC-012 completion.
|
||||
|
||||
- [x] **Step 4: Write privacy policy baseline**
|
||||
|
||||
Create `docs/privacy/privacy-policy.md` as an engineering release-candidate privacy baseline covering local storage, permissions, diagnostics, and the no automatic telemetry posture.
|
||||
|
||||
- [x] **Step 5: Write trademark review**
|
||||
|
||||
Create `docs/legal/trademark-and-attribution-review.md` with current non-affiliation wording requirement and open legal sign-off state.
|
||||
|
||||
### Task 4: Verify Documentation Pack
|
||||
|
||||
**Files:**
|
||||
- Inspect all created files.
|
||||
|
||||
- [x] **Step 1: Search for forbidden sentinel text**
|
||||
|
||||
Run the sentinel-language scan over `docs/verification`, `docs/release`, `docs/governance`, `docs/security`, `docs/privacy`, and `docs/legal`.
|
||||
|
||||
Expected: no matches introduced by the DV evidence pack except intentional historical references in source documents outside these folders.
|
||||
|
||||
- [x] **Step 2: Confirm expected files exist**
|
||||
|
||||
Run: `ls docs/verification docs/release docs/governance docs/security docs/privacy docs/legal`
|
||||
|
||||
Expected: all DV evidence pack files are listed.
|
||||
|
||||
- [x] **Step 3: Inspect git status and diff**
|
||||
|
||||
Run: `git diff -- docs/verification docs/release docs/governance docs/security docs/privacy docs/legal docs/superpowers/plans/2026-05-29-dv-evidence-pack.md`
|
||||
|
||||
Expected: only intended Markdown additions are present.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage: covers verification plan set, release decision evidence, waiver register, traceability matrix, and minimum security/privacy/legal gate summaries.
|
||||
- Placeholder scan: plan contains no incomplete instructions.
|
||||
- Type consistency: document paths match the README-advertised folders and task file list.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Finish DV Document Tree Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Fill the README-advertised document tree with baseline candidate documents so DV reviewers can navigate all required gates.
|
||||
|
||||
**Architecture:** Keep canonical large baselines at current root paths and add README-path stubs or summaries where needed. Governance, release, security, privacy, legal, UI/UX, i18n, and references each get explicit baseline documents that state current evidence and open gates honestly.
|
||||
|
||||
**Tech Stack:** Markdown documentation aligned to SysRS, SysDes, SRS, SAD, SDD, verification, release, and implementation status.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Requirements Path Wrappers
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/requirements/sysrs.md`
|
||||
- Create: `docs/requirements/srs.md`
|
||||
|
||||
- [x] **Step 1: Create README entry records that point to canonical root documents and summarize DV review anchors**
|
||||
|
||||
These wrappers preserve README paths without duplicating the canonical baselines.
|
||||
|
||||
### Task 2: Add Governance Baselines
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/governance/document-index.md`
|
||||
- Create: `docs/governance/document-naming-convention.md`
|
||||
- Create: `docs/governance/baseline-approval-record.md`
|
||||
- Create: `docs/governance/baseline-candidate-validation-report.md`
|
||||
- Create: `docs/governance/document-review-report.md`
|
||||
- Create: `docs/governance/product-decision-register.md`
|
||||
- Create: `docs/governance/decision-impact-assessment.md`
|
||||
- Create: `docs/governance/git-commit-message-convention.md`
|
||||
- Create: `docs/governance/repo-format-validation-report.md`
|
||||
- Create: `docs/governance/path-migration-map.md`
|
||||
|
||||
- [x] **Step 1: Write governance records**
|
||||
|
||||
Each record summarizes status for DV, identifies owner expectations, and avoids claiming final release approval.
|
||||
|
||||
### Task 3: Add Remaining Release, Security, UI/UX, I18n, Reference Docs
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/release/platform-release-policy.md`
|
||||
- Create: `docs/security/threat-model.md`
|
||||
- Create: `docs/security/secure-storage-audit-report.md`
|
||||
- Create: `docs/security/diagnostic-redaction-audit-report.md`
|
||||
- Create: `docs/ui-ux/material3-guideline.md`
|
||||
- Create: `docs/ui-ux/material3-design-tokens.md`
|
||||
- Create: `docs/ui-ux/material3-component-catalog.md`
|
||||
- Create: `docs/ui-ux/adaptive-layout-platform-guide.md`
|
||||
- Create: `docs/i18n/localization-architecture.md`
|
||||
- Create: `docs/references/external-references.md`
|
||||
- Create: `docs/references/aspice-swe2-swe3-integration-note.md`
|
||||
|
||||
- [x] **Step 1: Write remaining baseline docs**
|
||||
|
||||
Use concise DV-ready records that reference current implementation and open gaps.
|
||||
|
||||
### Task 4: Verify Complete Tree
|
||||
|
||||
**Files:**
|
||||
- Inspect all created documents.
|
||||
|
||||
- [x] **Step 1: Check README paths exist**
|
||||
|
||||
Run a shell `test -f` command over every README-advertised path.
|
||||
|
||||
- [x] **Step 2: Sentinel-language scan**
|
||||
|
||||
Run the sentinel-language scan over `docs/requirements`, `docs/architecture`, `docs/verification`, `docs/release`, `docs/security`, `docs/privacy`, `docs/legal`, `docs/ui-ux`, `docs/i18n`, `docs/governance`, and `docs/references`; expect no matches.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage: fills all README-advertised document paths except already existing files.
|
||||
- Placeholder scan: plan contains no incomplete document instructions.
|
||||
- Type consistency: file paths match README tree.
|
||||
@@ -0,0 +1,108 @@
|
||||
# State Sync and UI Settings Validation Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Complete validation-backed state-sync evidence and UI settings persistence while updating DV documents.
|
||||
|
||||
**Architecture:** State reducer work stays in `crates/chanora_state/src/lib.rs` with Rust unit tests. UI settings persistence stays in `apps/chanora_flutter/lib/services/ui_preferences_service.dart` with Flutter service tests; app-level theme application is wired in `apps/chanora_flutter/lib/main.dart` only if needed by the persisted setting.
|
||||
|
||||
**Tech Stack:** Rust/cargo tests, Flutter/Dart, shared_preferences, Markdown documentation.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: State Reducer Validation
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/chanora_state/src/lib.rs`
|
||||
|
||||
- [x] **Step 1: Write failing regression test**
|
||||
|
||||
Add `channel_delete_removes_clients_in_deleted_channel` proving channel deletion removes clients assigned to that channel and emits client-removal deltas before the channel-removal delta.
|
||||
|
||||
- [x] **Step 2: Verify RED**
|
||||
|
||||
Run: `cargo test -p chanora_state channel_delete_removes_clients_in_deleted_channel --locked`
|
||||
|
||||
Expected: FAIL because deleted-channel clients remain in state.
|
||||
|
||||
- [x] **Step 3: Implement minimal reducer fix**
|
||||
|
||||
In `StateEvent::ChannelDeleted`, collect clients whose `client.channel == id`, remove them from `clients` and `client_order`, then emit deterministic `ClientRemoved` deltas before `ChannelRemoved`.
|
||||
|
||||
- [x] **Step 4: Verify GREEN**
|
||||
|
||||
Run: `cargo test -p chanora_state channel_delete_removes_clients_in_deleted_channel --locked`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [x] **Step 5: Run full state crate tests**
|
||||
|
||||
Run: `cargo test -p chanora_state --locked`
|
||||
|
||||
Expected: all state crate tests pass.
|
||||
|
||||
### Task 2: UI Settings Persistence
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/chanora_flutter/lib/services/ui_preferences_service.dart`
|
||||
- Modify: `apps/chanora_flutter/test/services/ui_preferences_service_test.dart`
|
||||
- Modify: `apps/chanora_flutter/lib/main.dart`
|
||||
|
||||
- [x] **Step 1: Add failing tests for theme persistence**
|
||||
|
||||
Add tests for default `system` theme mode, saving `dark`, saving `light`, and invalid stored value fallback to `system`.
|
||||
|
||||
- [x] **Step 2: Verify RED**
|
||||
|
||||
Run: `flutter test test/services/ui_preferences_service_test.dart`
|
||||
|
||||
Expected: FAIL because `UiThemeMode`, `themeMode`, and `saveThemeMode` do not exist.
|
||||
|
||||
- [x] **Step 3: Implement minimal service changes**
|
||||
|
||||
Add `UiThemeMode`, `UiSettings.themeMode`, persisted key `ui.theme_mode`, and `saveThemeMode`.
|
||||
|
||||
- [x] **Step 4: Verify GREEN**
|
||||
|
||||
Run: `flutter test test/services/ui_preferences_service_test.dart`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [x] **Step 5: Wire app theme mode**
|
||||
|
||||
Make `ChanoraApp` load persisted theme mode and pass `themeMode` into `MaterialApp`.
|
||||
|
||||
- [x] **Step 6: Run focused Flutter tests**
|
||||
|
||||
Run: `flutter test test/services/ui_preferences_service_test.dart`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 3: Documentation Updates
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/implementation-status-2026-05-28.md`
|
||||
- Modify: `docs/release/dv-waiver-register.md`
|
||||
- Modify: `docs/verification/swe4-unit-verification-plan.md`
|
||||
- Modify: `docs/verification/swe6-software-verification-plan.md`
|
||||
- Modify: `docs/architecture/sdd.md`
|
||||
|
||||
- [x] **Step 1: Update implementation status**
|
||||
|
||||
Mark `chanora_state` scaffold statement as superseded by reducer implementation/tests and mark UI settings persistence implemented for SharedPreferences scope.
|
||||
|
||||
- [x] **Step 2: Update waiver and verification docs**
|
||||
|
||||
Record reducer evidence and leave event replay as the remaining P1 state-sync gap.
|
||||
|
||||
- [x] **Step 3: Verify docs**
|
||||
|
||||
Run sentinel-language scan over edited docs.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage: covers reducer evidence, UI settings persistence, and document updates.
|
||||
- Placeholder scan: plan contains no incomplete implementation instructions.
|
||||
- Type consistency: `UiThemeMode`, `themeMode`, and `saveThemeMode` names are used consistently.
|
||||
@@ -0,0 +1,68 @@
|
||||
# SWE.2/SWE.3 Baselines Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add reviewable SWE.2/SAD and SWE.3/SDD baselines so the DV document chain is no longer missing the architecture and detailed design layers.
|
||||
|
||||
**Architecture:** Keep SWE.2 in `docs/architecture/sad.md` and SWE.3 in `docs/architecture/sdd.md`, matching the README document tree. Update DV traceability and verification-plan wording to consume these baselines while preserving honest limitations for areas that still need deeper detail.
|
||||
|
||||
**Tech Stack:** Markdown, existing SysDes/SRS baselines, current Flutter/Rust workspace structure.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Write SWE.2 SAD Baseline
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/architecture/sad.md`
|
||||
|
||||
- [x] **Step 1: Create SAD with architecture views**
|
||||
|
||||
Write sections for purpose, upstream sources, components, static view, runtime flows, interface catalogue, dependency rules, non-functional allocation, architectural decisions, verification handoff, traceability, and open architecture risks.
|
||||
|
||||
### Task 2: Write SWE.3 SDD Baseline
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/architecture/sdd.md`
|
||||
|
||||
- [x] **Step 1: Create SDD with module designs**
|
||||
|
||||
Write sections for purpose, upstream sources, module catalogue, detailed API/data/state design, persistence, diagnostics, platform adapters, build/release design, verification hooks, traceability, and open detailed-design risks.
|
||||
|
||||
### Task 3: Update DV Traceability
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/governance/traceability-matrix.md`
|
||||
- Modify: `docs/verification/verification-master-plan.md`
|
||||
|
||||
- [x] **Step 1: Remove SAD/SDD missing limitation**
|
||||
|
||||
Update traceability text so it says SAD and SDD baselines exist, with known depth limitations instead of missing-document limitations.
|
||||
|
||||
- [x] **Step 2: Update verification plan inputs**
|
||||
|
||||
Update verification master plan to reference SAD and SDD as current inputs for SWE.4/SWE.5.
|
||||
|
||||
### Task 4: Verify SWE.2/SWE.3 Pack
|
||||
|
||||
**Files:**
|
||||
- Inspect created and updated docs.
|
||||
|
||||
- [x] **Step 1: Sentinel-language scan**
|
||||
|
||||
Run the sentinel-language scan over `docs/architecture`, `docs/governance`, and `docs/verification`.
|
||||
|
||||
Expected: no matches introduced by this baseline pack.
|
||||
|
||||
- [x] **Step 2: File presence check**
|
||||
|
||||
Run: `ls docs/architecture`
|
||||
|
||||
Expected: `sad.md` and `sdd.md` are listed.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage: creates SAD and SDD baselines and updates DV traceability consumers.
|
||||
- Placeholder scan: no incomplete instructions are present.
|
||||
- Type consistency: document names match README paths.
|
||||
Reference in New Issue
Block a user