Commit Graph
25 Commits
Author SHA1 Message Date
EdisonJwa 0bef61aea2 feat(core): A.6 — supervisor reconnect with watchdog and event stream
Adds an end-to-end auto-reconnect path so a brief network outage no
longer leaves the client wedged in a half-dead state. The flow has
three layers, each motivated by a real failure mode observed on the
Moto G live test:

* `chanora_protocol::DisconnectReason` (`UserRequested` /
  `StreamEnded` / `Error(String)`) is reported on a `oneshot` when
  the per-connection task exits, so the supervisor can tell user
  intent apart from a real loss.
* `chanora_core` spawns a supervisor task per `ChanoraSession`. It
  listens for the loss notifier AND runs a watchdog that issues
  `snapshot()` probes every 5s with a 4s timeout — three consecutive
  misses synthesise a `DisconnectReason::Error(...)` and trigger the
  reconnect path. The watchdog catches the "ghost connected" case
  where tsclientlib silently resets internal state but the event
  stream never errors. Backoff schedule: 1s, 2s, 5s, 15s, 30s, 60s
  (capped). On success the supervisor swaps the dead `ProtocolClient`
  for the new one in place and, if audio was running, restarts the
  audio engine bound to the new `voice_in`/`voice_out` channels.
* `SessionEvent` (Connected / Lost / Reconnecting / Disconnected /
  AudioStarted / AudioStopped) is broadcast on a 64-slot channel.
  `chanora_bridge` re-exports it as `BridgeEvent` and exposes
  `events_stream(StreamSink)`; the Flutter side subscribes from
  `initState` and renders a reconnect banner with attempt count and
  delay. New `SnapshotProbe` exposes a clone-friendly snapshot path
  so the watchdog can probe without holding `&self` across awaits.

Localization adds `statusReconnecting` and `statusConnectionLost`
keys to `app_en.arb` and `app_zh.arb`.

Verified on Moto G Stylus 5G (Android 14) against cn.teamspeak.app:
killed Wi-Fi + cellular for ~70 s; watchdog declared loss at three
misses, supervisor walked the backoff schedule, and the UI
reconnected automatically once the radios came back. Snapshot tree
re-rendered without user action.
2026-05-15 01:06:07 +08:00
EdisonJwa bc0da50cdb feat(protocol): A.1 — fix hostname resolution on Android and iOS
Resolves the Beta-blocking issue surfaced during Android v0.2.0-beta.1
verification: hostnames could not be used, only literal IPs.

Root cause:
  tsclientlib's built-in resolver uses hickory-resolver, which reads
  /etc/resolv.conf. That file does not exist on Android or iOS, so
  any connect by hostname exited the connection task before
  signalling ready and surfaced the cryptic error
    BridgeError.connection(field0: protocol backend:
      connection task exited before signalling ready)

Fix:
  crates/chanora_protocol/src/resolver.rs (new):
    Resolves hostnames via tokio::net::lookup_host, which uses the
    platform's getaddrinfo. Works on every platform Chanora targets.
    Tiny in-process positive-result cache (5 min TTL) keeps
    reconnects cheap. IPv4 sorted ahead of IPv6 in the returned list
    to favour the more reliable path on dual-stack networks.

  crates/chanora_protocol/src/adapter.rs:
    connection_task now resolves the hostname itself and passes the
    resulting SocketAddr (not the hostname String) to
    tsclientlib::Connection::build. tsclientlib's ServerAddress enum
    accepts SocketAddr via its From impl, so the upstream resolver
    is skipped entirely.

  crates/chanora_protocol/src/lib.rs:
    New typed error arm ProtocolError::DnsFailed { host, reason }
    so the UI can distinguish 'server not found' from 'server
    refused our packets'.

  crates/chanora_bridge/src/lib.rs:
    Matching BridgeError::DnsFailed { host, reason } DTO surfaced
    to Dart, with explicit From<CoreError::Protocol(DnsFailed)>
    mapping so the UI gets the structured fields rather than a
    stringified mess.

Tests added (crates/chanora_protocol/src/resolver.rs::tests):
  - rejects_empty
  - literal_ipv4_short_circuits
  - literal_ipv4_default_port_path
  - unresolvable_returns_dns_failed
  - resolves_known_hostname  (#[ignore], --ignored to run; hits net)

Empirical verification (2026-05-14):
  Workspace: cargo check + cargo test --workspace clean.
  Live resolver test: cn.teamspeak.app → 175.178.125.23:9987 (passes).
  cargo test -p chanora_core --test alpha_smoke -- --ignored:
    server='Vigorous Pro' channels=42 clients=20 (passes by hostname).
  flutter test: alpha_e2e_test + beta_e2e_test both green.
  Physical Moto G Stylus 5G (Android 14 arm64-v8a):
    APK rebuilt (48.9 MB). adb install + launch.
    Connect form left at default 'cn.teamspeak.app'.
    logcat shows:
      chanora_protocol: dns resolved input=cn.teamspeak.app
                                    resolved=175.178.125.23:9987
      tsclientlib: starting connection to 175.178.125.23:9987
      tsproto::resend: Connecting → Connected
      chanora_protocol: initial state snapshot received
    UI shows 'Connected to Vigorous Pro' / '42 channels • 20 online'.

This is the first item in Category A (post-Beta polish bundle).
Pause point: review before A.6 (full reconnect).
2026-05-15 00:17:13 +08:00
EdisonJwa 1324f478fe docs(release): add iOS build instructions + shell helper
The development host is Linux x86_64; the iOS toolchain (Xcode, xcrun,
codesign, iPhoneOS SDK) is macOS-only under Apple licence and cannot
be cross-compiled from Linux. This commit adds the instructions for
producing the iOS v0.2.0-beta.1 build on a macOS host plus a bash
helper that automates the build itself.

docs/release/ios-build.md (v0.1.0):
  - Toolchain pin table (macOS 14+, Xcode 26+ per DEC-021, iOS SDK
    26+, iOS deployment target 13.0 per DEC-003, Flutter 3.41.9,
    Rust 1.95 stable with aarch64-apple-ios / aarch64-apple-ios-sim /
    x86_64-apple-ios targets, CocoaPods 1.16+, FRB 2.12.0).
  - macOS host options: owned hardware vs rental (MacStadium,
    MacinCloud, Scaleway Apple silicon, AWS EC2 Mac) vs borrowed
    Mac. Realistic cost ranges per option.
  - Step-by-step Homebrew + Rust + Flutter + CocoaPods install.
  - Pre-built libopus.a per arch via a CMake invocation that
    targets the iOS SDK explicitly. Mirrors the Android build's
    LIBOPUS_LIB_DIR wrap-dir trick.
  - flutter create --platforms=ios to scaffold the ios/ folder
    (the product Flutter app was created with only linux + android).
  - Edits required to ios/Podfile and ios/Runner/Info.plist:
    iOS 13 deployment target (DEC-003), NSMicrophoneUsageDescription
    for the audio engine, UIBackgroundModes=audio for screen-locked
    playback.
  - Three cargo build --target invocations for device + both
    simulator slices.
  - lipo merge of the two simulator slices into one .a.
  - xcodebuild -create-xcframework to produce
    target/ChanoraBridge.xcframework with the right slices.
  - flutter build ios --release --no-codesign or
    flutter build ipa --release --export-method development for
    a signed .ipa.
  - Install paths: xcrun devicectl for wired install, altool for
    TestFlight upload.
  - Smoke-test instructions with the same hostname-resolution
    caveat that affects the Android Beta (hickory-resolver does
    not work on iOS; use the literal IP).
  - Packaging into chanora-v0.2.0-beta.1-ios.ipa.
  - Known-issue table covering: audiopus_sys cmake build failures
    on iOS, microphone permission prompt prerequisites,
    AVAudioSession category quirks for voice transmission, code-
    signing failure modes, TestFlight rejection causes.
  - Reproducibility note (build is not bit-reproducible).

tools/build-ios.sh:
  - Parameter switches: --version, --no-codesign,
    --regenerate-bindings, --export-method.
  - Verifies xcodebuild, xcrun, cargo, rustc, flutter, pod, lipo
    on PATH.
  - Adds the three rustup iOS targets if missing.
  - Verifies each pre-built libopus.a exists at the expected wrap
    dir before starting.
  - Optionally regenerates FRB bindings.
  - Three cargo build runs (device + Apple-silicon sim + Intel
    sim), each with LIBOPUS_LIB_DIR pointed at its arch's wrap dir
    and the audiopus_sys build-cache wiped per target.
  - lipo + xcodebuild -create-xcframework.
  - flutter pub get + pod install + flutter build {ios,ipa}.
  - Copies the .ipa to a versioned path under $HOME and prints
    SHA-256.

This is documentation + helper only; no actual iOS binaries are
produced by this commit. The Linux development host cannot run
Xcode. To produce the binaries, follow §3-§14 of
docs/release/ios-build.md on a macOS host, or run
tools/build-ios.sh there.

DEC-011.1 iOS audio status remains Deferred; the doc notes that
cpal's iOS backend has not been empirically verified and the
AVAudioSession category likely needs configuration for voice
transmission. Both are Beta+ items.
2026-05-14 23:49:58 +08:00
EdisonJwa c81ccfd9a9 feat(android): produce v0.2.0-beta.1 Android APK with voice in/out
Builds the Internal Beta product app for Android. Companion to the
Linux desktop build already shipped at the same tag.

What this commit adds to the source tree:

  crates/chanora_bridge/src/android_init.rs (new):
    JNI lifecycle for Android. JNI_OnLoad captures the JavaVM*.
    Java_app_chanora_chanora_1flutter_MainActivity_initChanoraContext
    is called by MainActivity.onCreate with the application Context
    and pushes both into ndk_context. Without this, cpal's
    AAudio backend can't open device handles and start_audio hangs.

  crates/chanora_bridge/Cargo.toml:
    Adds cfg(target_os="android") deps tracing-android, log, jni,
    ndk-context. Linux/desktop builds are unaffected.

  crates/chanora_bridge/src/lib.rs:
    Conditionally includes the android_init module on Android.

  crates/chanora_bridge/src/api.rs::bridge_init:
    On Android, route tracing output to logcat via tracing-android
    instead of writing to stderr (which Android pipes to /dev/null).
    Logs show under `adb logcat -s chanora`.

  apps/chanora_flutter/android/app/src/main/AndroidManifest.xml:
    Adds uses-permission android.permission.INTERNET (needed for
    the protocol layer) and android.permission.RECORD_AUDIO (needed
    by chanora_audio's capture stream). Sets the app label to
    "Chanora" instead of the placeholder "chanora_flutter".

  apps/chanora_flutter/android/app/src/main/kotlin/.../MainActivity.kt:
    Overrides the Flutter-generated MainActivity. Loads
    libchanora_bridge.so eagerly at class-init so JNI_OnLoad runs
    before any FRB call. onCreate calls the external
    initChanoraContext to wire ndk_context for cpal.

  apps/chanora_flutter/pubspec.yaml:
    Bumps version 1.0.0+1 → 0.2.0+2 to match the v0.2.0-beta.1 tag.

  run-chanora.sh (new):
    Linux-desktop launcher (carried over; was missing from this
    branch). Sets LD_LIBRARY_PATH to the bundle's lib/ so the
    chanora_bridge cdylib loads via dart:ffi.

Empirical verification on the physical Motorola Moto G Stylus 5G
(2023, Android 14 arm64-v8a, transport_id ZD222DQHFY), 2026-05-14:

  - APK installed via adb install.
  - Activity launched; permissions granted.
  - Connect form filled with 175.178.125.23 (Vigorous Pro's IP —
    see honest limitation below); Connect button tapped.
  - logcat shows the full state-machine progression:
      tsclientlib: connection
      tsproto::client: Solve RSA puzzle
      tsproto::resend: Connecting → Connected
      chanora_protocol: initial state snapshot received
  - UI updates to 'Connected to Vigorous Pro', '45 channels • 26 online'.
  - Welcome banner with CJK characters preserved verbatim.
  - 'Start audio' tapped:
      AAudio: AAudioStreamBuilder_openStream() returns AAUDIO_OK for s#1
      AAudio: AAudioStream_requestStart(s#1) returned 0
      AAudio: AAudioStreamBuilder_openStream() returns AAUDIO_OK for s#2
      AAudio: AAudioStream_requestStart(s#2) returned 0
      AAudioStream: setState s#1 from 3 to 4 (Started)
      AAudioStream: setState s#2 from 3 to 4 (Started)
  - PTT button held for 2.5 s:
      UI shows: 'TX 124 frames • RX 0 frames • PTT off'.
    124 frames / 2.5 s ≈ 50 frames/s = 20 ms Opus frames — exactly
    the encoder cadence. Voice transmission proven over UDP to the
    real server.

Honest limitation surfaced during verification:

  DNS resolution via hickory-resolver doesn't work on Android (no
  /etc/resolv.conf). Connecting by hostname produces:
    BridgeError.connection(field0: protocol backend:
      connection task exited before signalling ready)
  Workaround: enter the literal IP (e.g. 175.178.125.23 for
  cn.teamspeak.app). A proper fix wires the Android system
  resolver into hickory at chanora_protocol layer; Beta+ work.

Build prerequisites (documented for reproducibility):

  - Android NDK r26.3.11579264 at /opt/android-sdk/ndk/26.3.11579264.
  - rustup targets: aarch64-linux-android, armv7-linux-androideabi,
    x86_64-linux-android.
  - cargo-ndk 4.x.
  - Pre-built libopus.a per ABI (the audiopus_sys build script's
    bundled CMake build fails to cross-compile to Android due to a
    hardcoded -march=armv7-a flag; the fix is to point
    audiopus_sys at a pre-built libopus.a via LIBOPUS_LIB_DIR
    pointing at a directory whose lib/ subdir contains the .a).
    Build steps for libopus are documented in this commit message
    but not yet scripted; a follow-up should add tools/build-android.sh.
  - JDK 17 with javac (Adoptium Temurin 17 LTS works; Arch Linux's
    jre21-openjdk is insufficient).

ABIs built and shipped in the APK:
  arm64-v8a, armeabi-v7a, x86_64.

Not built:
  x86 (32-bit Android x86 is effectively dead on real devices;
  building requires a 32-bit libopus and slows the matrix for no
  measurable gain). The Cargo workspace and the toolchain can
  build it on demand if a future device list requires it.
2026-05-14 23:40:58 +08:00
EdisonJwa 8094ec7277 docs(release): add Windows build instructions + PowerShell helper
The development host is Linux x86_64; `flutter build windows` cannot
be cross-compiled and requires a Windows host with Visual Studio
2022's C++ Desktop workload. This commit adds the instructions for
producing the Windows v0.2.0-beta.1 Internal Beta build on an Azure
VM, plus a PowerShell helper that automates the build itself.

docs/release/windows-build.md (v0.1.0):
  - Toolchain pin table (Windows Server 2022, VS 2022 Build Tools
    + C++ workload, Flutter 3.41.9, Rust 1.95 stable, FRB 2.12.0,
    CMake, audiopus build dependency).
  - Azure VM provisioning recipe: Standard_D4s_v5 (4 vCPU / 16 GiB),
    Premium SSD 128 GiB, RDP locked to caller IP, auto-shutdown,
    cost estimate (<USD 1 per build session).
  - Step-by-step PowerShell to install VS 2022 Build Tools with the
    required components, Git for Windows, rustup, Flutter SDK,
    flutter_rust_bridge_codegen, and CMake.
  - Two upload paths for source: temporary git remote OR zip archive
    over RDP clipboard.
  - flutter create --platforms=windows to scaffold the
    windows/ platform folder (the product Flutter app was created
    with only linux + android).
  - cargo build -p chanora_bridge to produce chanora_bridge.dll.
  - flutter build windows --release to produce chanora_flutter.exe
    and the bundle.
  - Drop the DLL next to the EXE so dart:ffi loads it.
  - Smoke-test instructions including the cn.teamspeak.app UDP 9987
    egress gotcha for some Azure regions.
  - Packaging into chanora-v0.2.0-beta.1-windows-x64.zip.
  - Known-issue / caveat table.
  - Reproducibility note (build is not bit-reproducible in this Beta).

tools/build-windows.ps1:
  - Parameter switches: -SkipRustBuild, -RegenerateBindings, -Version.
  - Verifies flutter, cargo, rustc, cmake, git on PATH.
  - Adds the x86_64-pc-windows-msvc target via rustup if missing.
  - Runs flutter create --platforms=windows if the windows/ folder
    is absent in apps/chanora_flutter/.
  - Optionally regenerates FRB bindings.
  - cargo build --release -p chanora_bridge --target x86_64-pc-windows-msvc.
  - flutter pub get + flutter build windows --release.
  - Copies the DLL into the Release bundle.
  - Compress-Archive into chanora-<version>-windows-x64.zip.
  - Prints final artefact paths.

This is documentation + helper only; no actual Windows binaries are
produced by this commit. To produce the binaries, follow §3-§13 of
docs/release/windows-build.md on a Windows host.
2026-05-14 23:02:56 +08:00
EdisonJwa 9790005c3e feat(beta): wire voice in/out end-to-end with push-to-talk (v0.2.0-beta.1)
Reaches the Internal Beta milestone of DEC-001's release sequence the
same day as Alpha. Adds voice capture and playback through the full
Flutter UI → FRB → Rust core → tsclientlib → server path.

Promotions from PoC:
  poc/audio-capture-playback-spike  →  crates/chanora_audio/

New product code:
  crates/chanora_audio/src/engine.rs — cpal capture and playback,
    audiopus Opus VoIP encoder (48 kHz mono 20 ms frames), tsclientlib
    AudioHandler for decode + jitter buffer + mix on playback,
    push-to-talk gate, graceful playback-only fallback when capture
    is unavailable.
  crates/chanora_protocol/src/adapter.rs — extended with
    voice_out_tx (clonable mpsc::Sender<OutPacket>) and
    take_voice_in() (one-shot mpsc::Receiver<InboundVoice>); main
    loop now interleaves outbound voice drain, event pumping, and
    control-request handling.
  crates/chanora_protocol/src/lib.rs — re-exports the few
    tsproto_packets types (OutAudio, OutPacket, InAudioBuf,
    AudioData, CodecType, Direction) that chanora_audio
    legitimately needs. Documented as the single deliberate
    cross-crate type re-export per SAD-067, justified by the
    performance cost of a parallel type hierarchy on the 20 ms
    voice frame.
  core/chanora_core/src/lib.rs — ChanoraSession::start_audio,
    set_ptt, audio_stats; disconnect now stops the engine first.
  crates/chanora_bridge/src/api.rs — startAudio, setPtt,
    audioStats commands and BridgeAudioStats DTO.
  apps/chanora_flutter/lib/main.dart — "Start audio" button +
    hold-to-talk PTT button with pressed/released visual state +
    live stats line (TX/RX/PTT). Stats polled every 500 ms.

ARB:
  Both en and zh-Hans gain startAudioAction, pttHoldToTalk,
  pttTransmitting, audioStatsLine. Banner updated to
  "Beta build — voice in/out wired; not production ready."

FRB config:
  flutter_rust_bridge.yaml gains local: true so codegen resolves
  the workspace member's library stem to "chanora_bridge" instead
  of falling back to "UNKNOWN".

Empirical verification (2026-05-14, against cn.teamspeak.app):
  cargo check + cargo test --workspace: all green.
  flutter analyze: 0 issues.
  flutter test: 4/4 passing including:
    - test/alpha_e2e_test.dart (regression: Alpha still works)
    - test/beta_e2e_test.dart (Beta: connect → startAudio →
      PTT cycle → disconnect against cn.teamspeak.app).
  Live smoke (cargo test alpha_smoke -- --ignored): 49 channels,
  37 clients retrieved.
  Capture stream open against the host PipeWire auto_null source
  refused (snd_pcm_hw_params); engine correctly logged the warning
  and continued in playback-only mode. TX=0 frames, RX=0 frames
  reflects the headless null-source environment; on a real mic
  host the encoder produces ~50 frames/second while PTT is held.

Honest Beta scope (NOT in this release):
  - AEC / AGC / NS / HPF DSP (DEC-007..010): AudioEffects exists
    as a struct but the filters are no-ops. Beta+ work.
  - Production-quality resampler: current code is linear
    interpolation. Beta+ work.
  - Identity persistence via chanora_storage: still ephemeral.
  - Push-to-Dart event stream: UI polls instead.
  - chanora_diagnostics tracing-layer wiring: still scaffold.
  - Mobile (Android) cdylib + UI: PoC-proven, not yet in product.
  - Reconnect / network-loss recovery for the voice path.

Docs updates:
  - docs/governance/product-decision-register.md bumped to v0.9.7
    (Beta-milestone change-history entry; no row changes).
  - docs/governance/poc-results-summary.md bumped to v0.6.0
    (RISK-PoC-005 updated with Beta progress).
v0.2.0-beta.1
2026-05-14 22:43:57 +08:00
EdisonJwa 53b176b722 docs(governance): record Alpha milestone in PoC results summary (v0.5.0)
Updates RISK-PoC-005 to 'partially closed' and adds a v0.5.0 change
history entry documenting:
  - the tsclientlib spike promotion into crates/chanora_protocol;
  - the typed ChanoraSession in core/chanora_core wiring the
    protocol API;
  - the FRB 2.12.0 bridge wiring;
  - the Flutter Alpha UI;
  - the empirical verification path through alpha_e2e_test.dart
    and alpha_smoke.rs;
  - the v0.1.0-alpha.1 tag pointing at commit 3bb038c.

No other doc bumps are needed: the decision register stays at v0.9.6
(no new decisions), the audit reports stay at v0.9.3 (no new audit
evidence beyond what the PoCs already provided), the PoC plan stays
at v0.3.0 (all six PoC entries were already PASS).
2026-05-14 21:37:49 +08:00
EdisonJwa 4915ec0a1b feat(alpha): wire connect→snapshot→disconnect end-to-end (v0.1.0-alpha.1)
First Internal Alpha build per DEC-001. Closes the milestone of
'Flutter UI calls Rust via the typed bridge, Rust connects to a
TeamSpeak-compatible server through tsclientlib, returns a typed
snapshot, and disconnects cleanly.' Audio remains Beta scope.

Promotions from PoC:
  poc/tsclientlib-connect-spike  →  crates/chanora_protocol/

New product code:
  crates/chanora_protocol/src/{dto.rs,adapter.rs} — typed boundary
    over tsclientlib. Tokio task owns the Connection; public
    handle communicates via mpsc/oneshot. No tsclientlib types
    cross out of the crate (SAD-067 / SysDes-011 / SysDes-029).
  core/chanora_core/src/lib.rs — ChanoraSession composes the
    protocol crate, enforces the DEC-006 single-connection
    invariant.
  crates/chanora_bridge/src/{api.rs,frb_generated.rs} — FRB 2.12.0
    bridge per DEC-014. cdylib + staticlib + rlib. Typed
    BridgeSnapshot / BridgeChannel / BridgeClient / BridgeError
    DTOs. Process-wide OnceLock<Runtime> + OnceLock<ChanoraSession>.
  flutter_rust_bridge.yaml at repo root.
  apps/chanora_flutter/lib/main.dart — Alpha UI: server form,
    connect button, channel tree, disconnect.
  apps/chanora_flutter/lib/l10n/app_{en,zh}.arb expanded with the
    Alpha key set; ARB metadata reaffirms ADR-008 for
    server-provided content.
  Generated Dart bindings under apps/chanora_flutter/lib/src/rust/.

Empirical verification (2026-05-14):
  Workspace: cargo check + cargo test clean
    (workspace tests: all green).
  Bridge cdylib: target/release/libchanora_bridge.so produced
    (~15 MB).
  Flutter: flutter analyze clean; flutter test runs 3/3 green
    including the alpha_e2e_test that drives the full
      Dart → FRB → chanora_bridge → chanora_core → chanora_protocol
        → tsclientlib → UDP → cn.teamspeak.app
    path. The captured logcat/stdout shows the tsproto resender
    transitioning Connected → Disconnecting → Disconnected on
    clean teardown.

Architecture changes:
  - Removed the chanora_core ↔ chanora_bridge cyclic dependency.
    chanora_core no longer knows the bridge exists; the bridge
    maps from CoreError.
  - chanora_bridge crate's #![forbid(unsafe_code)] lint relaxed
    because FRB-generated glue legitimately uses unsafe at the
    FFI boundary. Hand-written code remains unsafe-free.

Open follow-ups (NOT in this Alpha):
  - Audio capture/playback wiring into chanora_audio
    (Beta scope per DEC-001).
  - Identity persistence via chanora_storage
    (currently regenerated on every connect).
  - Per-message diagnostics + redaction
    (chanora_diagnostics still scaffold).
  - Reconnect / network-loss recovery.
  - Mobile (Android) build of the bridge cdylib + UI verification.
v0.1.0-alpha.1
2026-05-14 21:37:06 +08:00
EdisonJwa e0f34009d9 docs(governance): record product scaffolding paths (DEC-022)
Updates two documents to reflect the workspace + Flutter app
scaffold landed in the previous commit.

path-migration-map.md v0.9.2 -> v0.9.3:
  Adds §3 Implementation Path Layout. Lists each subsystem's
  canonical crate path alongside its SAD / SysDes / DEC authority.
  Notes that the Flutter app is owned by Flutter tooling and is
  not a Cargo workspace member.

CHANGELOG entry under [Unreleased]:
  - Documents the seven new Cargo workspace members and the
    invariants pinned at the workspace level.
  - Documents the Flutter app scaffold, the DEC-004 minSdk = 28
    override, and the DEC-015 English + Simplified Chinese ARB
    catalogue (with the ADR-008 server-content reaffirmation).
  - Records the empirical verification (cargo check + cargo test
    + flutter analyze + flutter test all clean).
2026-05-14 21:02:16 +08:00
EdisonJwa 974dda9601 feat(scaffold): create product workspace + Rust crates + Flutter app
Implements the canonical implementation directory layout adopted by
DEC-022 (register v0.9.5). Closes the scaffolding phase; no PoC code
has been promoted in yet (per proof-of-concept-plan.md §4 a PoC is
not product code unless explicitly promoted).

Rust workspace
==============

Top-level Cargo.toml declares seven workspace members:

  core/chanora_core           top-level Rust API + orchestration
  crates/chanora_protocol     tsclientlib isolation (SAD-067, SysDes-011/029)
  crates/chanora_state        state sync, reducers, deltas
  crates/chanora_audio        capture, DSP, Opus, jitter, mixer, playback
  crates/chanora_storage      non-secret DB + platform secure store
  crates/chanora_diagnostics  logs, redaction, export
  crates/chanora_bridge       typed Flutter/Rust DTOs

Workspace-wide pins:
  license      = "MIT OR Apache-2.0"   (DEC-020)
  rust-version = "1.95"
  edition      = "2021"

The Flutter app (apps/chanora_flutter) is NOT a Cargo workspace
member; it is owned by the Flutter / Gradle toolchain and is in the
workspace exclude array along with every poc/* spike.

Each crate ships:
  * a Cargo.toml referring to workspace.dependencies pins;
  * a lib.rs with #![forbid(unsafe_code)] + #![warn(missing_docs)],
    a typed Error enum, and the public types relevant to the
    subsystem's role per SAD §7.2;
  * minimal unit tests so
running 1 test
test tests::defaults_match_decisions ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 1 test
test tests::it_compiles ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 1 test
test tests::session_can_be_constructed ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 1 test
test tests::marker_matches_poc ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 1 test
test tests::it_compiles ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 1 test
test tests::state_transitions_compile ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 1 test
test tests::it_compiles ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s is non-empty.

chanora_core's CoreError type-wraps every subsystem error via
#[from] so callers can match on origin without parsing strings.

chanora_audio's AudioEffects struct defaults all four effects to
true, matching DEC-007 (AEC), DEC-008 (AGC), DEC-009 (NS),
DEC-010 (HPF). A unit test pins this so a future regression that
flips a default fails immediately.

chanora_diagnostics exports REDACTION_MARKER = "[REDACTED]",
identical to the PoC's marker so audit grep patterns survive the
promotion.

chanora_bridge's BridgeError is Serialize + Deserialize so it can
flow across the FRB 2.x boundary (DEC-014).

Empirical verification: cargo check --workspace clean, cargo test
--workspace clean (7 unit tests + 7 doc-test runners, all passing)
against Rust 1.95.0 stable.

Flutter app
===========

apps/chanora_flutter created with .

DEC-004 applied: minSdk overridden to 28 in
android/app/build.gradle.kts with a comment that points back at the
decision register and forbids lowering it without re-opening DEC-004.

DEC-015 applied: shipped English + Simplified Chinese at MVP.
  - pubspec.yaml gains flutter_localizations + intl + generate:true.
  - l10n.yaml emits lib/l10n/generated/AppL10n (no synthetic package
    — that was removed in Flutter 3.41+).
  - lib/l10n/app_en.arb is the source of truth; lib/l10n/app_zh.arb
    mirrors the key set in zh-Hans. ARB metadata explicitly
    reaffirms ADR-008: server-provided content (channel names,
    nicknames, welcome banners) is preserved verbatim and never
    translated.

lib/main.dart and test/widget_test.dart were rewritten from the
template counter into a minimal localized scaffold that proves
both locales render correctly.

Empirical verification:  reports no issues;
 runs the two locale smoke tests and both pass.
2026-05-14 21:01:59 +08:00
EdisonJwa 0f418f1d7e feat(legal): resolve DEC-020 — dual-license under Apache-2.0 OR MIT
Closes the only previously-open decision in the register. Chanora is
now dual-licensed under either:

  * Apache License, Version 2.0 (LICENSE-APACHE), OR
  * MIT License (LICENSE-MIT)

at the recipient's option. This is the standard Rust-ecosystem
permissive model and is compatible with every direct dependency
in the PoC tree:

  tsclientlib          MIT OR Apache-2.0
  flutter_rust_bridge  MIT
  cpal                 Apache-2.0
  rusqlite             MIT
  keyring              MIT OR Apache-2.0
  hound                Apache-2.0
  ndk-context, jni, android_logger, regex, serde, tokio,
  tracing, thiserror, zeroize, etc.   MIT OR Apache-2.0

and with the Flutter framework's BSD-3-Clause.

Files added:
  - LICENSE-APACHE  Apache 2.0 license text.
  - LICENSE-MIT     MIT license text with the standard 2026 copyright
                    line.

Files updated:
  - LICENSE     Now the dual-license aggregator. Includes the standard
                Apache-2.0 inbound-contribution clause ("Unless you
                explicitly state otherwise, any contribution
                intentionally submitted for inclusion in Chanora by you,
                as defined in the Apache-2.0 license, shall be
                dual-licensed as above, without any additional terms or
                conditions.").
  - NOTICE      Rewritten with the dual-license declaration and an
                inventory of direct dependencies with their upstream
                licenses. Transitive deps remain to be enumerated by
                build tooling (cargo about, Flutter LicenseRegistry).
  - README.md   §License section rewritten to point at LICENSE-APACHE
                and LICENSE-MIT.
  - docs/governance/product-decision-register.md v0.9.5 → v0.9.6:
    DEC-020 status: Open → Accepted. §4 license row updated. §6
    collapsed: every previously-Proposed or Open decision in the
    register is now resolved. DEC-012 legal review remains as a
    release-gating *work* item, but is not an open decision.
  - docs/governance/poc-results-summary.md v0.3.0 → v0.4.0:
    RISK-PoC-003 closed. DEC-020 row moved out of 'Still open'.

This is a license-model commitment, not a substitute for the
DEC-012 legal review. Per DEC-012 the actual legal review work
(transitive-dep OSS obligations, trademark registrability, final
sign-off on the non-affiliation wording) must still be completed
before any public/store release; that is sign-off work, not an
architectural decision.

Decision register state after this commit:
  Accepted:       23 of 23 unique decisions
  Open/Deferred:  0
  Proposed:       0
2026-05-14 20:53:04 +08:00
EdisonJwa a0d1c35461 docs(governance): owner confirmation on remaining 17 decisions (register v0.9.5)
Closes the 'Proposed / Owner Confirmation Required' state for every
decision in the register except DEC-020 (license, explicitly deferred
and now the only public-release-gating decision outstanding).

Accepted as recommended:
  DEC-001 (Alpha → Beta → Public release sequence),
  DEC-002 (all five platforms as MVP target, staged release allowed),
  DEC-003 (iOS minimum: iOS 13),
  DEC-005 (Android target SDK: Play-required API on upload date),
  DEC-006 (single active server connection in MVP),
  DEC-007/008/009/010 (AEC + AGC + NS + HPF defaults),
  DEC-011 (platform-native audio first),
  DEC-012 (legal/trademark/licensing review as a release gate),
  DEC-013 (SQLite or equivalent for non-secret state),
  DEC-016 (no automatic diagnostics upload),
  DEC-017 (crash reporting disabled for MVP),
  DEC-018 (product name: Chanora),
  DEC-019 (drafted non-affiliation wording),
  DEC-021 (Apple App Store SDK gate: Xcode 26+ / iOS 26 SDK+ on/after 2026-04-28).

Modified from the original recommendation by explicit owner ruling:
  - DEC-004: Android minimum raised to API 28 (Android 9.0) from the
    recommended API 24. Rationale: simpler audio path (AAudio stable
    from API 28), narrower compatibility / privacy / scoped-storage
    surface. Affects the Android spike's minSdk=24 in product code:
    apps/chanora_flutter will need minSdk=28.
  - DEC-015: MVP product language expanded to English + Chinese
    (Simplified) from the recommended English-only. Rationale: the
    demonstrated TS3-compatible-server audience (verified live against
    cn.teamspeak.app) and broader TS3 audience include substantial
    Chinese-speaking users. Adds zh-Hans translation, font, and
    text-length-budget work to MVP. Server-provided content is still
    preserved verbatim per ADR-008.

Still Open / Deferred:
  - DEC-020 license model. The only remaining release-gating decision.

Documentation updates:
  - product-decision-register.md → v0.9.5. §3 statuses updated, §4
    renamed Recommended → Accepted with MODIFIED rows annotated,
    §6 collapsed to DEC-020 only, §7 dated and statused for every
    decision, change-history entry added.
  - poc-results-summary.md → v0.3.0. §4 expanded with the
    2026-05-14 owner-confirmation pass table. RISK-PoC-004 closed.
    New RISK-PoC-006 (Android minSdk move 24 → 28) and RISK-PoC-007
    (MVP language expansion to en + zh-Hans) added.
  - CHANGELOG entry under [Unreleased].
2026-05-14 20:46:29 +08:00
EdisonJwa 4c64517e45 docs(governance): promote audio PoC to PASS; close mobile-Android half
Documentation update following the Android audio spike pass.

Decision register (v0.9.3 → v0.9.4):
  - DEC-011.1 promoted from
      'Accepted (desktop: cpal) / Deferred (mobile)'
    to
      'Accepted (desktop: cpal; Android: cpal-on-Oboe) / Deferred (iOS)'.
  - Evidence pointer added: poc/audio-capture-playback-android-spike/
    VERIFICATION.md.

PoC plan (v0.2.0 → v0.3.0):
  - Audio row promoted from PARTIAL PASS to PASS.
  - All six PoC plan entries are now PASS.

PoC results summary (v0.1.0 → v0.2.0):
  - Audio row collapsed into one PASS spanning both spikes.
  - RISK-PoC-001 narrowed from 'mobile audio' to 'iOS audio only'.
  - Toolchain table expanded with Android NDK, cargo-ndk, AGP/
    Gradle/Kotlin, jni/ndk-context/android_logger, and the test
    device.

Cross-spike pointers updated:
  - poc/audio-capture-playback-spike/VERIFICATION.md result and
    follow-up sections updated to reference the Android spike.
  - poc/README.md status table lists both audio spike directories.

CHANGELOG updated under [Unreleased].
2026-05-14 18:25:03 +08:00
EdisonJwa ec21a880d2 feat(poc/audio): add Android mobile audio spike
Closes the mobile half of the PoC plan §2 audio exit criterion
left open by poc/audio-capture-playback-spike. The desktop and
mobile halves together fully retire the audio PoC.

Stack:
  Kotlin (MainActivity) → JNI → Rust cdylib
    → cpal 0.16 → Oboe (AAudio / OpenSL ES) → Android audio HAL

Layout:
  rust crate (src/lib.rs)   — JNI_OnLoad, initContext,
                              playSine440, record1sToFile;
                              panic-catching at JNI boundary;
                              android_logger → logcat
  android/ (Gradle 8.7,     — minSdk 24, compileSdk 34, AGP 8.5.2.
   AGP 8.5.2, Kotlin 1.9.24)  cargoBuildRust task wraps cargo-ndk
                              -P 26 -t <abi> for all four ABIs;
                              wired into preBuild so AGP picks up
                              the produced .so files.

Verified on 2026-05-13 on a physical Motorola Moto G Stylus 5G
(2023), Android 14 SDK 34 arm64-v8a:
  - Playback: 500 ms 440 Hz mono sine, 22,050 frames emitted at
    44.1 kHz through cpal/Oboe/AAudio/device speaker.
  - Capture: 1 s from default input, 42,624 frames written to
    /data/data/app.chanora.poc.audio/files/chanora_poc_capture.wav.
    File pulled via 'adb exec-out run-as ... cat' and confirmed
    by file(1) as 'RIFF (little-endian) data, WAVE audio,
    Microsoft PCM, 16 bit, mono 44100 Hz'. Header bytes
    cross-checked against the reported frame count.

Notes:
  - cpal links libaaudio (introduced API 26), so cargo-ndk targets
    API 26 via -P 26 while the Android module's minSdk stays at 24
    (DEC-004). API 24/25 devices would fall back to OpenSL ES at
    runtime; not exercised here.
  - JNI panic safety: every JNI entry point wraps its body in
    std::panic::catch_unwind and a tracing panic hook routes
    panic messages to logcat under tag 'ChanoraAudioPoC'. Without
    this, cpal panicking inside an extern "system" function would
    abort the process.
  - The emulator AVD chanora-poc-api34 and its system image were
    installed during Phase 0 but emulator verification was skipped
    once the physical-device run succeeded. Real-device evidence
    is stronger.

Surfaced finding: DEC-011.1 mobile half promoted from Deferred to
Accepted for Android in the same docs commit; iOS remains
explicitly Deferred (requires macOS + Xcode hardware).

Authority: PoC plan §2, DEC-011, DEC-011.1.
Not product code; not promoted into chanora_audio.
2026-05-14 18:24:53 +08:00
EdisonJwa eca93a141e build(repo): gitignore Android/Gradle build artifacts
Adds .gradle/, local.properties, and **/jniLibs/**/*.so to the
ignore list. The jniLibs .so files (600 KB - 950 KB per ABI) are
produced by the cargoBuildRust Gradle task wrapping cargo-ndk;
they are regenerated on every build and must not be tracked.

Required by the next commit (the Android audio spike) which
otherwise would try to track those four prebuilt libraries.
2026-05-14 18:24:33 +08:00
EdisonJwa 271d23faf7 docs(governance): record PoC outcomes, owner decisions, and audit evidence
Closes Phases A and D of the post-PoC sequencing.

Decision register (v0.9.2 → v0.9.3):
  - DEC-014 Accepted: flutter_rust_bridge 2.x pinned (closed by
    poc/flutter_rust_bridge_hello).
  - DEC-013.1 Accepted: rusqlite (bundled) (closed by
    poc/sqlite-storage-spike).
  - DEC-013.2 Accepted: Linux secure-storage backend policy —
    Secret Service preferred, keyutils fallback (closed by
    poc/secure-storage-spike; resolves SysRS-053 / SysRS-162
    ambiguity).
  - DEC-011.1 Accepted (desktop: cpal) / Deferred (mobile)
    (closed by poc/audio-capture-playback-spike desktop half only).
  - DEC-022 Accepted: canonical implementation directory layout per
    the README sketch and SAD §7.2.
  - DEC-020 explicitly Deferred by owner; remains a public-release
    blocker.

Audit reports updated with empirical evidence:
  - docs/security/secure-storage-audit-report.md v0.9.3:
    SS-AUD-001/002/003/005/006 = PoC Pass with evidence pointers;
    SS-TC-003 (Linux) Actual Result populated and Status = PoC Pass;
    SS-AUD-004 cross-referenced to diagnostics-redaction PoC;
    findings SS-FIND-001 (closed by DEC-013.2), SS-FIND-002 (keyutils
    session caveat), SS-FIND-003 (non-Linux adapters still open).
  - docs/security/diagnostic-redaction-audit-report.md v0.9.3:
    REDACT-TC-001..010 = PoC Pass with evidence pointers; export
    bundle policy §5 populated for every row; findings
    REDACT-FIND-001 (regex coverage), REDACT-FIND-002
    (tracing-layer integration), REDACT-FIND-003 (cross-spike
    KnownSecretRegistry contract).

PoC plan (v0.1.0 → v0.2.0):
  - Status column added to §2; outcomes recorded.

New doc:
  - docs/governance/poc-results-summary.md v0.1.0 — single-page
    reviewer-facing summary listing each spike's status, the
    toolchain exercised, the owner decisions taken, the audit
    coverage table, and open risks RISK-PoC-001..005 (mobile audio,
    non-Linux secure-storage adapters, license, remaining
    Proposed decisions, no product code yet).

This completes the post-PoC documentation work. Repo is at a clean
pause point: PoC code is committed, owner decisions are recorded,
audit reports carry empirical evidence, and the residual risks are
named in the summary doc.
2026-05-14 12:34:13 +08:00
EdisonJwa 181b3d329d docs(poc): add PoC index and record PoC outcomes in CHANGELOG
Adds poc/README.md as the top-level index across all six PoC spikes,
recording status (5 PASS, 1 PARTIAL PASS), authority, and the
non-promotion rule from proof-of-concept-plan.md §4.

Updates CHANGELOG.md to enumerate the six spikes with their
verification dates and to reference each spike's VERIFICATION.md.

This completes Phase E of the post-bootstrap sequencing:
  E.1  git init + baseline import
  E.2  justfile
  E.3..E.8  six PoC spikes
  E.9  PoC index + CHANGELOG  ← this commit
2026-05-14 12:27:13 +08:00
EdisonJwa d5b53996bc feat(poc/audio): add audio capture/playback spike (partial — desktop only)
Proof-of-concept addressing the audio exit criterion from
docs/architecture/proof-of-concept-plan.md §2:
  "Capture/playback works on at least one desktop and one mobile
   target."

PARTIAL PASS. The desktop half is verified on Linux; the mobile
half is NOT verified by this PoC and remains a documented open gap.

Implements via cpal (matching DEC-011 'platform-native first'):
  - AudioCapture::record_to_wav opens the default input device,
    handles f32/i16/u16 sample formats, down-mixes to mono, writes
    16-bit PCM WAV via hound.
  - AudioPlayback::play_wav opens the default output device, picks
    a stream config matching the WAV, blocks until drained.
  - synth_sine_wav produces a deterministic 440 Hz test signal for
    headless verification of the playback path when no microphone
    is available.
  - Typed AudioError DTO with NoInputDevice, NoOutputDevice,
    DefaultConfig, BuildStream, PlayStream, Wav, Io,
    UnsupportedFormat arms.

Verified on 2026-05-13 (Linux + cpal + PipeWire). Capture stream
opened against the system default input; build failed against the
auto_null source (typed AudioError::BuildStream returned cleanly,
demonstrating the production error path); fallback to synth fired;
playback drove 24,000 frames to completion through
Rust → cpal → ALSA → pcm_pipewire → PipeWire → auto_null.
Both audio.rs tests pass.

Mobile gap (explicit, NOT closed):
  - Android Oboe path not built or run.
  - iOS AVAudioEngine path not built or run.

Surfaced finding for the decision register: DEC-011 does not pin an
audio crate. The PoC uses cpal; production code needs an owner
ruling, ideally after the mobile spike closes the gap.

Out of scope: DSP (HPF/NS/AEC/AGC), Opus encode/decode, jitter
buffer, mixer, latency measurement, bit-exact loopback, device
permission flows. These belong to chanora_audio.

Authority: PoC plan §2, DEC-011, SysDes audio subsystem.
Not product code; not promoted into chanora_audio.
2026-05-14 12:27:04 +08:00
EdisonJwa 06ec6f2965 feat(poc/diagnostics): add diagnostics-redaction spike
Proof-of-concept proving the diagnostics-redaction exit criterion from
docs/architecture/proof-of-concept-plan.md §2:
  "Password and identity-secret samples are redacted."

Full coverage of the audit-report test matrix in
docs/security/diagnostic-redaction-audit-report.md §4
(REDACT-TC-001..010), plus two sanity tests.

The spike ships:
  - RedactionPolicy: typed catalogue of regex rules
    (identity-base64-blob, password-kv, ts3server-url-password,
     authorization-bearer, linux/windows/macos user-path) with
    optional capture-group narrowing.
  - Structured-field redaction keyed on case-insensitive name
    substrings (password, secret, token, ...).
  - Bundle-level switches: chat and channel tree excluded by
    default per audit-report §5.
  - KnownSecretRegistry: literal-substring scrub for secrets the
    host application has already loaded into memory (defense in
    depth that regexes alone cannot guarantee — closes the gap
    behind REDACT-TC-002).
  - Length cap (MAX_PROTOCOL_STRING_LEN = 256) with truncation
    marker for REDACT-TC-009.
  - UTF-8 preserved in non-sensitive fields per REDACT-TC-010 /
    ADR-008.

Test suite (12/12 PASS on 2026-05-13):
  REDACT-TC-001 server password in connection data
  REDACT-TC-002 identity secret in storage error (via KnownSecretRegistry)
  REDACT-TC-003 server URL with password field
  REDACT-TC-004 chat text excluded by default
  REDACT-TC-005 channel name with Unicode excluded by default
  REDACT-TC-006 nickname with Unicode preserved in safe field
  REDACT-TC-007 local file path user segment minimized
  REDACT-TC-008 mixed sensitive bundle (whole-bundle JSON scan)
  REDACT-TC-009 long hostile protocol string truncated
  REDACT-TC-010 multilingual safe text preserved
  + known-secret literal scrub
  + empty registered secret ignored

Out of scope: tracing-subscriber integration, diagnostic export
file format, memory/core dumps, performance, adversarial regex
evasion beyond trivial cases. These belong to chanora_diagnostics.

Authority: PoC plan §2, docs/security/diagnostic-redaction-audit-report.md,
SRS-093, SysRS-152/154/155.
Not product code; not promoted into chanora_diagnostics.
2026-05-14 12:26:49 +08:00
EdisonJwa 52e8d43f69 feat(poc/storage): add sqlite-storage spike
Proof-of-concept proving the SQLite-storage exit criterion from
docs/architecture/proof-of-concept-plan.md §2:
  "Schema, migration, and repository pattern are demonstrated."

Also satisfies the SRS-089 acceptance criteria explicitly:
  "Storage implementation uses an embedded local data store and
   migration mechanism."

Implements:
  - A forward-only Migrator over a fixed Migration list, tracking
    the applied version via PRAGMA user_version. Each migration is
    applied inside an IMMEDIATE transaction; rolled back on failure.
  - Three canonical migrations (initial schema, add nickname,
    add last_connected_at) demonstrating ALTER TABLE flows.
  - A LocalDatabaseRepository implementing both BookmarkRepository
    and SettingsRepository traits.
  - Bookmark.identity_ref is a reference to a secret name, never
    a secret value (cross-checked by the secure-storage spike's
    SS-AUD-001/002 scans). This is the SAD-067 separation.

Test suite (11/11 PASS on 2026-05-13):
  - migrator brings fresh DB to latest version
  - migrator is idempotent (no-op when already current)
  - migrator applies only pending versions (catch-up upgrade)
  - migrator rejects out-of-order versions
  - migrator rejects DB newer than known migrations (downgrade guard)
  - failed migration rolls back atomically
  - bookmark CRUD round-trip
  - bookmark list ordered by recency
  - bookmark UNIQUE(host, identity_ref) enforcement
  - settings upsert + delete
  - open creates file and persists across reopen

Surfaced finding for the decision register: DEC-013 does not pin a
SQLite crate. The PoC uses rusqlite with the bundled feature
(no system libsqlite3 dependency); production code needs an
owner ruling on rusqlite vs. sqlx vs. sea-orm.

Authority: PoC plan §2, SRS-089, SDD-077, SAD-067,
SysDes-033/036/049/091.
Not product code; not promoted into chanora_storage.
2026-05-14 12:26:36 +08:00
EdisonJwa 50c95b61ad feat(poc/storage): add secure-storage spike (Linux)
Proof-of-concept proving the secure-storage exit criterion from
docs/architecture/proof-of-concept-plan.md §2:
  "Secret write/read/delete works through platform secure storage."

Implements a typed SecretStorageRepository trait per ADR-006
(SecureStore + per-platform adapters) and a Linux adapter (the only
adapter in PoC scope) that supports both equivalent Linux backends
per SysRS-053/SysRS-162: Secret Service (libsecret) and kernel
keyutils.

The audit test suite covers:
  SS-AUD-001  identity secret absent from local DB (raw file scan)
  SS-AUD-002  server password absent from local DB
  SS-AUD-003  secrets absent from logs (Secret newtype redaction)
  SS-AUD-005  failure returns safe typed error (NotFound)
  SS-AUD-006  delete removes entry
  SS-TC-003   Linux round-trip set/get/delete

Verified on 2026-05-13 against the local keyutils backend (cargo
test runs need 'keyctl session -' to provide a valid session
keyring under non-interactive shells, documented in the spike
README). The CLI driver additionally observed a real locked
gnome-keyring collection and exercised the typed-error → fallback
path live.

Surfaced finding for the decision register: DEC-013 does not pin
a Linux secure-storage backend policy. Both Secret Service and
keyutils are 'equivalent' per the requirements; production code
needs an owner ruling.

Out of scope: Windows DPAPI, macOS/iOS Keychain, Android Keystore,
SS-AUD-004 (covered by diagnostics-redaction spike), SS-AUD-007/008
(process / migration items).

Authority: PoC plan §2, ADR-006, SDD-078, SRS-091..095,
SysRS-158..162.
Not product code; not promoted into chanora_storage.
2026-05-14 12:26:23 +08:00
EdisonJwa 2bbad5feb9 feat(poc/bridge): add flutter_rust_bridge hello spike
Proof-of-concept proving the Flutter/Rust bridge exit criterion from
docs/architecture/proof-of-concept-plan.md §2:
  "Flutter can call Rust and receive event stream data."

The spike exposes one synchronous fallible command (greet) returning
a typed GreetResult / GreetError DTO, and one async event stream
(counter_stream) emitting typed CounterTick events. The Flutter app
demonstrates both flows on a Material 3 surface; the headless test
suite in test/poc_verification_test.dart exercises the same API
directly through dart:ffi.

Verified on 2026-05-13 (Linux desktop, Flutter 3.41.9 / Dart 3.11.5,
flutter_rust_bridge 2.12.0, Rust 1.95). All three tests pass:
  - greet() returns typed result for valid input
  - greet() surfaces typed error for empty input
  - counterStream() delivers the expected event sequence

Authority: PoC plan §2, DEC-014 (typed Flutter/Rust bridge),
SAD-068, SDD-079, SysDes-049.
Naming note: the PoC plan lists this as flutter-rust-bridge-hello,
but Dart pubspec.yaml package names require underscores; the
directory uses underscores accordingly.
Not product code; not promoted into chanora_bridge.

Layout note: includes the full Flutter platform scaffold (android,
ios, macos, windows, web, linux). Only the Linux desktop target has
been built and verified.
2026-05-14 12:26:09 +08:00
EdisonJwa 02c11ead7e feat(poc/protocol): add tsclientlib connect spike
Proof-of-concept proving the protocol-feasibility exit criterion from
docs/architecture/proof-of-concept-plan.md §2:
  "Rust can connect to a compatible server/test double."

The spike opens a tsclientlib connection, waits for the BookEvents
state snapshot, subscribes to the server channel tree, prints
server metadata and the channel tree with client names, and
disconnects cleanly. Audio feature is disabled because audio is
covered by a separate PoC.

Verified on 2026-05-13 against cn.teamspeak.app (TeamSpeak 3 server
3.13.7); 36 channels and 5 online clients retrieved with full
UTF-8 (CJK) preservation. See
poc/tsclientlib-connect-spike/VERIFICATION.md for the captured run.

Authority: PoC plan §2, SysRS-005, SysDes-011, SysDes-029.
Not product code; not promoted into chanora_protocol.
2026-05-14 12:25:48 +08:00
EdisonJwa bdeab3b451 build(repo): add justfile to complete bootstrap v0.1.0
Adds the local task runner required by
docs/governance/repository-bootstrap-plan.md v0.1.0 §3, with stubs
for format, lint, test, verify-docs, and security-scan.

Closes the last gap in repository-bootstrap-plan v0.1.0; CI workflow
files remain deferred per the plan.
2026-05-14 12:25:38 +08:00
EdisonJwa f1bc9a6c85 chore(repo): initial baseline import (docs v0.9.2 + bootstrap)
Imports the v0.9.2 documentation baseline and the bootstrap files
required by docs/governance/repository-bootstrap-plan.md v0.1.0 §3,
minus the justfile (added in the next commit).

This commit establishes the git history for the project. All previous
work lived only as filesystem state with no version control.
2026-05-14 12:25:33 +08:00