chore: restore product scaffold to rollback baseline
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_server_prefetch` without changing Flutter APIs, resolver behavior, protocol dialing behavior, or Android connect UX.
|
||||
|
||||
**Architecture:** Add `crates/chanora_server_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_server_prefetch/Cargo.toml`: package metadata and dependencies.
|
||||
- Create `crates/chanora_server_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_server_prefetch` to workspace members and update the workspace layout comment.
|
||||
- Modify `core/chanora_core/Cargo.toml`: replace the direct `chanora_resolver` dependency with `chanora_server_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_server_prefetch` Crate With Cache Policy Tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `Cargo.toml`
|
||||
- Create: `crates/chanora_server_prefetch/Cargo.toml`
|
||||
- Create: `crates/chanora_server_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_server_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_server_prefetch",
|
||||
"crates/chanora_bridge",
|
||||
"crates/chanora_resolver",
|
||||
]
|
||||
```
|
||||
|
||||
Create `crates/chanora_server_prefetch/Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "chanora_server_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_server_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_server_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_server_prefetch",
|
||||
host = %normalized,
|
||||
resolved = %addr,
|
||||
"resolution prefetch result"
|
||||
);
|
||||
guard.store_success(generation, &normalized, addr, Instant::now());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
target: "chanora_server_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_server_prefetch",
|
||||
host = %host,
|
||||
resolved = %addr,
|
||||
"connect using prefetched resolution"
|
||||
);
|
||||
Some(addr)
|
||||
}
|
||||
None => {
|
||||
info!(target: "chanora_server_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_server_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_server_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_server_prefetch = { path = "../../crates/chanora_server_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_server_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_server_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_server_prefetch = { path = "../../crates/chanora_server_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_server_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_server_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_server_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_server_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_server_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,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.
|
||||
@@ -0,0 +1,116 @@
|
||||
# Chanora Server Prefetch Crate Design
|
||||
|
||||
Date: 2026-05-28
|
||||
|
||||
## Goal
|
||||
|
||||
Move server-resolution prefetch policy out of `chanora_core` into a focused Rust crate named `chanora_server_prefetch`, without changing connection behavior, Flutter APIs, or protocol dialing semantics.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not change resolver behavior or DNS/SRV/TSDNS ordering.
|
||||
- Do not change `ConnectConfig.resolved_address` semantics in `chanora_protocol`.
|
||||
- Do not expose prefetch state in the UI.
|
||||
- Do not prefetch bookmarks or additional hosts.
|
||||
- Do not persist prefetched addresses.
|
||||
|
||||
## Architecture
|
||||
|
||||
Add a workspace member at `crates/chanora_server_prefetch`.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Normalize server host keys by trimming and lowercasing.
|
||||
- Track one active prefetch generation.
|
||||
- Store at most one successful prefetched socket address.
|
||||
- Reject stale async completions by generation.
|
||||
- Return a prefetched address only for an exact normalized host match.
|
||||
- Enforce the 2-minute freshness TTL.
|
||||
- Resolve server addresses by calling `chanora_resolver::ChanoraResolver::resolve_client_address`.
|
||||
- Log prefetch start, success, miss/stale, and failure diagnostics.
|
||||
|
||||
Dependencies:
|
||||
|
||||
- `chanora_resolver` for actual server address resolution.
|
||||
- `tokio` for `Mutex` and spawned prefetch tasks.
|
||||
- `tracing` for diagnostics.
|
||||
- `thiserror` for a narrow `ServerPrefetchError` public error type.
|
||||
|
||||
## Public API
|
||||
|
||||
The crate exposes this small async owner type:
|
||||
|
||||
```rust
|
||||
pub struct ServerPrefetcher { ... }
|
||||
|
||||
impl ServerPrefetcher {
|
||||
pub fn new() -> Self;
|
||||
pub async fn prefetch(&self, host: String) -> Result<(), ServerPrefetchError>;
|
||||
pub async fn fresh_match(&self, host: &str) -> Option<std::net::SocketAddr>;
|
||||
}
|
||||
```
|
||||
|
||||
`prefetch` returns after scheduling work, preserving the current invisible, non-blocking behavior. Empty normalized hosts are ignored successfully. Resolution failures are stored only as diagnostics and do not affect connect semantics.
|
||||
|
||||
## Core Integration
|
||||
|
||||
`chanora_core` replaces its private prefetch cache fields and helpers with `ServerPrefetcher`.
|
||||
|
||||
Core remains the trust boundary for connection config:
|
||||
|
||||
- It clears any caller-provided `ConnectConfig.resolved_address` before lookup.
|
||||
- It asks `ServerPrefetcher::fresh_match` for the current host.
|
||||
- It sets `dial_cfg.resolved_address` only from a fresh exact cache hit.
|
||||
- It stores supervisor/reconnect config with `resolved_address: None`.
|
||||
|
||||
The Flutter bridge keeps calling the same core API, `prefetch_server_resolution(host)`. No Dart API change is intended.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. Flutter host editing schedules `ChanoraSession::prefetch_server_resolution(host)`.
|
||||
2. Core delegates to `ServerPrefetcher::prefetch(host)`.
|
||||
3. The prefetcher normalizes the host, increments generation, and spawns resolver work.
|
||||
4. On success, the prefetcher stores the resolved socket address if the generation is still current.
|
||||
5. On connect, core prepares `(stored_cfg, dial_cfg)`.
|
||||
6. Core clears untrusted `resolved_address`, asks the prefetcher for a fresh exact match, and applies the result only to `dial_cfg`.
|
||||
7. Protocol uses `dial_cfg.resolved_address` if present; otherwise it resolves normally.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Prefetch failures remain invisible to users.
|
||||
- Prefetch failures are logged through `tracing`.
|
||||
- If prefetch misses, is stale, or fails, connect falls back to normal protocol resolution.
|
||||
- A caller-supplied `resolved_address` is never trusted by core.
|
||||
|
||||
## Testing
|
||||
|
||||
Move cache-policy tests from `chanora_core` into `chanora_server_prefetch`:
|
||||
|
||||
- fresh exact match returns the socket address.
|
||||
- stale entries are ignored.
|
||||
- different hosts are ignored.
|
||||
- stale generation completions are ignored.
|
||||
- blank normalized hosts return `Ok(())` and do not update generation or spawn resolver work.
|
||||
|
||||
Keep core tests for connection trust-boundary behavior:
|
||||
|
||||
- untrusted `resolved_address` is cleared on cache miss.
|
||||
- prepared stored/supervisor config has `resolved_address: None`.
|
||||
- prepared dial config can receive a fresh prefetched address.
|
||||
|
||||
Run at minimum:
|
||||
|
||||
- `cargo test -p chanora_server_prefetch --lib`
|
||||
- `cargo test -p chanora_core --lib`
|
||||
- `cargo test -p chanora_protocol --lib`
|
||||
|
||||
For final confidence, rerun the Android server connect smoke path that verifies prefetch logs and connected UI.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Workspace builds with the new crate member.
|
||||
- `chanora_core` no longer owns the prefetch cache implementation.
|
||||
- `chanora_server_prefetch` owns prefetch normalization, TTL, generation, storage, and resolver-backed warming.
|
||||
- Public Flutter and Rust protocol behavior is unchanged.
|
||||
- Existing server connect and reconnect safety tests pass.
|
||||
- Android connect still reaches the connected server UI and does not get stuck in `Connecting` or `Synchronizing`.
|
||||
@@ -0,0 +1,203 @@
|
||||
# Server Resolution Prefetch Design
|
||||
|
||||
Date: 2026-05-28
|
||||
|
||||
## Purpose
|
||||
|
||||
Reduce perceived server join latency by resolving the active TeamSpeak server address before the user taps Connect. Prefetch must be invisible, conservative, and safe: it may warm resolver state, but it must not change connection semantics or surface background errors to the user.
|
||||
|
||||
Recent Android testing showed resolver latency can dominate the first part of the connect flow. A prior fix bounded slow TS3 SRV discovery and removed Android's forced Cloudflare resolver. Prefetch builds on that by hiding remaining address-resolution work when the user has already entered or loaded a likely server address.
|
||||
|
||||
## Goals
|
||||
|
||||
- Prefetch only the active server address field.
|
||||
- Keep the feature invisible to users.
|
||||
- Reuse prefetched results only for exact normalized host matches.
|
||||
- Keep prefetched results fresh for 2 minutes.
|
||||
- Preserve today's Connect behavior when prefetch misses, fails, or is stale.
|
||||
- Avoid prefetching all bookmarks.
|
||||
- Avoid opening a TS3 session before the user taps Connect.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No visible resolving, ready, or failed UI state.
|
||||
- No bookmark fan-out prefetch.
|
||||
- No persisted resolver cache across app launches.
|
||||
- No password, channel, or permission validation during prefetch.
|
||||
- No server reachability probe beyond address resolution.
|
||||
- No connection warm-up or pre-authentication.
|
||||
|
||||
## Chosen Approach
|
||||
|
||||
Use a Rust-owned resolver prefetch cache with Flutter-owned scheduling.
|
||||
|
||||
Flutter knows when the active host field changes, so it schedules prefetch requests. Rust owns resolver correctness, normalization, cache validity, and connect-time reuse. This keeps Flutter from depending on resolver internals and ensures Connect can independently decide whether a prefetched result is safe to use.
|
||||
|
||||
Other approaches considered:
|
||||
|
||||
- Flutter-only prefetch: rejected because it pushes resolver state into Dart and creates a weaker boundary between UI and connection behavior.
|
||||
- Resolver-internal repeated-call cache only: rejected because it does not hide first-click latency from the active host field.
|
||||
|
||||
## Behavior
|
||||
|
||||
Prefetch starts for the active host value in two cases:
|
||||
|
||||
- After `_loadUiSettings()` loads the last-used host into `_hostCtl`.
|
||||
- After the user stops editing the host field for about 700 ms.
|
||||
|
||||
The feature is invisible:
|
||||
|
||||
- No SnackBars.
|
||||
- No inline status text.
|
||||
- No disabled Connect button.
|
||||
- No user-facing error if prefetch fails.
|
||||
|
||||
Connect behavior:
|
||||
|
||||
- If the current normalized host exactly matches a fresh prefetched entry, Connect uses the cached resolved address.
|
||||
- If the cache is missing, stale, failed, or for a different host, Connect resolves normally.
|
||||
- Connect remains the only operation that opens a TS3 session.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Flutter Scheduling
|
||||
|
||||
`_BetaHomeState` owns the host text field. It should add a listener to `_hostCtl` and manage a short debounce timer.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Trim the host input before scheduling.
|
||||
- Skip empty values.
|
||||
- Reset the debounce timer on each edit.
|
||||
- Call a bridge prefetch API after about 700 ms of idle typing.
|
||||
- Schedule one prefetch after settings load if the loaded host is non-empty.
|
||||
- Dispose the listener and timer with the widget state.
|
||||
|
||||
Flutter does not store resolved addresses and does not decide whether Connect can use a prefetched result.
|
||||
|
||||
### Bridge API
|
||||
|
||||
Add a fire-and-forget bridge function shaped like:
|
||||
|
||||
```text
|
||||
prefetch_server_resolution(host: String) -> Result<(), BridgeError>
|
||||
```
|
||||
|
||||
The bridge call should return after the prefetch task has been accepted by the Rust runtime. It must not wait for resolution to complete. Background completion or failure is reported only through diagnostics/logging.
|
||||
|
||||
### Rust Resolver Cache
|
||||
|
||||
Rust stores a small prefetch cache owned near the session/resolver boundary. A single latest-host entry is enough for v1, because the design only prefetches the active field.
|
||||
|
||||
Cache entry fields:
|
||||
|
||||
- Normalized input host.
|
||||
- Resolved `host:port` address.
|
||||
- Resolution method.
|
||||
- Completion timestamp.
|
||||
- Generation or request id.
|
||||
- Optional sanitized failure metadata for diagnostics.
|
||||
|
||||
The cache TTL is 2 minutes.
|
||||
|
||||
### Connect Integration
|
||||
|
||||
Connect should ask Rust for a fresh exact-match prefetched result before running normal resolution.
|
||||
|
||||
Rules:
|
||||
|
||||
- Exact normalized host match is required.
|
||||
- Entry age must be at most 2 minutes.
|
||||
- Failed entries must not block normal connect resolution.
|
||||
- Stale entries must be ignored.
|
||||
- Missing cache must behave exactly like today.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. App starts.
|
||||
2. `_loadUiSettings()` loads the last-used host into `_hostCtl`.
|
||||
3. Flutter schedules invisible prefetch for that host.
|
||||
4. User edits the host field.
|
||||
5. Flutter cancels the pending debounce timer and starts a new one.
|
||||
6. After 700 ms idle, Flutter calls Rust prefetch with the latest trimmed host.
|
||||
7. Rust normalizes and resolves the host through the same resolver path used by Connect.
|
||||
8. Rust stores the result if it still matches the latest generation for that normalized host.
|
||||
9. User taps Connect.
|
||||
10. Rust Connect checks the cache for a fresh exact-match result.
|
||||
11. Cache hit: Connect uses the prefetched address.
|
||||
12. Cache miss/stale/failure: Connect resolves normally.
|
||||
|
||||
## Cancellation And Staleness
|
||||
|
||||
Cancellation can be logical rather than hard task cancellation.
|
||||
|
||||
- Flutter prevents obsolete debounce timers from firing.
|
||||
- Rust tags requests by normalized host and generation.
|
||||
- Late completions for stale generations must not replace newer successful entries.
|
||||
- Duplicate prefetches for the same normalized host may coalesce or refresh the same entry.
|
||||
|
||||
This avoids complexity while preventing old input values from poisoning the cache.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Prefetch failures are diagnostic-only.
|
||||
|
||||
- Empty host: skip prefetch.
|
||||
- Invalid host shape: skip or fail silently with debug diagnostics.
|
||||
- Resolver failure: store optional sanitized failure metadata for diagnostics only.
|
||||
- Connect after failure: normal connect path runs and surfaces errors as it does today.
|
||||
- App resume and network changes: no special invalidation in v1; TTL handles staleness.
|
||||
- Disconnect: cache may remain because it is independent of the TS3 session.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Add privacy-safe logs for:
|
||||
|
||||
- Prefetch started.
|
||||
- Prefetch result.
|
||||
- Prefetch failed.
|
||||
- Connect using prefetched resolution.
|
||||
- Connect prefetch miss or stale entry.
|
||||
|
||||
Do not log passwords, channel passwords, or nickname. Host and resolved address are acceptable because resolver/connect logging already includes them today.
|
||||
|
||||
## Testing
|
||||
|
||||
Rust tests:
|
||||
|
||||
- Fresh exact-match prefetched result is reusable.
|
||||
- Stale prefetched result is ignored.
|
||||
- Different normalized host is ignored.
|
||||
- Failed prefetch does not block normal resolution.
|
||||
- Late stale generation cannot overwrite a newer cache entry.
|
||||
|
||||
Flutter tests should cover the scheduling logic through a small testable helper if wiring directly through `_BetaHomeState` would be brittle:
|
||||
|
||||
- Host edits debounce prefetch scheduling.
|
||||
- Empty host does not prefetch.
|
||||
- Settings-loaded host schedules one prefetch.
|
||||
|
||||
Manual Android smoke test:
|
||||
|
||||
- Install debug APK.
|
||||
- Launch app.
|
||||
- Wait for last-used host prefetch or type host and wait past debounce.
|
||||
- Tap Connect.
|
||||
- Confirm UI reaches connected server view.
|
||||
- Confirm logcat shows either a prefetch cache hit or safe fallback behavior.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Typing or loading a valid host can warm resolver state before Connect.
|
||||
- Connect never fails because prefetch failed.
|
||||
- Connect never uses a prefetched result for a different normalized host.
|
||||
- Prefetched entries older than 2 minutes are ignored.
|
||||
- No visible UI is added for prefetch state.
|
||||
- Bookmarks are not prefetched in bulk.
|
||||
- Android debug build and focused resolver/Flutter tests pass.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Prefer a single latest-host cache unless implementation reveals an existing cache abstraction that makes a tiny map simpler.
|
||||
- Prefer minimal bridge API surface: one prefetch call and connect-time internal cache lookup.
|
||||
- Keep the resolver cache near existing Rust session/connect code so future non-Flutter clients can benefit from the same behavior.
|
||||
@@ -0,0 +1,70 @@
|
||||
# State Sync and UI Settings Validation Design
|
||||
|
||||
**Date:** 2026-05-29
|
||||
**Status:** Approved for implementation
|
||||
**Scope:** P0/P1 validation-based completion for state-sync evidence and UI settings persistence
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Close the current DV/P0-P1 gaps for reducer/state-sync evidence and UI settings persistence with tests first, minimal behavior changes, and updated documentation evidence.
|
||||
|
||||
## 2. State-Sync Design
|
||||
|
||||
`chanora_state` remains the reducer owner. The validation pass adds focused tests for known reducer contracts rather than broad refactoring. Missing behavior is implemented only when a test proves a gap.
|
||||
|
||||
Required evidence covers:
|
||||
|
||||
| Contract | Evidence |
|
||||
|---|---|
|
||||
| Snapshot creates ready state and deterministic normalized order | Existing and expanded reducer tests |
|
||||
| Reconnect discards stale state and reconnect snapshot replaces state | Existing reducer tests |
|
||||
| Disconnected/lost states suppress live deltas | Existing reducer tests |
|
||||
| Duplicate IDs are normalized deterministically | Existing reducer tests |
|
||||
| Unknown client voice activity is ignored | Existing reducer tests |
|
||||
| Channel deletion removes clients in deleted channel | New reducer regression test and implementation |
|
||||
| Same event sequence produces same state and deltas | Existing reducer determinism test |
|
||||
|
||||
## 3. UI Settings Design
|
||||
|
||||
`UiPreferencesService` remains a Flutter service backed by `shared_preferences`. This is the minimal P0/P1-complete implementation because the current app already uses SharedPreferences and no current behavior requires SQLite-backed UI settings.
|
||||
|
||||
`UiSettings` gains a typed `themeMode` field with values:
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `system` | Follow platform theme |
|
||||
| `light` | Force light theme |
|
||||
| `dark` | Force dark theme |
|
||||
|
||||
The service persists the selected theme mode, falls back to `system` for invalid stored values, and preserves independent saves for host and nickname.
|
||||
|
||||
## 4. App Wiring
|
||||
|
||||
`ChanoraApp` becomes stateful enough to load and apply persisted theme mode. `_BetaHome` continues to load/save host and nickname through `UiPreferencesService`. UI controls for selecting theme mode are out of this slice unless already present; this slice provides persistence and app-level application.
|
||||
|
||||
## 5. Documentation Updates
|
||||
|
||||
After tests pass:
|
||||
|
||||
| Document | Update |
|
||||
|---|---|
|
||||
| `docs/implementation-status-2026-05-28.md` | Mark reducer scaffold statement stale/resolved and UI settings persistence implemented for SharedPreferences scope |
|
||||
| `docs/release/dv-waiver-register.md` | Close or soften reducer waiver; keep event replay as P1 gap |
|
||||
| `docs/verification/swe4-unit-verification-plan.md` | Record reducer test evidence and UI settings tests |
|
||||
| `docs/verification/swe6-software-verification-plan.md` | Update state sync and UI settings DV status |
|
||||
| `docs/architecture/sdd.md` | Record UI settings persistence design |
|
||||
|
||||
## 6. Validation
|
||||
|
||||
Run focused tests:
|
||||
|
||||
```text
|
||||
cargo test -p chanora_state --locked
|
||||
flutter test test/services/ui_preferences_service_test.dart
|
||||
```
|
||||
|
||||
Run wider checks if touched app-shell behavior requires it:
|
||||
|
||||
```text
|
||||
flutter test --exclude-tags e2e
|
||||
```
|
||||
Reference in New Issue
Block a user