Compare commits

..
Author SHA1 Message Date
Edison Jwa d97118bb6a docs: update spec and plan for Docusaurus consistency 2026-06-13 03:39:23 +09:00
Edison Jwa 033cf2d7aa chore: update docs submodule to latest (723/723 traceability) 2026-06-13 03:35:30 +09:00
Edison Jwa bba6273af7 refactor: restructure docs as submodule, add dev-docs/ and AGENTS.md
- Move ASPICE docs to chanoraapp/docs submodule at docs/
- Move development docs to dev-docs/ (superpowers, offline-knowledge, impl-mapping)
- Add AGENTS.md with project conventions for AI agents
- Add impl-mapping.md (SAD component → source file mapping)
- Archive completed plans to dev-docs/superpowers/plans/_archived/
- Remove AGENTS.md from .gitignore (now tracked)
2026-06-13 03:32:33 +09:00
Edison Jwa 5765e9cf6f docs: fix final 3 count errors (Dart tests, integration tests, doc count)
- Dart tests: 233→221 (verified with rg count)
- Rust integration tests: 6→5 (5 .rs files in tests/ dirs)
- Doc file count: 86→66 (excluding offline-knowledge/)
2026-06-13 02:36:22 +09:00
Edison Jwa 2e07d5fd4a docs: fix all remaining review issues for 10/10 accuracy
function-inventory.md:
- Fix SnapshotStateMapper → OwnClientSnapshotState + add 3 missing functions
- Fix enum variant counts: CoreError 7→12, ProtocolError 9→10, BridgeError 8→7, AudioError 7→8

coverage-analysis.md:
- Fix total Dart tests 221→233
- Fix chat_views_test.dart count 18→29
- Fix widget test count 13→14, add audio_device_list_tile_test.dart
- Fix total doc count 55→86

doc-quality-analysis.md:
- Relabel 'Useless Content' → 'Path Record Files (DV Navigation Aids)'
- Soften 'identical' → 'overlapping' for commit examples

README.md:
- Update Dart test count 221→233
2026-06-13 02:30:23 +09:00
Edison Jwa f7b0e841f4 docs: apply review corrections and add second-pass review reports
Corrections:
- README: add 6 missing files to index, fix integration test count (2→6)
- coverage-analysis: fix chanora_audio tests (221→333), chanora_core (11→38), integration tests (2→6)
- docs-out-of-date: remove false maintainability-review claim
- link-coverage-report + docs-link-not-covered: fix line number (12→11)

New review reports (5):
- function-inventory-review.md (score: 7/10)
- coverage-docquality-review.md (coverage: 4/10, docquality: 7/10)
- mismatch-outofdate-review.md (mismatch: 8/10, outofdate: 9/10)
- link-reports-review.md (link: 8/10, notcovered: 7/10)
- external-index-review.md (external: 8-9/10, index: 6/10)
2026-06-13 02:26:58 +09:00
Edison Jwa dce6607317 docs: add deep doc-code analysis (mismatch, out-of-date, link coverage)
- docs-code-mismatch.md: 17 mismatches found (2 critical, 4 major, 11 minor)
- docs-out-of-date.md: 12 outdated docs, 8 undocumented recent changes
- docs-link-not-covered.md: 2 broken links, 9 missing targets, 18 orphaned docs
- reviews/: cross-validation for all 3 analyses, corrections applied

Key findings:
- LICENSE-APACHE/LICENSE-MIT files missing (DEC-020 non-compliance)
- README missing 3 crates (resolver, prefetch, cache)
- CHANGELOG missing v0.3.0+ entries
- SAD/SDD missing file transfer, poke, desktop VAD, chanora_cache
2026-06-13 02:14:37 +09:00
Edison Jwa dd6e80f72a docs: add offline knowledge library with project analysis and external references
- function-inventory: complete public API for 10 Rust crates + 56 Dart files
- coverage-analysis: 312 Rust tests, 221 Dart tests, doc coverage gaps
- doc-quality-analysis: duplications, broken refs, useless content audit
- link-coverage-report: all internal/external links validated
- external/teaspeak: TeaSpeak voice server architecture & protocol
- external/respeak: ReSpeak org, tsclientlib, tsproto, crypto docs
- external/yatqa-en/de: yat.qa admin tool (English + German)
- reviews/: cross-validation reports for all analyses

All documentation only, no code changes.
2026-06-13 02:00:26 +09:00
Edison Jwa 89bbfa1e18 fix(ios): activate AVAudioSession before connect to fix voice
Root cause: OSStatus 561145187 (!rec) - AVAudioSession was not in
.playAndRecord mode when VoiceProcessingIO tried to start.

Two changes:
1. Activate session BEFORE rust.connect() so it's ready when auto-join runs
2. Defensive activation in Connected handler as backup

This fixes both the !rec error and the subsequent 'voice_in already taken'
error that occurred after auto-join consumed voice_in on failure.
2026-06-10 18:38:11 +09:00
Edison Jwa 0a6ef55937 fix(ios): unblock iOS Debug builds + regenerate FRB for file transfer API (#41)
* fix(ios): preserve Silero VAD symbols in Flutter Debug builds

Flutter Debug builds split user code into a sibling Chanora.debug.dylib
alongside a minimal launcher executable; the six @_cdecl symbols
chanora_silero_vad_* land in the dylib, not the main binary. Two
problems were hiding behind that:

1. Debug.xcconfig only carried -exported_symbol flags, missing the
   load-bearing -u force-undefined flags Release.xcconfig already had.
   Without -u, the linker dropped the Swift @_cdecl symbols (no Swift
   caller exists) before -exported_symbol could re-export them, so the
   dylib shipped without the VAD entry points.

2. verify_silero_exports.sh inspected only ${EXECUTABLE_PATH}, which in
   Debug builds is the launcher stub. Even with the symbols correctly
   landing in the debug dylib, the script falsely reported them missing.

Mirror Release.xcconfig's -u + -exported_symbol pair in Debug.xcconfig
and teach the verify script to prefer Chanora.debug.dylib when it sits
next to the launcher. Release builds are unaffected (single fat binary
remains the inspected target).

* build(flutter): regenerate FRB bindings for file transfer API

Run flutter_rust_bridge_codegen against the post-PR-#40 chanora_bridge
to emit typed Dart bindings for initCache, downloadAvatar, downloadIcon,
clearFileCache, and fileCacheSize. Replaces the dynamic-cast +
NoSuchMethodError-swallowing shims that lib/src/rust/api.dart shipped
as a pre-regen safety net.

Functional change: downloadAvatar/downloadIcon now return Uint8List?
(was List<int>?) directly through the typed dispatch table. Existing
consumers stay source-compatible because Uint8List extends List<int>;
no call sites needed updates.

* fix(ios): set ITSAppUsesNonExemptEncryption to false

App uses only standard/exempt encryption (HTTPS, system-provided crypto);
declaring exempt status removes the App Store export-compliance prompt
at every TestFlight/release upload.
2026-06-10 15:20:59 +09:00
Edison Jwa aa796d7395 feat: file transfer system (avatar/icon download with cacache) (#40)
* docs(architecture): add file transfer design, research, and implementation plan

* feat(cache): add chanora_cache crate with cacache-backed blob cache

- New chanora_cache crate: content-addressed blob store wrapping cacache
- BlobCache API: async put/get/remove/clear/total_size/evict
- Key validation: av_ prefix (32 hex chars), ic_ prefix (decimal digits)
- Cacache provides crash safety, SSRI integrity, content dedup
- Mtime-based eviction via cacache::list_sync + sort by timestamp
- 7 unit tests all passing
- Added to workspace members

* feat(protocol): add file download support for avatars and icons

- Add Request::DownloadFile variant with oneshot reply
- Add ProtocolClient::download_avatar(client_uid) and download_icon(icon_id)
- Track pending file downloads by FiletransferHandle
- Handle StreamItem::FileDownload: read bytes from TCP stream
- Handle StreamItem::FiletransferFailed: map to ProtocolError
- Add ProtocolError::FileTransfer(String) variant
- Add path helper tests for avatar/icon download paths
- No tsclientlib types leak across the adapter boundary

* feat(core): add blob cache wiring and avatar download orchestration

- Add chanora_cache dependency to Cargo.toml
- Add blob_cache field to ChanoraSession (Arc<Mutex<Option<BlobCache>>>)
- Add init_cache() method: creates BlobCache, runs eviction
- Add get_avatar() method: cache-first, download on miss, store in cache
- Add clear_cache() and cache_size() methods for cache management
- Add CoreError::Cache variant for BlobCacheError conversion
- Add avatar_cache integration test

* feat(bridge): add init_cache, download_avatar, and cache management functions

- Add init_cache(dir) bridge function
- Add download_avatar(avatar_hash, client_uid) bridge function
- Add clear_file_cache() and file_cache_size() bridge functions
- Map CoreError::Cache and ProtocolError::FileTransfer in BridgeError

* feat(flutter): add cache initialization wiring and avatar download shims

- Add wireCache() to app_bootstrap using getApplicationCacheDirectory()
- Call wireCache() after wireStorage() in main bootstrap flow
- Add Dart-side initCache and downloadAvatar wrapper shims in api.dart
- Update Cargo.lock for new chanora_cache dependency

* feat(core): FileTransferService with coalescing, throttling, negative cache

- New file_transfer module with FileTransferService struct
- Semaphore(2) throttles concurrent downloads
- In-flight HashMap coalesces duplicate avatar requests
- 5-min negative cache short-circuits ServerRejected misses
- ChanoraSession delegates get_avatar through the service
- connect/disconnect update shared protocol handle
- clear_cache/cache_size delegate to service
- 2 new unit tests (cached hit, negative cache)

* feat(core,bridge): add get_icon with coalescing and negative cache

- FileTransferService::get_icon() mirrors get_avatar pattern
- ChanoraSession::get_icon() delegates through FileTransferService
- Bridge download_icon() exposed for Flutter
- Dart downloadIcon() shim added
- Uses PREFIX_ICON (ic_<crc32u>) cache key format
- 1 new unit test (cached icon hit)

* fix(core,protocol): simplify store_protocol and add download size cap

- store_protocol: always write to shared Arc<Mutex<Option<ProtocolClient>>>;
  the FileTransferService holds the same Arc so it sees updates automatically
- read_download_bytes: reject downloads exceeding 10 MB to prevent
  malicious servers from causing OOM
2026-06-10 11:45:14 +09:00
Edison Jwa 08d7ace25d fix(audio,flutter): restore VAD on iOS and fix Android build
- Add iOS to voiceActivityTransmitAvailable — iOS has CoreML Silero
  VAD pipeline (AppleCoreMlVadWorker) but was excluded by DEC-030
  gating that predated the CoreML integration
- Inline deleted VadWorkerPolicy in android_voice_unit.rs — PR #37
  removed the enum from vad/mod.rs but missed updating Android
2026-06-10 09:18:32 +09:00
Edison Jwa ddd977796f Merge pull request #39 from EdisonJwa/refactor/remove-ios-raw-unit
refactor(audio): remove experimental iOS RemoteIO+WebRTC APM path
2026-06-10 00:53:43 +09:00
Edison Jwa 2b285491d0 refactor(bridge): remove SonoraExperimental from bridge API and regenerate FRB
Remove SonoraExperimental variant from BridgeIosVoiceProcessingMode
and collapse all match arms in the bridge config builder. Regenerate
flutter_rust_bridge bindings and update Podfile.lock.
2026-06-10 00:51:52 +09:00
Edison Jwa 413f247378 fix(audio): inline VAD worker policy for iOS capture callback
PR #37 inlined VadWorkerPolicy into the desktop capture path but missed
the iOS files. Instead of restoring the deleted types, inline the same
direct if-let-Some pattern into ios_voice_unit.rs (the only remaining
iOS backend) and permanently remove VadWorkerPolicy and
callback_vad_worker_policy from vad/mod.rs.

iOS uses Option<AppleCoreMlVadWorker> with &mut self (no Mutex), so the
inline is simpler than the desktop's try_lock pattern.
2026-06-10 00:47:30 +09:00
Edison Jwa 3f9ea4f7b8 refactor(audio): remove experimental iOS RemoteIO+WebRTC APM path
Delete ios_raw_unit.rs (538 lines) and all SonoraExperimental references
from the core audio crate. The experimental RemoteIO path that bypassed
Apple VPIO in favor of software WebRTC APM was never shipped and is no
longer needed. VPIO is the sole production iOS audio backend.

- Delete ios_raw_unit.rs entirely
- Remove Raw variant from IosVoiceBackend enum in engine.rs
- Remove SonoraExperimental from IosVoiceProcessingMode enum
- Simplify validate_for_ios() (single-variant enum, no mode check)
- Remove 2 SonoraExperimental validation tests
- Remove ios_raw_unit module declaration from lib.rs
- Remove include_str!-based debug_wav test for the deleted file
2026-06-10 00:47:09 +09:00
Edison Jwa 2f6d45fb04 feat(audio): desktop Silero ONNX VAD + Windows PTT modernization + MSVC CRT build fix (#37)
* feat(audio): add Silero ONNX VAD with WebRTC fallback

Introduce SileroOnnxVad and SileroOnnxVadWorker for desktop targets. The worker runs Silero v6 ONNX inference on a dedicated thread, accumulating 10 ms frames into the 512-sample 16 kHz input the model expects. Add VadOutput, VoiceActivityDetector trait, and WebRtcFallbackVad to provide a uniform VAD interface with graceful fallback when the ONNX model is unavailable. Wire the new VadBackend variants through AudioProcessingConfig and the snapshot stats so the bridge can report which detector is active.

* feat(audio): integrate desktop VAD worker into capture engine

Wire SileroOnnxVadWorker into the desktop capture path so voice activity can open the transmit gate before encoding. The capture callback now processes all audio through resample, downmix, and VAD unconditionally; transmit_active still gates Opus encoding.

Add new_desktop_audio_processing_state() to construct the config/stats/worker triple, and apply_desktop_vad_backend() to synchronously load or clear the worker on config changes. Override processing_backend to Noop for desktop so bridge diagnostics report the correct backend rather than the iOS-oriented PlatformVoiceProcessing default.

Includes review-driven cleanups: StreamConfig clone to deref per clippy, and a comment explaining why two try_lock calls on silero_vad_worker are structurally necessary (borrow checker requires the policy probe and the fallback path to not share a lock guard because mark_vad_fallback_active takes &mut self).

* fix(audio): modernize Windows PTT to current windows-rs API

Port the Raw Input plus low-level keyboard hook PTT backend to the newer windows-rs patterns: OptionalHandle, Result-returning CreateWindowExW, and None for CallNextHookEx. Replaces the old HHOOK(0) pointer casts. Add deterministic tests for mouse button 4 and 5 press and release driving the gate.

* build(windows): force MSVC release CRT for audiopus cmake builds

audiopus_sys calls cmake::build(opus_path), so downstream Cargo env cannot use cmake-rs Config::define() to override CMake's MSVC Debug CRT defaults. Point cmake-rs at a small wrapper that injects the policy and cache variables during configure while passing cmake --build, --version, and -E through unchanged. Keeps Opus Debug builds on Rust's release dynamic CRT (/MD) instead of CMake's default debug CRT (/MDd), which otherwise pulls in unresolved __imp__CrtDbgReportW symbols at test link.

Document that the iOS deployment target is intentionally absent from this file. It is enforced by tools/build-ios.sh and the Xcode project; setting it globally here would make native macOS cargo check runs try to link iPhone objects against the macOS SDK.

* build(flutter): update pubspec.lock after plugin additions

Regenerated lockfile reflecting the local_notifications and connectivity_plus plugin additions from the poke-notifications feature.

* fix(audio): address PR #37 review findings

Six fixes from independent PR review:

1. BLOCKER: Replace Windows-only cmake .cmd wrapper with cross-platform
   CMake env vars. Setting CMAKE=tools/cmake-msvc-release-crt.cmd
   globally broke non-Windows hosts because cmake-rs would try to
   execute a .cmd file on macOS/Linux. Instead, set
   CMAKE_POLICY_DEFAULT_CMP0091=NEW and CMAKE_MSVC_RUNTIME_LIBRARY=
   MultiThreadedDLL as env vars that CMake reads natively. MSVC-
   specific vars are safely ignored by GCC/Clang toolchains. Delete
   the now-unnecessary wrapper script.

2. IMPORTANT: Join the Silero worker thread in Drop instead of
   detaching it. The old code dropped the JoinHandle which detaches
   the thread; the new code calls handle.join() after closing the
   channel, ensuring the ONNX session is cleaned up before the
   worker is replaced during config changes.

3. IMPORTANT: Single-try_lock refactor of the capture VAD callback.
   The double try_lock (policy probe + send) is replaced by a single
   scoped try_lock that both probes availability and sends the frame.
   The guard is dropped before the fallback path, which needs &mut
   self for mark_vad_fallback_active. This also eliminates the
   VadWorkerPolicy enum and callback_vad_worker_policy function,
   whose behavior is now inlined into the callback.

4. IMPORTANT: Remove tracing from the realtime capture callback.
   mark_vad_fallback_active and sync_vad_backend emitted info!/warn!
   from the audio thread. Replace with silent atomic state
   publishing via SharedAudioProcessingStats; the bridge stats
   stream already exposes vad_fallback_active for diagnostics.

5. IMPORTANT: Defer ONNX model load outside the worker mutex.
   apply_desktop_vad_backend_to_worker now constructs the new worker
   before taking the lock, then swaps it in under a short hold.
   This prevents the realtime callback from being blocked during
   model I/O + thread spawn.

6. MINOR: Remove unused VadBackend import from vad/mod.rs after
   deleting the policy code.

* fix(audio): address PR #37 second-pass review findings

5-agent review found 5 blocking issues. All addressed:

1. BLOCKER: CMake env vars don't reach CMake cache. Restored .cmd wrapper
   but scoped to Windows MSVC targets only via [target.x86_64-pc-windows-msvc]
   and [target.aarch64-pc-windows-msvc] in .cargo/config.toml. Non-Windows
   hosts are unaffected.

2. BLOCKER: processing_backend normalized in set_audio_processing_config
   on desktop (cfg-gated override to Noop), mirroring startup default.

3. BLOCKER: Model-path reload was already wired via reload_audio_processing_config.
   Fixed misleading doc comment in core/lib.rs.

4. BLOCKER: DEC-030 updated to reflect desktop VoiceActivity enablement.
   Traceability docs (SRS, SysDes, SAD, SDD, implementation-status) updated.

5. Silero ONNX cfg narrowed to desktop-only (excludes macOS/Android).
   Cargo.toml ort dependency target cfg narrowed similarly.

6. Realtime callback debt documented as TODO at CaptureState::ingest.

* fix(audio): exclude ort dep on Android target

ort does not provide first-class Android prebuilts in our pin, mirror the
iOS/macOS exclusion so cargo metadata succeeds for android targets.

* test(audio): fix stale select_ptt_backend import in ptt_privacy

The helper moved out of the ptt_backends submodule onto the crate root;
update the integration test imports so the test compiles again.

* build(windows): scope MSVC release CRT cmake wrapper via Cargo [env]

Cargo's [target.<triple>] table only forwards a fixed allowlist
(linker, runner, rustflags, rustdocflags, ar), so setting CMAKE there
was silently dropped and audiopus_sys kept linking the debug CRT,
producing LNK4098 'MSVCRTD conflicts' and __imp__CrtDbgReportW errors
on x86_64-pc-windows-msvc test builds.

Move the override to Cargo's [env] table using cc/cmake-rs's
target-suffixed CMAKE_<triple> lookup (force=true, relative=true) so it
applies to MSVC targets only and not to host tooling. Add stdout
markers to the wrapper so its invocation is provable in cargo -vv logs.

Verified: cargo test -p chanora_audio --target x86_64-pc-windows-msvc
--lib --no-run now links cleanly; CMakeCache.txt records
CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL and CMP0091=NEW.

* fix(flutter): gate VoiceActivity transmit mode by platform support

VoiceActivity relies on the native VAD worker, which is only wired up
on Windows, Linux, and Android. Showing the option on iOS, macOS, or
web let users select a mode that silently never transmitted.

Add voiceActivityTransmitAvailable + transmitModeSegmentsFor() helpers
in voice_settings_controls.dart, hide the VAD row in voice_compact.dart
and drop the VAD segment from the settings dialog when unsupported.
Keep the legacy const transmitModeSegments for the existing widget test
and add two new tests covering the gated helper.
2026-06-09 20:47:16 +09:00
Edison Jwa 5c3dd70bba fix(ios-audio): activate session before voice joins (#38)
* fix(ios-audio): add voice join session coordinator

* fix(ios-audio): activate session before voice joins

* docs(ios-audio): align activation lifecycle comments

* fix(ios-audio): keep session active when already-in-channel

The 'already in channel' server response (code 0x0302) is treated as a
successful join by _onJoinChannel: the user stays in the channel and
local state is updated to reflect the joined target. But the underlying
voiceJoin call still raises BridgeError_ServerRejected, which the
joinVoiceChannelWithIosAudioSession helper used to interpret as a join
failure and deactivate the iOS audio session. Result: the UI shows the
user as joined while the audio session is dead and capture/playback
remain silent.

Add an isJoinSuccess predicate to the ordering helper. When the
predicate matches, the helper rethrows (so the caller can still run its
success-on-already-joined branch) without deactivating the session.
Wire _onJoinChannel to pass _isAlreadyInChannel as the predicate so the
0x0302 path keeps the session active.

Adds two regression tests covering the success-on-rethrow and the
predicate-false-still-deactivates paths.

* docs(security): regenerate license inventories

Cargo inventory: pick up chanora_resolver bump from 0.1.0 to
0.2.0-beta.1 so it matches the workspace; also adds a trailing newline
so 'cargo about generate' is idempotent in CI license-drift checks.

Flutter inventory: pick up flutter_local_notifications (+ platform
interfaces) and timezone pulled in by the prior notification
permission work.
2026-06-09 19:58:19 +09:00
Edison Jwa ef14c22300 Merge pull request #36 from EdisonJwa/docs/poke-without-message-design
Allow pokes without messages
2026-06-09 09:33:58 +09:00
Edison Jwa b841d3f3e4 docs(security): refresh license inventories 2026-06-09 07:30:23 +09:00
137 changed files with 9707 additions and 27256 deletions
+46 -31
View File
@@ -1,34 +1,49 @@
# Environment variables set for all cargo invocations in this workspace.
# CMAKE_POLICY_VERSION_MINIMUM is required for audiopus_sys's bundled
# Opus CMake build to succeed on CMake 4.x (which removed compatibility
# with cmake_minimum_required < 3.5). audiopus_sys v0.2.2 bundles
# Opus 1.3.1 whose CMakeLists.txt uses a very old minimum version.
# audiopus_sys calls cmake::build(opus_path), so downstream Cargo env cannot
# call cmake-rs Config::define() to override CMake's MSVC Debug CRT defaults.
# Instead, point cmake-rs at a small wrapper that injects -D cache/policy
# variables during configure while passing cmake --build / --version / -E /
# --install / --open through unchanged. This keeps Opus Debug builds on
# Rust's release dynamic CRT (/MD) instead of CMake's default debug CRT
# (/MDd), which otherwise pulls in unresolved __imp__CrtDbgReportW symbols
# at test link.
#
# IMPORTANT: Cargo's `[target.<triple>]` config sections only forward a
# fixed allowlist of keys (linker, runner, rustflags, rustdocflags, ar)
# to build scripts. Arbitrary keys such as `CMAKE` placed under
# `[target.<triple>]` are silently ignored and never reach the
# audiopus_sys build script. cmake-rs (via cc-style env resolution)
# looks up CMAKE in this order:
# 1. CMAKE_<target-triple-with-dashes>
# 2. CMAKE_<target_triple_with_underscores>
# 3. TARGET_CMAKE (or HOST_CMAKE when host == target)
# 4. CMAKE
# We therefore scope the wrapper to Windows MSVC targets by setting the
# target-suffixed variant in the global [env] section. Non-Windows
# hosts (macOS, Linux, iOS, Android) never see CMAKE set and invoke
# `cmake` directly.
#
# NOTE: CMAKE_POLICY_DEFAULT_CMP0091 and CMAKE_MSVC_RUNTIME_LIBRARY cannot
# be set via the process environment because CMake does NOT auto-import
# them into its cache; they must be passed as `-D` definitions, which the
# wrapper does.
#
# iOS deployment target (DEC-003: iOS 13.0 minimum) is NOT set here. It is
# enforced in two places that own the iOS build:
# 1. tools/build-ios.sh — sets IPHONEOS_DEPLOYMENT_TARGET for the cargo
# invocation and bypasses audiopus_sys's CMake build via
# LIBOPUS_STATIC=1 / LIBOPUS_NO_PKG=1 / LIBOPUS_LIB_DIR.
# 2. apps/chanora_flutter/ios/Runner.xcodeproj — sets the Xcode
# IPHONEOS_DEPLOYMENT_TARGET build setting for the final link.
# Setting it globally here would make native macOS `cargo check` runs try
# to link iPhone objects against the macOS SDK.
[env]
CMAKE_POLICY_VERSION_MINIMUM = "3.5"
# iOS builds must set IPHONEOS_DEPLOYMENT_TARGET in the invoking script
# or Xcode build phase. Do not set it globally here: native macOS cargo
# checks also compile bundled C/C++ dependencies, and a global iOS
# deployment target makes clang try to link iPhone objects against the
# macOS SDK.
# iOS target linker flags (DEC-003: minimum deployment target iOS 13.0).
#
# These rustflags pass -miphoneos-version-min=13.0 to the linker, ensuring
# the final binary targets iOS 13.0+. This is defense-in-depth alongside
# the IPHONEOS_DEPLOYMENT_TARGET env var above — the env var affects C
# compilation (cc crate, CMake), while these rustflags affect the final
# link step.
#
# NOTE: The canonical iOS build is done via tools/build-ios.sh, which
# sets LIBOPUS_STATIC=1, LIBOPUS_NO_PKG=1, and LIBOPUS_LIB_DIR to
# bypass audiopus_sys's CMake build entirely.
[target.aarch64-apple-ios]
rustflags = ["-C", "link-arg=-miphoneos-version-min=13.0"]
[target.aarch64-apple-ios-sim]
rustflags = ["-C", "link-arg=-miphonesimulator-version-min=13.0"]
[target.x86_64-apple-ios]
rustflags = ["-C", "link-arg=-miphonesimulator-version-min=13.0"]
# Scope the cmake wrapper to Windows MSVC targets only via the
# target-suffixed env var name that cc/cmake-rs already resolve.
# Force = true so a developer's pre-existing CMAKE_x86_64-pc-windows-msvc
# does not silently bypass the wrapper. Relative = true so the path
# resolves from the workspace root regardless of where cargo is invoked.
CMAKE_x86_64-pc-windows-msvc = { value = "tools/cmake-msvc-release-crt.cmd", force = true, relative = true }
CMAKE_aarch64-pc-windows-msvc = { value = "tools/cmake-msvc-release-crt.cmd", force = true, relative = true }
-1
View File
@@ -125,7 +125,6 @@ opencode.json
/apps/chanora_flutter/macos/Frameworks/
.opencode/
.omo/
AGENTS.md
Screenshot 2026-05-17 at 22.23.07.png
# Xcode archive / export bundles (generated by Product > Archive > Distribute)
+3
View File
@@ -1,3 +1,6 @@
[submodule "silero-coreml"]
path = silero-coreml
url = git@github.com:chanoraapp/silero-coreml.git
[submodule "docs"]
path = docs
url = git@github.com:chanoraapp/docs.git
+111
View File
@@ -0,0 +1,111 @@
# AGENTS.md — Chanora Project Conventions
## Project Overview
Chanora is a cross-platform voice client (Flutter + Rust) targeting TeamSpeak-compatible servers. The project follows ASPICE engineering processes with full traceability from system requirements through verification.
## Repository Structure
```
chanora/ ← Code repo (this one)
├── docs/ → chanoraapp/docs ← Git submodule: ASPICE docs, Docusaurus doc site
├── dev-docs/ ← Local-only development docs
│ ├── superpowers/ ← AI agent specs and plans
│ │ ├── specs/ ← Feature/design specs (active)
│ │ └── plans/ ← Implementation plans (active)
│ │ └── _archived/ ← Completed plans
│ ├── offline-knowledge/ ← Doc maintenance tools, link coverage
│ ├── implementation-status-* ← Code state snapshots
│ └── release/ios-build.md ← Operational build instructions
├── apps/chanora_flutter/ ← Flutter application
├── core/chanora_core/ ← Rust core API + orchestration
├── crates/ ← Rust crates (protocol, audio, state, etc.)
└── dev-docs/impl-mapping.md ← SAD component → source file mapping
```
## Documentation Two-Repo Model
**`docs/` is a git submodule** pointing to the `chanoraapp/docs` repository. It serves a Docusaurus doc site with ASPICE traceability. It is NOT a local directory you can freely create files in.
### What lives where
| Content | Location | Reason |
|---|---|---|
| SysRS, SysDes, SRS, SAD, SDD | `docs/` (submodule) | ASPICE baselines, served on doc site |
| Verification plans (SWE.4/5/6, SYS.4) | `docs/` (submodule) | ASPICE verification evidence |
| Governance, traceability, decision register | `docs/` (submodule) | ASPICE governance |
| Security, privacy, legal | `docs/` (submodule) | Stakeholder-facing |
| UI/UX guidelines, i18n architecture | `docs/` (submodule) | Design references |
| Feature specs and implementation plans | `dev-docs/superpowers/` | Code-coupled, agent working files |
| Link coverage, doc quality analysis | `dev-docs/offline-knowledge/` | Maintenance tools |
| Implementation status snapshots | `dev-docs/` | Code state tracking |
| Source file path references | `dev-docs/impl-mapping.md` | Developer convenience, not ASPICE |
### Rules for agents
1. **Never create or edit files in `docs/`** without understanding it's a submodule. Changes there require committing in the `chanoraapp/docs` repo first, then updating the submodule pointer in this repo.
2. **ASPICE documents do not contain code file paths.** ASPICE traces requirement IDs (e.g., `SysRS-233`, `SRS-045`, `SDD-MOD-009`), not source file paths. If you need to map a component to its source, use or update `dev-docs/impl-mapping.md`.
3. **Specs and plans go in `dev-docs/superpowers/`.** Follow the naming convention: `YYYY-MM-DD-<topic>-design.md` for specs, `YYYY-MM-DD-<topic>.md` for plans.
4. **Completed plans move to `_archived/`.** Once a plan is fully implemented and verified, move it to `dev-docs/superpowers/plans/_archived/`.
5. **Doc site uses Docusaurus.** The `chanoraapp/docs` repo uses Docusaurus 3.10 (Meta-maintained). Do not add MkDocs, mdBook, or other doc site generators.
## ASPICE Traceability Chain
```text
SysRS → SysDes → SRS → SAD (SWE.2) → SDD (SWE.3) → Verification
↓ ↓
SWE.4 (unit) SWE.4/5/6/SYS.4
```
- Requirement IDs are the traceability mechanism, not file paths.
- Every downstream document must reference upstream IDs it traces from.
- Verification plans map to their upstream design/requirements level:
- SYS.4 ← SysDes, SysRS
- SWE.5 ← SAD (SWE.2)
- SWE.4 ← SDD (SWE.3)
- SWE.6 ← SRS
- Requirement IDs should be added to doc front matter `tags:` for traceability browsing.
## Code Architecture
| Component | Location | Responsibility |
|---|---|---|
| Flutter app shell | `apps/chanora_flutter/` | UI, Material 3, navigation, localization |
| Rust core | `core/chanora_core/` | Session orchestration, bridge events |
| Protocol adapter | `crates/chanora_protocol/` | TeamSpeak protocol via tsclientlib |
| State sync | `crates/chanora_state/` | Snapshots, deltas, reducers |
| Audio subsystem | `crates/chanora_audio/` | Capture, DSP, Opus, PTT |
| Storage | `crates/chanora_storage/` | Bookmarks, identity, encryption |
| Diagnostics | `crates/chanora_diagnostics/` | Redaction, logs, export |
| Resolver | `crates/chanora_resolver/` | SRV/TSDNS/DNS resolution |
| Prefetch | `crates/chanora_prefetch/` | Resolution warming, TTL cache |
| Bridge | `crates/chanora_bridge/` | Flutter/Rust typed DTO boundary |
| Cache | `crates/chanora_cache/` | Avatar/icon blob cache |
## Coding Conventions
- **Rust:** Follow workspace `Cargo.toml` structure. Run `cargo check`, `cargo clippy`, `cargo test` before committing.
- **Flutter:** Run `flutter analyze`, `flutter test` before committing.
- **No code comments** unless explicitly requested.
- **Git commits:** Follow convention in `docs/governance/git-commit-message-convention.md` (accessible via submodule).
- **Bridge boundary:** Flutter must not directly depend on protocol-library internals. All cross-boundary communication goes through `chanora_bridge` typed DTOs.
## Verification Commands
```bash
cargo check && cargo clippy && cargo test
cd apps/chanora_flutter && flutter analyze && flutter test
```
## Important References
- Traceability matrix: `docs/governance/traceability-matrix.md`
- Decision register: `docs/governance/product-decision-register.md`
- Security guidelines: `docs/security/security-privacy-legal-guideline.md`
- Release readiness: `docs/release/release-readiness-go-nogo-record.md`
- Doc site repo: `chanoraapp/docs` (Docusaurus)
- Doc site local preview: `cd docs && npm run start`
Generated
+367 -15
View File
@@ -148,12 +148,111 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
"event-listener",
"event-listener 5.4.1",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-channel"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener 2.5.3",
"futures-core",
]
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-executor"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
dependencies = [
"async-task",
"concurrent-queue",
"fastrand",
"futures-lite",
"pin-project-lite",
"slab",
]
[[package]]
name = "async-global-executor"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
dependencies = [
"async-channel 2.5.0",
"async-executor",
"async-io",
"async-lock",
"blocking",
"futures-lite",
"once_cell",
]
[[package]]
name = "async-io"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
dependencies = [
"autocfg",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-lock"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
"event-listener 5.4.1",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
dependencies = [
"async-channel 2.5.0",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener 5.4.1",
"futures-lite",
"rustix",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
@@ -165,6 +264,57 @@ dependencies = [
"syn",
]
[[package]]
name = "async-signal"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-std"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b"
dependencies = [
"async-channel 1.9.0",
"async-global-executor",
"async-io",
"async-lock",
"async-process",
"crossbeam-utils",
"futures-channel",
"futures-core",
"futures-io",
"futures-lite",
"gloo-timers",
"kv-log-macro",
"log",
"memchr",
"once_cell",
"pin-project-lite",
"pin-utils",
"slab",
"wasm-bindgen-futures",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -257,6 +407,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
@@ -308,6 +464,19 @@ dependencies = [
"objc2",
]
[[package]]
name = "blocking"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
"async-channel 2.5.0",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]]
name = "build-target"
version = "0.4.0"
@@ -352,6 +521,32 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cacache"
version = "13.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5063741c7b2e260bbede781cf4679632dd90e2718e99f7715e46824b65670b"
dependencies = [
"async-std",
"digest 0.10.7",
"either",
"futures",
"hex",
"libc",
"memmap2",
"miette",
"reflink-copy",
"serde",
"serde_derive",
"serde_json",
"sha1",
"sha2",
"ssri",
"tempfile",
"thiserror 1.0.69",
"walkdir",
]
[[package]]
name = "cast"
version = "0.3.0"
@@ -475,11 +670,23 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "chanora_cache"
version = "0.2.0-beta.1"
dependencies = [
"cacache",
"tempfile",
"thiserror 2.0.18",
"tokio",
"tracing",
]
[[package]]
name = "chanora_core"
version = "0.2.0-beta.1"
dependencies = [
"chanora_audio",
"chanora_cache",
"chanora_diagnostics",
"chanora_prefetch",
"chanora_protocol",
@@ -515,7 +722,7 @@ name = "chanora_protocol"
version = "0.2.0-beta.1"
dependencies = [
"async-trait",
"base64",
"base64 0.22.1",
"chanora_resolver",
"futures",
"reqwest 0.13.4",
@@ -554,7 +761,7 @@ dependencies = [
name = "chanora_storage"
version = "0.2.0-beta.1"
dependencies = [
"base64",
"base64 0.22.1",
"chacha20poly1305",
"keyring",
"rand 0.8.6",
@@ -1239,6 +1446,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "event-listener"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "event-listener"
version = "5.4.1"
@@ -1256,7 +1469,7 @@ version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"event-listener 5.4.1",
"pin-project-lite",
]
@@ -1559,6 +1772,18 @@ dependencies = [
"time",
]
[[package]]
name = "gloo-timers"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "group"
version = "0.13.0"
@@ -1837,7 +2062,7 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-util",
@@ -2142,6 +2367,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "kv-log-macro"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
dependencies = [
"log",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -2226,6 +2460,9 @@ name = "log"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
dependencies = [
"value-bag",
]
[[package]]
name = "lru-slab"
@@ -2274,6 +2511,15 @@ version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "memmap2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.9.1"
@@ -2283,6 +2529,29 @@ dependencies = [
"autocfg",
]
[[package]]
name = "miette"
version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e"
dependencies = [
"miette-derive",
"once_cell",
"thiserror 1.0.69",
"unicode-width",
]
[[package]]
name = "miette-derive"
version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "mime"
version = "0.3.17"
@@ -2813,6 +3082,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
@@ -2857,6 +3137,20 @@ dependencies = [
"plotters-backend",
]
[[package]]
name = "polling"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "poly1305"
version = "0.8.0"
@@ -3183,6 +3477,18 @@ dependencies = [
"syn",
]
[[package]]
name = "reflink-copy"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13362233b147e57674c37b802d216b7c5e3dcccbed8967c84f0d8d223868ae27"
dependencies = [
"cfg-if",
"libc",
"rustix",
"windows",
]
[[package]]
name = "regex"
version = "1.12.3"
@@ -3218,7 +3524,7 @@ version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"futures-core",
"http",
@@ -3256,7 +3562,7 @@ version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"encoding_rs",
"futures-core",
@@ -3672,6 +3978,17 @@ dependencies = [
"digest 0.10.7",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
[[package]]
name = "sha2"
version = "0.10.9"
@@ -3851,6 +4168,23 @@ dependencies = [
"der",
]
[[package]]
name = "ssri"
version = "9.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7a2b3c2bc9693bcb40870c4e9b5bf0d79f9cb46273321bf855ec513e919082"
dependencies = [
"base64 0.21.7",
"digest 0.10.7",
"hex",
"miette",
"serde",
"sha-1",
"sha2",
"thiserror 1.0.69",
"xxhash-rust",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -4349,7 +4683,7 @@ name = "ts-bookkeeping"
version = "0.1.0"
source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e"
dependencies = [
"base64",
"base64 0.22.1",
"heck",
"itertools 0.14.0",
"num-derive",
@@ -4370,7 +4704,7 @@ version = "0.2.0"
source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e"
dependencies = [
"audiopus",
"base64",
"base64 0.22.1",
"futures",
"git-testament",
"hickory-net",
@@ -4398,7 +4732,7 @@ version = "0.2.0"
source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e"
dependencies = [
"aes",
"base64",
"base64 0.22.1",
"curve25519-dalek-ng",
"eax",
"futures",
@@ -4427,7 +4761,7 @@ name = "tsproto-packets"
version = "0.1.0"
source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e"
dependencies = [
"base64",
"base64 0.22.1",
"bitflags 2.12.1",
"num-derive",
"num-traits",
@@ -4442,7 +4776,7 @@ name = "tsproto-structs"
version = "0.2.0"
source = "git+https://github.com/EdisonJwa/tsclientlib.git?branch=fix%2Fp256-short-coordinate-pad#8b7a3226c692319b714ea1d32fd5ded05911aa40"
dependencies = [
"base64",
"base64 0.22.1",
"csv",
"heck",
"once_cell",
@@ -4455,7 +4789,7 @@ name = "tsproto-structs"
version = "0.2.0"
source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e"
dependencies = [
"base64",
"base64 0.22.1",
"csv",
"heck",
"once_cell",
@@ -4468,7 +4802,7 @@ name = "tsproto-types"
version = "0.1.0"
source = "git+https://github.com/EdisonJwa/tsclientlib.git?branch=fix%2Fp256-short-coordinate-pad#8b7a3226c692319b714ea1d32fd5ded05911aa40"
dependencies = [
"base64",
"base64 0.22.1",
"bitflags 2.12.1",
"curve25519-dalek-ng",
"elliptic-curve",
@@ -4512,6 +4846,12 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-xid"
version = "0.2.6"
@@ -4570,6 +4910,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "value-bag"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -5256,6 +5602,12 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "xxhash-rust"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
[[package]]
name = "yoke"
version = "0.8.2"
@@ -5289,7 +5641,7 @@ dependencies = [
"async-recursion",
"async-trait",
"enumflags2",
"event-listener",
"event-listener 5.4.1",
"futures-core",
"futures-lite",
"hex",
+2
View File
@@ -10,6 +10,7 @@
# crates/chanora_resolver/ — TeamSpeak address resolution
# crates/chanora_state/ — snapshot, deltas, reducers
# crates/chanora_audio/ — capture, DSP, Opus, jitter, mixer
# crates/chanora_cache/ — avatar/icon blob cache (cacache-backed)
# crates/chanora_storage/ — bookmarks, settings, identity refs
# crates/chanora_diagnostics/ — logs, redaction, export
# crates/chanora_prefetch — server-resolution prefetch cache/policy
@@ -30,6 +31,7 @@ members = [
"crates/chanora_state",
"crates/chanora_audio",
"crates/chanora_storage",
"crates/chanora_cache",
"crates/chanora_diagnostics",
"crates/chanora_prefetch",
"crates/chanora_bridge",
@@ -1,6 +1,9 @@
#include? "Pods/Target Support Files/Pods-Chanora/Pods-Chanora.debug.xcconfig"
#include "Generated.xcconfig"
// Mirror Release.xcconfig (see explanation there).
OTHER_LDFLAGS = $(inherited) -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_create -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_destroy -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_reset -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_process -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_last_error -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_free_string
// Mirror Release.xcconfig (see explanation there). `-u` is the load-bearing
// flag: without it the linker drops Swift @_cdecl symbols (no Swift caller)
// before `-exported_symbol` can re-export them, and the verify_silero_exports
// build phase fails the build.
OTHER_LDFLAGS = $(inherited) -Xlinker -u -Xlinker _chanora_silero_vad_create -Xlinker -u -Xlinker _chanora_silero_vad_destroy -Xlinker -u -Xlinker _chanora_silero_vad_reset -Xlinker -u -Xlinker _chanora_silero_vad_process -Xlinker -u -Xlinker _chanora_silero_vad_last_error -Xlinker -u -Xlinker _chanora_silero_vad_free_string -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_create -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_destroy -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_reset -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_process -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_last_error -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_free_string
STRIP_STYLE = non-global
+1 -1
View File
@@ -23,7 +23,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/haptic_kit/ios"
SPEC CHECKSUMS:
chanora_bridge: 26252acdf9ca660ce9c132ad25cd5ad5af467b16
chanora_bridge: 27a03592058709f6f38701343eb51c3a55b02da0
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
haptic_kit: b22c4fbb2aa7b0d66f2891f81a9e950ad2de5758
@@ -46,7 +46,8 @@ import AVFoundation
//
// VoIP configuration is engaged on voice-channel join via the
// `chanora/ios_audio_session` MethodChannel, driven from Dart
// by the BridgeEvent::AudioStarted / AudioStopped lifecycle.
// before `voiceJoin` starts VoiceProcessingIO and again as an
// idempotent guard on the AudioStarted lifecycle.
do {
try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default)
logAudioSessionState(context: "launch-ambient")
@@ -79,8 +80,8 @@ import AVFoundation
}
/// Activate the VoIP audio session. Called from Dart via the
/// `chanora/ios_audio_session` channel when a voice channel join
/// reaches the `BridgeEvent::AudioStarted` stage. Configures
/// `chanora/ios_audio_session` channel before a voice channel join
/// starts VoiceProcessingIO. Configures
/// .playAndRecord + .voiceChat with .mixWithOthers so other apps
/// (Spotify, podcasts) can keep playing alongside the voice
/// channel matching the Telegram group-call UX. Idempotent:
+1 -1
View File
@@ -25,7 +25,7 @@
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<true/>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
+19 -2
View File
@@ -34,6 +34,7 @@ import 'services/prefetch_debouncer.dart';
import 'services/snapshot_state_mapper.dart';
import 'services/ts3_server_link.dart';
import 'services/ui_preferences_service.dart';
import 'services/voice_join_ordering.dart';
import 'src/rust/api.dart' as rust;
import 'src/rust/frb_generated.dart';
import 'src/rust/lib.dart' as rust_err;
@@ -92,7 +93,10 @@ String _kAppVersion = appSemverBaseline;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await RustLib.init();
unawaited(wireStorage());
unawaited(() async {
await wireStorage();
await wireCache();
}());
unawaited(wireConnectivity());
wireAudioLifecycle();
await configureBundledVadModels();
@@ -744,6 +748,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
_reconnectAttempt = null;
_reconnectDelay = null;
});
unawaited(iosAudioSessionController.activate());
unawaited(_refreshSnapshot(recordActivity: false, reportErrors: true));
case rust.BridgeEvent_Lost(:final reason):
_recordUiDiagnostic('connection', 'lost: $reason');
@@ -1157,6 +1162,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
_chatMessages.clear();
});
_releaseFocusedPttIfHeld();
await iosAudioSessionController.activate();
try {
final snap = await rust.connect(
host: (host ?? _hostCtl.text).trim(),
@@ -1301,7 +1307,18 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
});
}
}
await rust.voiceJoin(channelId: ch.id, password: password ?? '');
await joinVoiceChannelWithIosAudioSession(
channelId: ch.id,
password: password ?? '',
voiceJoin: rust.voiceJoin,
activateIosAudioSession: iosAudioSessionController.activate,
deactivateIosAudioSession: iosAudioSessionController.deactivate,
// Server says we are already in the target channel: the user is
// still joined to a voice channel, so the iOS audio session must
// stay active. The catch below converts this rethrow into the
// success-on-already-joined branch.
isJoinSuccess: _isAlreadyInChannel,
);
if (!mounted) return;
setState(() {
_currentVoiceChannelId = ch.id;
@@ -15,15 +15,22 @@ typedef StorageDirectoryProvider = Future<Directory> Function();
typedef StorageInitializer = Future<void> Function(String dir);
Future<void>? _storageInitFuture;
Future<void>? _cacheInitFuture;
Future<void>? _vadBootstrapFuture;
StorageDirectoryProvider _storageDirectoryProvider =
getApplicationSupportDirectory;
StorageDirectoryProvider _cacheDirectoryProvider = getApplicationCacheDirectory;
StorageInitializer _storageInitializer = _defaultStorageInitializer;
StorageInitializer _cacheInitializer = _defaultCacheInitializer;
Future<void> _defaultStorageInitializer(String dir) {
return rust.initStorage(dir: dir);
}
Future<void> _defaultCacheInitializer(String dir) {
return rust.initCache(dir: dir);
}
Future<File> _copyBundledAssetToDocuments({
required String assetPath,
required String fileName,
@@ -121,16 +128,50 @@ Future<void> _wireStorageImpl() async {
}
}
Future<void> wireCache() async {
final existing = _cacheInitFuture;
if (existing != null) {
await existing;
return;
}
final initFuture = _wireCacheImpl();
_cacheInitFuture = initFuture;
await initFuture;
}
Future<void> _wireCacheImpl() async {
var initialized = false;
try {
final dir = await _cacheDirectoryProvider();
await _cacheInitializer(dir.path);
initialized = true;
} catch (_) {
// Best-effort; missing cache just means protocol-owned assets are
// re-downloaded this session.
} finally {
if (!initialized) {
_cacheInitFuture = null;
}
}
}
@visibleForTesting
void debugResetStorageBootstrap({
StorageDirectoryProvider? storageDirectoryProvider,
StorageDirectoryProvider? cacheDirectoryProvider,
StorageInitializer? storageInitializer,
StorageInitializer? cacheInitializer,
}) {
_storageInitFuture = null;
_cacheInitFuture = null;
_vadBootstrapFuture = null;
_storageDirectoryProvider =
storageDirectoryProvider ?? getApplicationSupportDirectory;
_cacheDirectoryProvider =
cacheDirectoryProvider ?? getApplicationCacheDirectory;
_storageInitializer = storageInitializer ?? _defaultStorageInitializer;
_cacheInitializer = cacheInitializer ?? _defaultCacheInitializer;
}
rust.BridgeNetworkState _mapConnectivity(List<ConnectivityResult> results) {
@@ -10,9 +10,9 @@ const iosAudioSessionChannelName = 'chanora/ios_audio_session';
/// launch and leaves it inactive. The session is only switched to
/// `.playAndRecord` + `.voiceChat` (with `.mixWithOthers`) while a
/// voice channel is actually active. This controller is the Dart
/// side of that contract — call [activate] when the Rust engine
/// emits `BridgeEvent::AudioStarted` and [deactivate] on
/// `BridgeEvent::AudioStopped`.
/// side of that contract — call [activate] before the Rust engine
/// starts VoiceProcessingIO and [deactivate] on
/// `BridgeEvent::AudioStopped` or failed joins.
///
/// On non-iOS platforms both methods are no-ops; the platforms
/// handle their own session lifecycle elsewhere (Android via
@@ -0,0 +1,36 @@
typedef VoiceJoinCallback = Future<void> Function({
required BigInt channelId,
required String password,
});
typedef IosVoiceSessionActivation = Future<void> Function();
typedef IosVoiceSessionDeactivation = Future<void> Function();
/// Predicate used to recognise `voiceJoin` errors that the caller treats as a
/// successful join outcome (e.g. the server replied "already in channel").
///
/// When this returns `true` for a thrown error, the iOS audio session is kept
/// active because the user is still considered joined to the channel. The
/// error is still rethrown so the caller can run its success-on-already-joined
/// branch and update local state.
typedef VoiceJoinSuccessPredicate = bool Function(Object error);
Future<void> joinVoiceChannelWithIosAudioSession({
required BigInt channelId,
required String password,
required VoiceJoinCallback voiceJoin,
required IosVoiceSessionActivation activateIosAudioSession,
required IosVoiceSessionDeactivation deactivateIosAudioSession,
VoiceJoinSuccessPredicate? isJoinSuccess,
}) async {
await activateIosAudioSession();
try {
await voiceJoin(channelId: channelId, password: password);
} catch (e) {
if (isJoinSuccess != null && isJoinSuccess(e)) {
rethrow;
}
await deactivateIosAudioSession();
rethrow;
}
}
+24 -4
View File
@@ -226,6 +226,29 @@ String exportDiagnostics() => RustLib.instance.api.crateApiExportDiagnostics();
Future<void> initStorage({required String dir}) =>
RustLib.instance.api.crateApiInitStorage(dir: dir);
/// Configure the bridge blob cache root.
Future<void> initCache({required String dir}) =>
RustLib.instance.api.crateApiInitCache(dir: dir);
/// Resolve avatar bytes through the bridge.
Future<Uint8List?> downloadAvatar({
required String avatarHash,
required String clientUid,
}) => RustLib.instance.api.crateApiDownloadAvatar(
avatarHash: avatarHash,
clientUid: clientUid,
);
/// Resolve icon bytes through the bridge.
Future<Uint8List?> downloadIcon({required BigInt iconId}) =>
RustLib.instance.api.crateApiDownloadIcon(iconId: iconId);
/// Purge cached protocol-owned assets.
Future<void> clearFileCache() => RustLib.instance.api.crateApiClearFileCache();
/// Report the configured file-cache size.
Future<BigInt> fileCacheSize() => RustLib.instance.api.crateApiFileCacheSize();
/// List persisted bookmarks.
Future<List<BridgeBookmark>> listBookmarks() =>
RustLib.instance.api.crateApiListBookmarks();
@@ -1339,11 +1362,8 @@ sealed class BridgeEvent with _$BridgeEvent {
/// Bridge iOS voice-processing mode.
enum BridgeIosVoiceProcessingMode {
/// Shipping VPIO path.
/// Apple VoiceProcessingIO path.
platformVoiceProcessing,
/// Experimental Sonora path.
sonoraExperimental,
}
@freezed
@@ -67,7 +67,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0';
@override
int get rustContentHash => -20394775;
int get rustContentHash => 635684021;
static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig(
@@ -87,6 +87,8 @@ abstract class RustLibApi extends BaseApi {
Future<void> crateApiBridgeInit();
Future<void> crateApiClearFileCache();
Future<BridgeClientProfile> crateApiClientProfile({required BigInt clientId});
Future<BridgeSnapshot> crateApiConnect({
@@ -99,12 +101,21 @@ abstract class RustLibApi extends BaseApi {
Future<void> crateApiDisconnect();
Future<Uint8List?> crateApiDownloadAvatar({
required String avatarHash,
required String clientUid,
});
Future<Uint8List?> crateApiDownloadIcon({required BigInt iconId});
Future<void> crateApiEnableAudioDebugWavDump({required bool enabled});
Stream<BridgeEvent> crateApiEventsStream();
String crateApiExportDiagnostics();
Future<BigInt> crateApiFileCacheSize();
Future<BridgeAudioProcessingConfig> crateApiGetAudioProcessingConfig();
Future<BridgePttBinding> crateApiGetPttBinding();
@@ -121,6 +132,8 @@ abstract class RustLibApi extends BaseApi {
void crateApiHandleRouteChange({required BridgeAudioRoute route});
Future<void> crateApiInitCache({required String dir});
Future<void> crateApiInitStorage({required String dir});
Stream<double> crateApiInputLevelStream();
@@ -320,6 +333,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiBridgeInitConstMeta =>
const TaskConstMeta(debugName: "bridge_init", argNames: []);
@override
Future<void> crateApiClearFileCache() {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 5,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: sse_decode_bridge_error,
),
constMeta: kCrateApiClearFileCacheConstMeta,
argValues: [],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiClearFileCacheConstMeta =>
const TaskConstMeta(debugName: "clear_file_cache", argNames: []);
@override
Future<BridgeClientProfile> crateApiClientProfile({
required BigInt clientId,
@@ -332,7 +372,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 5,
funcId: 6,
port: port_,
);
},
@@ -366,7 +406,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 6,
funcId: 7,
port: port_,
);
},
@@ -396,7 +436,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 7,
funcId: 8,
port: port_,
);
},
@@ -423,7 +463,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 8,
funcId: 9,
port: port_,
);
},
@@ -441,6 +481,68 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiDisconnectConstMeta =>
const TaskConstMeta(debugName: "disconnect", argNames: []);
@override
Future<Uint8List?> crateApiDownloadAvatar({
required String avatarHash,
required String clientUid,
}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(avatarHash, serializer);
sse_encode_String(clientUid, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 10,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_opt_list_prim_u_8_strict,
decodeErrorData: sse_decode_bridge_error,
),
constMeta: kCrateApiDownloadAvatarConstMeta,
argValues: [avatarHash, clientUid],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiDownloadAvatarConstMeta => const TaskConstMeta(
debugName: "download_avatar",
argNames: ["avatarHash", "clientUid"],
);
@override
Future<Uint8List?> crateApiDownloadIcon({required BigInt iconId}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_u_64(iconId, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 11,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_opt_list_prim_u_8_strict,
decodeErrorData: sse_decode_bridge_error,
),
constMeta: kCrateApiDownloadIconConstMeta,
argValues: [iconId],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiDownloadIconConstMeta =>
const TaskConstMeta(debugName: "download_icon", argNames: ["iconId"]);
@override
Future<void> crateApiEnableAudioDebugWavDump({required bool enabled}) {
return handler.executeNormal(
@@ -451,7 +553,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 9,
funcId: 12,
port: port_,
);
},
@@ -484,7 +586,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 10,
funcId: 13,
port: port_,
);
},
@@ -510,7 +612,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
SyncTask(
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_String,
@@ -526,6 +628,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiExportDiagnosticsConstMeta =>
const TaskConstMeta(debugName: "export_diagnostics", argNames: []);
@override
Future<BigInt> crateApiFileCacheSize() {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 15,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_u_64,
decodeErrorData: sse_decode_bridge_error,
),
constMeta: kCrateApiFileCacheSizeConstMeta,
argValues: [],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiFileCacheSizeConstMeta =>
const TaskConstMeta(debugName: "file_cache_size", argNames: []);
@override
Future<BridgeAudioProcessingConfig> crateApiGetAudioProcessingConfig() {
return handler.executeNormal(
@@ -535,7 +664,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 12,
funcId: 16,
port: port_,
);
},
@@ -565,7 +694,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 13,
funcId: 17,
port: port_,
);
},
@@ -592,7 +721,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 14,
funcId: 18,
port: port_,
);
},
@@ -619,7 +748,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 15,
funcId: 19,
port: port_,
);
},
@@ -643,7 +772,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
SyncTask(
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -666,7 +795,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_bool(shouldResume, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -692,7 +821,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(routeClass, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -718,7 +847,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_bridge_audio_route(route, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -736,6 +865,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["route"],
);
@override
Future<void> crateApiInitCache({required String dir}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(dir, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 24,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: sse_decode_bridge_error,
),
constMeta: kCrateApiInitCacheConstMeta,
argValues: [dir],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiInitCacheConstMeta =>
const TaskConstMeta(debugName: "init_cache", argNames: ["dir"]);
@override
Future<void> crateApiInitStorage({required String dir}) {
return handler.executeNormal(
@@ -746,7 +903,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 20,
funcId: 25,
port: port_,
);
},
@@ -776,7 +933,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 21,
funcId: 26,
port: port_,
);
},
@@ -805,7 +962,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 22,
funcId: 27,
port: port_,
);
},
@@ -832,7 +989,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 23,
funcId: 28,
port: port_,
);
},
@@ -859,7 +1016,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 24,
funcId: 29,
port: port_,
);
},
@@ -883,7 +1040,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
SyncTask(
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_String,
@@ -913,7 +1070,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 26,
funcId: 31,
port: port_,
);
},
@@ -943,7 +1100,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 27,
funcId: 32,
port: port_,
);
},
@@ -970,7 +1127,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 28,
funcId: 33,
port: port_,
);
},
@@ -995,7 +1152,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(state, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 34)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1028,7 +1185,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 30,
funcId: 35,
port: port_,
);
},
@@ -1055,7 +1212,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_bridge_audio_route(route, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 31)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 36)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1089,7 +1246,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 32,
funcId: 37,
port: port_,
);
},
@@ -1124,7 +1281,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 33,
funcId: 38,
port: port_,
);
},
@@ -1154,7 +1311,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 34,
funcId: 39,
port: port_,
);
},
@@ -1182,7 +1339,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 35,
funcId: 40,
port: port_,
);
},
@@ -1210,7 +1367,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 36,
funcId: 41,
port: port_,
);
},
@@ -1240,7 +1397,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 37,
funcId: 42,
port: port_,
);
},
@@ -1268,7 +1425,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_bridge_network_state(state, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 38)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 43)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1294,7 +1451,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 39,
funcId: 44,
port: port_,
);
},
@@ -1322,7 +1479,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 40,
funcId: 45,
port: port_,
);
},
@@ -1350,7 +1507,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 41,
funcId: 46,
port: port_,
);
},
@@ -1378,7 +1535,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 42,
funcId: 47,
port: port_,
);
},
@@ -1410,7 +1567,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 43,
funcId: 48,
port: port_,
);
},
@@ -1440,7 +1597,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 44,
funcId: 49,
port: port_,
);
},
@@ -1468,7 +1625,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 45,
funcId: 50,
port: port_,
);
},
@@ -1496,7 +1653,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 46,
funcId: 51,
port: port_,
);
},
@@ -1523,7 +1680,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 47,
funcId: 52,
port: port_,
);
},
@@ -1551,7 +1708,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 48,
funcId: 53,
port: port_,
);
},
@@ -1583,7 +1740,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 49,
funcId: 54,
port: port_,
);
},
@@ -1612,7 +1769,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 50,
funcId: 55,
port: port_,
);
},
@@ -2290,6 +2447,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw == null ? null : dco_decode_box_autoadd_u_64(raw);
}
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw == null ? null : dco_decode_list_prim_u_8_strict(raw);
}
@protected
PermissionStateKind dco_decode_permission_state_kind(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -3249,6 +3412,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
if (sse_decode_bool(deserializer)) {
return (sse_decode_list_prim_u_8_strict(deserializer));
} else {
return null;
}
}
@protected
PermissionStateKind sse_decode_permission_state_kind(
SseDeserializer deserializer,
@@ -4096,6 +4270,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_bool(self != null, serializer);
if (self != null) {
sse_encode_list_prim_u_8_strict(self, serializer);
}
}
@protected
void sse_encode_permission_state_kind(
PermissionStateKind self,
@@ -201,6 +201,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw);
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected
PermissionStateKind dco_decode_permission_state_kind(dynamic raw);
@@ -444,6 +447,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer);
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
PermissionStateKind sse_decode_permission_state_kind(
SseDeserializer deserializer,
@@ -746,6 +752,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer);
@protected
void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self,
SseSerializer serializer,
);
@protected
void sse_encode_permission_state_kind(
PermissionStateKind self,
@@ -203,6 +203,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw);
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected
PermissionStateKind dco_decode_permission_state_kind(dynamic raw);
@@ -446,6 +449,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer);
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
PermissionStateKind sse_decode_permission_state_kind(
SseDeserializer deserializer,
@@ -748,6 +754,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer);
@protected
void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self,
SseSerializer serializer,
);
@protected
void sse_encode_permission_state_kind(
PermissionStateKind self,
@@ -638,12 +638,19 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
selected: _mode == rust.BridgeTransmitMode.continuous,
onTap: () => _setMode(rust.BridgeTransmitMode.continuous),
),
_ModeRow(
label: l10n.voiceModeVoiceActivity,
icon: Icons.graphic_eq,
selected: _mode == rust.BridgeTransmitMode.voiceActivity,
onTap: () => _setMode(rust.BridgeTransmitMode.voiceActivity),
),
// Voice-activity transmit is only honoured by the engine on
// hosts that ship a Chanora-owned VAD pipeline (DEC-030:
// Windows + Linux desktop and Android). iOS / macOS rely
// on Apple VoiceProcessingIO and have no VAD bridge, so
// hiding the row prevents the UI from advertising a
// transmit mode the engine cannot honour.
if (voiceActivityTransmitAvailable)
_ModeRow(
label: l10n.voiceModeVoiceActivity,
icon: Icons.graphic_eq,
selected: _mode == rust.BridgeTransmitMode.voiceActivity,
onTap: () => _setMode(rust.BridgeTransmitMode.voiceActivity),
),
// 3) Release-tail slider (PTT only).
if (isPtt) ...[
@@ -128,7 +128,12 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
VoiceSectionHeader(l10n.voiceModeLabel),
SegmentedButton<rust.BridgeTransmitMode>(
style: voiceSegmentedButtonStyle(theme),
segments: transmitModeSegments,
// DEC-030: hide the voice-activity segment on hosts
// that ship no Chanora-owned VAD pipeline (iOS,
// macOS, web).
segments: transmitModeSegmentsFor(
voiceActivityAvailable: voiceActivityTransmitAvailable,
),
selected: {_mode},
onSelectionChanged: (s) => setState(() => _mode = s.first),
),
@@ -1,3 +1,6 @@
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import '../src/rust/api.dart' as rust;
@@ -10,7 +13,11 @@ ButtonStyle voiceSegmentedButtonStyle(ThemeData theme) {
);
}
/// Transmit mode selector segments.
/// Transmit mode selector segments — full set, all three modes.
///
/// This list is kept stable for legacy call sites and tests; UI
/// surfaces that must respect DEC-030 platform gating should prefer
/// [transmitModeSegmentsFor] with [voiceActivityTransmitAvailable].
const transmitModeSegments = [
ButtonSegment(
value: rust.BridgeTransmitMode.ptt,
@@ -29,6 +36,43 @@ const transmitModeSegments = [
),
];
/// Transmit mode selector segments, optionally dropping the
/// voice-activity entry on hosts that do not ship a VAD pipeline.
///
/// Voice activity transmit is gated by [voiceActivityTransmitAvailable]
/// because the underlying VAD pipeline ships only on Windows, Linux, and
/// Android per DEC-030. Builds for unsupported platforms (iOS, macOS,
/// web) drop the VAD segment entirely so the UI never advertises a
/// transmit mode the engine cannot honour.
List<ButtonSegment<rust.BridgeTransmitMode>> transmitModeSegmentsFor({
required bool voiceActivityAvailable,
}) {
if (voiceActivityAvailable) return transmitModeSegments;
return const [
ButtonSegment(
value: rust.BridgeTransmitMode.ptt,
label: Text('PTT'),
icon: Icon(Icons.radio_button_checked, size: 14),
),
ButtonSegment(
value: rust.BridgeTransmitMode.continuous,
label: Text('Always'),
icon: Icon(Icons.podcasts, size: 14),
),
];
}
/// True when this host advertises VAD transmit per DEC-030.
///
/// The desktop Silero ONNX + WebRTC fallback ships on Windows and
/// Linux; the Android Oboe + WebRTC path covers Android; iOS uses the
/// Apple CoreML Silero VAD pipeline via `vad::apple_coreml`. macOS is
/// still gated until its VAD pipeline is confirmed.
bool get voiceActivityTransmitAvailable {
if (kIsWeb) return false;
return Platform.isWindows || Platform.isLinux || Platform.isAndroid || Platform.isIOS;
}
/// Android hardware/WebRTC selector segments.
const androidProcessingSegments = [
ButtonSegment(
+24 -32
View File
@@ -5,18 +5,18 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d"
sha256: "3b19a47f6ea7c2632760777c78174f47f6aec1e05f0cd611380d4593b8af1dbc"
url: "https://pub.dev"
source: hosted
version: "93.0.0"
version: "96.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b
sha256: "0c516bc4ad36a1a75759e54d5047cb9d15cded4459df01aa35a0b5ec7db2c2a0"
url: "https://pub.dev"
source: hosted
version: "10.0.1"
version: "10.2.0"
args:
dependency: transitive
description:
@@ -133,10 +133,10 @@ packages:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.2.1"
collection:
dependency: transitive
description:
@@ -197,10 +197,10 @@ packages:
dependency: transitive
description:
name: dbus
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91"
url: "https://pub.dev"
source: hosted
version: "0.7.12"
version: "0.7.13"
fake_async:
dependency: transitive
description:
@@ -361,18 +361,18 @@ packages:
dependency: "direct main"
description:
name: haptic_kit
sha256: "39efffa513c9f8ce3cdded8a4423797f69d71c9281779b83727337f3ee1ed9b8"
sha256: "457f825a3413be2651954639bed27bb2987570f75d90c4e8e1cb9be62db2e59d"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.0.1"
hooks:
dependency: transitive
description:
name: hooks
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
version: "2.0.2"
http:
dependency: transitive
description:
@@ -509,14 +509,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev"
source: hosted
version: "0.17.6"
nm:
dependency: transitive
description:
@@ -529,10 +521,10 @@ packages:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
version: "9.4.1"
package_config:
dependency: transitive
description:
@@ -705,10 +697,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
sha256: a2c49fc1fed7140cadd892d765bd47edbe4ac0b9c7e7e3c493dcb58126f99cf0
url: "https://pub.dev"
source: hosted
version: "2.4.23"
version: "2.4.25"
shared_preferences_foundation:
dependency: transitive
description:
@@ -862,10 +854,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c"
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
url: "https://pub.dev"
source: hosted
version: "6.3.30"
version: "6.3.32"
url_launcher_ios:
dependency: transitive
description:
@@ -974,10 +966,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: a1fc9eb9248baa05dfc12ed5b66e377b3e23f095eec078e0371622b9033810d9
sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738
url: "https://pub.dev"
source: hosted
version: "6.2.0"
version: "6.3.0"
xdg_directories:
dependency: transitive
description:
@@ -990,10 +982,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
version: "7.0.1"
yaml:
dependency: transitive
description:
@@ -1003,5 +995,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.11.5 <4.0.0"
flutter: ">=3.38.4"
dart: ">=3.12.0 <4.0.0"
flutter: ">=3.44.0"
@@ -31,6 +31,18 @@ if [ ! -f "${BINARY}" ]; then
exit 1
fi
# Flutter Debug builds split user code into a sibling `<App>.debug.dylib`
# alongside a tiny launcher executable; the @_cdecl symbols live in the
# dylib. Release/Profile builds put everything in the main executable.
# Prefer the dylib when both exist so the check verifies the slice that
# actually carries the symbols.
EXECUTABLE_DIR=$(dirname "${BINARY}")
EXECUTABLE_NAME=$(basename "${BINARY}")
DEBUG_DYLIB="${EXECUTABLE_DIR}/${EXECUTABLE_NAME}.debug.dylib"
if [ -f "${DEBUG_DYLIB}" ]; then
BINARY="${DEBUG_DYLIB}"
fi
REQUIRED_SYMBOLS="
_chanora_silero_vad_create
_chanora_silero_vad_destroy
@@ -0,0 +1,123 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/services/voice_join_ordering.dart';
void main() {
group('joinVoiceChannelWithIosAudioSession', () {
test('activates the iOS audio session before Rust voiceJoin', () async {
final calls = <String>[];
await joinVoiceChannelWithIosAudioSession(
channelId: BigInt.from(42),
password: 'secret',
activateIosAudioSession: () async {
calls.add('activateIosAudioSession');
},
deactivateIosAudioSession: () async {
calls.add('deactivateIosAudioSession');
},
voiceJoin: ({required channelId, required password}) async {
expect(channelId, BigInt.from(42));
expect(password, 'secret');
calls.add('voiceJoin');
},
);
expect(calls, ['activateIosAudioSession', 'voiceJoin']);
});
test('deactivates the iOS audio session when Rust voiceJoin fails',
() async {
final calls = <String>[];
await expectLater(
joinVoiceChannelWithIosAudioSession(
channelId: BigInt.from(42),
password: '',
activateIosAudioSession: () async {
calls.add('activateIosAudioSession');
},
deactivateIosAudioSession: () async {
calls.add('deactivateIosAudioSession');
},
voiceJoin: ({required channelId, required password}) async {
calls.add('voiceJoin');
throw StateError('join rejected');
},
),
throwsStateError,
);
expect(calls, [
'activateIosAudioSession',
'voiceJoin',
'deactivateIosAudioSession',
]);
});
test(
'keeps the iOS audio session active when voiceJoin throws but the '
'error is recognised as already-in-channel (treated as success); '
'still rethrows so the caller runs its success-on-already-joined branch',
() async {
final calls = <String>[];
await expectLater(
joinVoiceChannelWithIosAudioSession(
channelId: BigInt.from(42),
password: '',
activateIosAudioSession: () async {
calls.add('activateIosAudioSession');
},
deactivateIosAudioSession: () async {
calls.add('deactivateIosAudioSession');
},
voiceJoin: ({required channelId, required password}) async {
calls.add('voiceJoin');
throw _FakeAlreadyInChannel();
},
isJoinSuccess: (error) => error is _FakeAlreadyInChannel,
),
throwsA(isA<_FakeAlreadyInChannel>()),
);
expect(calls, ['activateIosAudioSession', 'voiceJoin']);
},
);
test(
'deactivates the iOS audio session when isJoinSuccess returns false '
'for a non-success error',
() async {
final calls = <String>[];
await expectLater(
joinVoiceChannelWithIosAudioSession(
channelId: BigInt.from(42),
password: '',
activateIosAudioSession: () async {
calls.add('activateIosAudioSession');
},
deactivateIosAudioSession: () async {
calls.add('deactivateIosAudioSession');
},
voiceJoin: ({required channelId, required password}) async {
calls.add('voiceJoin');
throw StateError('join rejected');
},
isJoinSuccess: (error) => error is _FakeAlreadyInChannel,
),
throwsStateError,
);
expect(calls, [
'activateIosAudioSession',
'voiceJoin',
'deactivateIosAudioSession',
]);
},
);
});
}
class _FakeAlreadyInChannel implements Exception {}
@@ -13,6 +13,24 @@ void main() {
]);
});
test('gated transmit mode segments drop VAD when unsupported', () {
expect(
transmitModeSegmentsFor(voiceActivityAvailable: false).map((s) => s.value),
[rust.BridgeTransmitMode.ptt, rust.BridgeTransmitMode.continuous],
);
});
test('gated transmit mode segments include VAD when supported', () {
expect(
transmitModeSegmentsFor(voiceActivityAvailable: true).map((s) => s.value),
[
rust.BridgeTransmitMode.ptt,
rust.BridgeTransmitMode.continuous,
rust.BridgeTransmitMode.voiceActivity,
],
);
});
test('shared Android processing segments expose hardware and WebRTC', () {
expect(androidProcessingSegments.map((s) => s.value), [true, false]);
});
+1
View File
@@ -10,6 +10,7 @@ repository.workspace = true
publish.workspace = true
[dependencies]
chanora_cache = { path = "../../crates/chanora_cache" }
chanora_protocol = { path = "../../crates/chanora_protocol" }
chanora_state = { path = "../../crates/chanora_state" }
chanora_audio = { path = "../../crates/chanora_audio" }
+344
View File
@@ -0,0 +1,344 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use chanora_cache::{BlobCache, BlobCacheError, PREFIX_AVATAR, PREFIX_ICON};
use chanora_protocol::{ProtocolClient, ProtocolError};
use tokio::sync::{Mutex, Semaphore, oneshot};
use tracing::warn;
const MAX_CONCURRENT_DOWNLOADS: usize = 2;
const NEGATIVE_CACHE_TTL: Duration = Duration::from_secs(5 * 60);
type InFlightWaiters = Vec<oneshot::Sender<Result<Option<Vec<u8>>, FileTransferError>>>;
/// Errors raised while resolving protocol-owned file assets.
#[derive(Debug, thiserror::Error)]
pub enum FileTransferError {
/// No live protocol client is available for a download.
#[error("not connected")]
NotConnected,
/// The protocol layer failed while downloading the asset.
#[error("protocol error: {0}")]
Protocol(#[from] ProtocolError),
/// The blob cache failed while reading or writing the asset.
#[error("cache error: {0}")]
Cache(#[from] BlobCacheError),
}
impl Clone for FileTransferError {
fn clone(&self) -> Self {
match self {
Self::NotConnected => Self::NotConnected,
Self::Protocol(error) => Self::Protocol(clone_protocol_error(error)),
Self::Cache(error) => Self::Cache(clone_blob_cache_error(error)),
}
}
}
pub struct FileTransferService {
cache: BlobCache,
protocol: Arc<Mutex<Option<ProtocolClient>>>,
semaphore: Arc<Semaphore>,
in_flight: Arc<Mutex<HashMap<String, InFlightWaiters>>>,
negative_cache: Arc<Mutex<HashMap<String, Instant>>>,
}
impl FileTransferService {
pub fn new(cache: BlobCache, protocol: Arc<Mutex<Option<ProtocolClient>>>) -> Self {
Self {
cache,
protocol,
semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_DOWNLOADS)),
in_flight: Arc::new(Mutex::new(HashMap::new())),
negative_cache: Arc::new(Mutex::new(HashMap::new())),
}
}
pub async fn set_protocol(&self, client: Option<ProtocolClient>) {
*self.protocol.lock().await = client;
}
pub async fn get_avatar(
&self,
avatar_hash: &str,
client_uid: &str,
) -> Result<Option<Vec<u8>>, FileTransferError> {
if let Some(bytes) = self.cache.get(PREFIX_AVATAR, avatar_hash).await? {
return Ok(Some(bytes));
}
if self.is_negative_cache_hit(avatar_hash).await {
return Ok(None);
}
let rx = {
let mut in_flight = self.in_flight.lock().await;
if let Some(waiters) = in_flight.get_mut(avatar_hash) {
let (tx, rx) = oneshot::channel();
waiters.push(tx);
Some(rx)
} else {
in_flight.insert(avatar_hash.to_string(), Vec::new());
None
}
};
if let Some(rx) = rx {
return rx.await.unwrap_or_else(|_| {
Err(FileTransferError::Protocol(ProtocolError::Lost(
"coalesced avatar download waiter dropped".to_string(),
)))
});
}
let _permit = self
.semaphore
.acquire()
.await
.expect("file transfer semaphore should stay open");
let result = self.do_download_avatar(avatar_hash, client_uid).await;
self.finish_in_flight(avatar_hash, &result).await;
result
}
pub async fn get_icon(&self, icon_id: u64) -> Result<Option<Vec<u8>>, FileTransferError> {
let icon_key = icon_id.to_string();
let negative_key = format!("ic_{icon_id}");
let in_flight_key = format!("icon_{icon_id}");
if let Some(bytes) = self.cache.get(PREFIX_ICON, &icon_key).await? {
return Ok(Some(bytes));
}
if self.is_negative_cache_hit(&negative_key).await {
return Ok(None);
}
let rx = {
let mut in_flight = self.in_flight.lock().await;
if let Some(waiters) = in_flight.get_mut(&in_flight_key) {
let (tx, rx) = oneshot::channel();
waiters.push(tx);
Some(rx)
} else {
in_flight.insert(in_flight_key.clone(), Vec::new());
None
}
};
if let Some(rx) = rx {
return rx.await.unwrap_or_else(|_| {
Err(FileTransferError::Protocol(ProtocolError::Lost(
"coalesced icon download waiter dropped".to_string(),
)))
});
}
let _permit = self
.semaphore
.acquire()
.await
.expect("file transfer semaphore should stay open");
let result = self.do_download_icon(icon_id).await;
self.finish_in_flight(&in_flight_key, &result).await;
result
}
pub async fn clear_cache(&self) -> Result<(), FileTransferError> {
self.cache.clear().await?;
self.negative_cache.lock().await.clear();
Ok(())
}
pub async fn cache_size(&self) -> Result<u64, FileTransferError> {
Ok(self.cache.total_size().await?)
}
async fn do_download_avatar(
&self,
avatar_hash: &str,
client_uid: &str,
) -> Result<Option<Vec<u8>>, FileTransferError> {
if let Some(bytes) = self.cache.get(PREFIX_AVATAR, avatar_hash).await? {
return Ok(Some(bytes));
}
if self.is_negative_cache_hit(avatar_hash).await {
return Ok(None);
}
let protocol = self.protocol.lock().await;
let client = protocol.as_ref().ok_or(FileTransferError::NotConnected)?;
match client.download_avatar(client_uid).await {
Ok(bytes) => {
self.cache.put(PREFIX_AVATAR, avatar_hash, &bytes).await?;
self.negative_cache.lock().await.remove(avatar_hash);
Ok(Some(bytes))
}
Err(ProtocolError::ServerRejected { .. }) => {
self.negative_cache
.lock()
.await
.insert(avatar_hash.to_string(), Instant::now() + NEGATIVE_CACHE_TTL);
Ok(None)
}
Err(error) => Err(FileTransferError::Protocol(error)),
}
}
async fn do_download_icon(&self, icon_id: u64) -> Result<Option<Vec<u8>>, FileTransferError> {
let icon_key = icon_id.to_string();
let negative_key = format!("ic_{icon_id}");
if let Some(bytes) = self.cache.get(PREFIX_ICON, &icon_key).await? {
return Ok(Some(bytes));
}
if self.is_negative_cache_hit(&negative_key).await {
return Ok(None);
}
let protocol = self.protocol.lock().await;
let client = protocol.as_ref().ok_or(FileTransferError::NotConnected)?;
match client.download_icon(icon_id).await {
Ok(bytes) => {
self.cache.put(PREFIX_ICON, &icon_key, &bytes).await?;
self.negative_cache.lock().await.remove(&negative_key);
Ok(Some(bytes))
}
Err(ProtocolError::ServerRejected { .. }) => {
self.negative_cache
.lock()
.await
.insert(negative_key, Instant::now() + NEGATIVE_CACHE_TTL);
Ok(None)
}
Err(error) => Err(FileTransferError::Protocol(error)),
}
}
async fn finish_in_flight(
&self,
avatar_hash: &str,
result: &Result<Option<Vec<u8>>, FileTransferError>,
) {
let waiters = self.in_flight.lock().await.remove(avatar_hash).unwrap_or_default();
for waiter in waiters {
if waiter.send(result.clone()).is_err() {
warn!(target: "chanora_core", avatar_hash, "avatar download waiter dropped");
}
}
}
async fn is_negative_cache_hit(&self, avatar_hash: &str) -> bool {
let now = Instant::now();
let mut negative_cache = self.negative_cache.lock().await;
match negative_cache.get(avatar_hash).copied() {
Some(expires_at) if expires_at > now => true,
Some(_) => {
negative_cache.remove(avatar_hash);
false
}
None => false,
}
}
}
fn clone_protocol_error(error: &ProtocolError) -> ProtocolError {
match error {
ProtocolError::Invalid(message) => ProtocolError::Invalid(message.clone()),
ProtocolError::DnsFailed { host, reason } => ProtocolError::DnsFailed {
host: host.clone(),
reason: reason.clone(),
},
ProtocolError::Connect(message) => ProtocolError::Connect(message.clone()),
ProtocolError::DisconnectedEarly(message) => {
ProtocolError::DisconnectedEarly(message.clone())
}
ProtocolError::Lost(message) => ProtocolError::Lost(message.clone()),
ProtocolError::Identity(message) => ProtocolError::Identity(message.clone()),
ProtocolError::Timeout => ProtocolError::Timeout,
ProtocolError::ServerRejected { code, message } => ProtocolError::ServerRejected {
code: *code,
message: message.clone(),
},
ProtocolError::Backend(message) => ProtocolError::Backend(message.clone()),
ProtocolError::FileTransfer(message) => ProtocolError::FileTransfer(message.clone()),
}
}
fn clone_blob_cache_error(error: &BlobCacheError) -> BlobCacheError {
match error {
BlobCacheError::Io(message) => BlobCacheError::Io(message.clone()),
BlobCacheError::InvalidKey(message) => BlobCacheError::InvalidKey(message.clone()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_cache_dir(name: &str) -> std::path::PathBuf {
let mut path = std::env::temp_dir();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
path.push(format!("chanora-core-file-transfer-{name}-{nanos}"));
path
}
#[tokio::test]
async fn returns_cached_avatar_without_connection() {
let cache_dir = test_cache_dir("cache-hit");
let cache = BlobCache::new(&cache_dir, 1024).unwrap();
cache.put(PREFIX_AVATAR, "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", b"avatar")
.await
.unwrap();
let service = FileTransferService::new(cache, Arc::new(Mutex::new(None)));
let avatar = service
.get_avatar("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "client")
.await
.unwrap();
assert_eq!(avatar, Some(b"avatar".to_vec()));
let _ = std::fs::remove_dir_all(cache_dir);
}
#[tokio::test]
async fn negative_cache_short_circuits_not_connected() {
let cache_dir = test_cache_dir("negative-cache");
let cache = BlobCache::new(&cache_dir, 1024).unwrap();
let service = FileTransferService::new(cache, Arc::new(Mutex::new(None)));
service
.negative_cache
.lock()
.await
.insert("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6".to_string(), Instant::now() + NEGATIVE_CACHE_TTL);
let avatar = service
.get_avatar("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "client")
.await
.unwrap();
assert_eq!(avatar, None);
let _ = std::fs::remove_dir_all(cache_dir);
}
#[tokio::test]
async fn returns_cached_icon_without_connection() {
let cache_dir = test_cache_dir("icon-cache-hit");
let cache = BlobCache::new(&cache_dir, 1024).unwrap();
cache.put(PREFIX_ICON, "12345", b"icon").await.unwrap();
let service = FileTransferService::new(cache, Arc::new(Mutex::new(None)));
let icon = service.get_icon(12345).await.unwrap();
assert_eq!(icon, Some(b"icon".to_vec()));
let _ = std::fs::remove_dir_all(cache_dir);
}
}
+157 -40
View File
@@ -53,6 +53,7 @@ use chanora_state::channel_join::{
};
mod events;
mod file_transfer;
mod network_diagnostics;
pub mod ptt;
@@ -75,6 +76,7 @@ pub use events::{
NetworkState, PersistedPttBinding, PttDescriptorSnapshot, SessionEvent, VoiceJoinErrorCode,
VoiceJoinSyncState,
};
pub use file_transfer::FileTransferError;
use network_diagnostics::NetworkDiagnostics;
/// Errors that can arise during top-level orchestration.
@@ -92,6 +94,12 @@ pub enum CoreError {
/// Storage error.
#[error("storage: {0}")]
Storage(#[from] chanora_storage::StorageError),
/// Blob-cache failure.
#[error("cache: {0}")]
Cache(#[from] chanora_cache::BlobCacheError),
/// File-transfer failure.
#[error("file transfer: {0}")]
FileTransfer(#[from] FileTransferError),
/// Diagnostics error.
#[error("diagnostics: {0}")]
Diagnostics(#[from] chanora_diagnostics::DiagnosticsError),
@@ -130,7 +138,6 @@ struct SupervisorInner {
}
struct ConnectedState {
protocol: chanora_protocol::ProtocolClient,
audio: Option<chanora_audio::AudioEngine>,
/// Active PTT controller (SDD-088). Owns the platform input
/// backend, the active binding, and the capability watch
@@ -197,6 +204,8 @@ pub struct ChanoraSession {
/// extension). Lives alongside the identity file. Wired by
/// [`Self::init_storage`].
bookmark_store: Arc<Mutex<Option<BookmarkRepository>>>,
protocol: Arc<Mutex<Option<chanora_protocol::ProtocolClient>>>,
file_transfer: Arc<Mutex<Option<Arc<file_transfer::FileTransferService>>>>,
/// Invisible server-address prefetch cache. Warmed by Flutter typing
/// but validated by Rust before Connect can reuse it.
server_prefetch: ServerPrefetcher,
@@ -252,6 +261,8 @@ impl ChanoraSession {
network_tx,
identity_store: Arc::new(Mutex::new(None)),
bookmark_store: Arc::new(Mutex::new(None)),
protocol: Arc::new(Mutex::new(None)),
file_transfer: Arc::new(Mutex::new(None)),
server_prefetch: ServerPrefetcher::new(),
voice_selector: selector,
release_tail,
@@ -273,6 +284,17 @@ impl ChanoraSession {
ConnectionEpoch(epoch)
}
async fn store_protocol(&self, client: Option<chanora_protocol::ProtocolClient>) {
// Always update the shared Arc. The FileTransferService holds
// the same Arc, so it sees the new client automatically — no
// separate set_protocol call needed.
*self.protocol.lock().await = client;
}
async fn take_protocol(&self) -> Option<chanora_protocol::ProtocolClient> {
self.protocol.lock().await.take()
}
/// Wire a directory-backed identity store. Called by the bridge
/// during `bridge_init` once Flutter has resolved the platform
/// app-private storage directory. Subsequent [`Self::connect`]
@@ -337,6 +359,65 @@ impl ChanoraSession {
Ok(())
}
/// Configure the blob cache root.
pub async fn init_cache(&self, dir: &str) -> Result<(), CoreError> {
let cache = chanora_cache::BlobCache::new(dir, 100 * 1024 * 1024)?;
cache.evict().await?;
let service = Arc::new(file_transfer::FileTransferService::new(
cache,
self.protocol.clone(),
));
let mut guard = self.file_transfer.lock().await;
*guard = Some(service);
Ok(())
}
/// Resolve avatar bytes.
pub async fn get_avatar(
&self,
avatar_hash: &str,
client_uid: &str,
) -> Result<Option<Vec<u8>>, CoreError> {
let service = { self.file_transfer.lock().await.clone() };
if let Some(service) = service {
return Ok(service.get_avatar(avatar_hash, client_uid).await?);
}
let protocol = self.protocol.lock().await;
let client = protocol.as_ref().ok_or(CoreError::NotConnected)?;
Ok(Some(client.download_avatar(client_uid).await?))
}
/// Resolve icon bytes.
pub async fn get_icon(&self, icon_id: u64) -> Result<Option<Vec<u8>>, CoreError> {
let service = { self.file_transfer.lock().await.clone() };
if let Some(service) = service {
return Ok(service.get_icon(icon_id).await?);
}
let protocol = self.protocol.lock().await;
let client = protocol.as_ref().ok_or(CoreError::NotConnected)?;
Ok(Some(client.download_icon(icon_id).await?))
}
/// Purge cached protocol-owned assets.
pub async fn clear_cache(&self) -> Result<(), CoreError> {
let service = { self.file_transfer.lock().await.clone() };
if let Some(service) = service {
service.clear_cache().await?;
}
Ok(())
}
/// Report the configured blob-cache size.
pub async fn cache_size(&self) -> Result<u64, CoreError> {
let service = { self.file_transfer.lock().await.clone() };
match service {
Some(service) => Ok(service.cache_size().await?),
None => Ok(0),
}
}
/// List persisted bookmarks. Returns an empty list if the store
/// has not been wired or has no entries.
pub async fn list_bookmarks(&self) -> Result<Vec<Bookmark>, CoreError> {
@@ -514,6 +595,7 @@ impl ChanoraSession {
let supervisor = tokio::spawn(supervisor_loop(SupervisorContext {
state_arc: self.inner.clone(),
protocol: self.protocol.clone(),
events_tx: self.events_tx.clone(),
initial_cfg: cfg.clone(),
initial_lost_rx: lost_rx,
@@ -568,9 +650,9 @@ impl ChanoraSession {
}
spawn_event_forwarders(&client, &self.events_tx);
self.store_protocol(Some(client)).await;
*guard = Some(ConnectedState {
protocol: client,
audio: None,
ptt_controller: None,
cancel_tx: Some(cancel_tx),
@@ -631,9 +713,13 @@ impl ChanoraSession {
/// Return a fresh snapshot of the current server state.
pub async fn snapshot(&self) -> Result<ServerSnapshot, CoreError> {
let snap = {
let protocol = self.protocol.lock().await;
let client = protocol.as_ref().ok_or(CoreError::NotConnected)?;
client.snapshot().await?
};
let mut guard = self.inner.lock().await;
let state = guard.as_mut().ok_or(CoreError::NotConnected)?;
let snap = state.protocol.snapshot().await?;
let current_channel = self
.find_own_in(&snap)
.await
@@ -661,9 +747,9 @@ impl ChanoraSession {
/// Fetch richer profile and live connection details for one online client.
pub async fn client_profile(&self, client_id: u64) -> Result<ClientProfile, CoreError> {
let guard = self.inner.lock().await;
let state = guard.as_ref().ok_or(CoreError::NotConnected)?;
Ok(state.protocol.client_profile(client_id).await?)
let protocol = self.protocol.lock().await;
let client = protocol.as_ref().ok_or(CoreError::NotConnected)?;
Ok(client.client_profile(client_id).await?)
}
/// True if a connection is currently active.
@@ -680,9 +766,9 @@ impl ChanoraSession {
if !should_dispatch_text_message(&message, &target) {
return Ok(());
}
let guard = self.inner.lock().await;
let state = guard.as_ref().ok_or(CoreError::NotConnected)?;
state.protocol.send_text_message(message, target).await?;
let protocol = self.protocol.lock().await;
let client = protocol.as_ref().ok_or(CoreError::NotConnected)?;
client.send_text_message(message, target).await?;
Ok(())
}
@@ -741,11 +827,16 @@ impl ChanoraSession {
// session permanently unable to restart audio without a
// reconnect (the user saw "voice_in already taken" on the
// second channel switch).
let voice_out = state.protocol.voice_out();
let voice_in = state
.protocol
.take_voice_in()
.ok_or(CoreError::Invariant("voice_in already taken"))?;
let (voice_out, voice_in) = {
let protocol = self.protocol.lock().await;
let client = protocol.as_ref().ok_or(CoreError::NotConnected)?;
(
client.voice_out(),
client
.take_voice_in()
.ok_or(CoreError::Invariant("voice_in already taken"))?,
)
};
let gate = AudioTransmitGate::new(cfg.ptt_initial);
cfg.voice_activity_selector = Some(self.voice_selector.clone());
let new_engine = match chanora_audio::AudioEngine::start_with_gate(
@@ -976,10 +1067,11 @@ impl ChanoraSession {
let password_to_send = requested_password
.clone()
.or_else(|| state.channel_passwords.get(&channel_id).cloned());
state
.protocol
.move_to_channel(channel_id, password_to_send)
.await?;
{
let protocol = self.protocol.lock().await;
let client = protocol.as_ref().ok_or(CoreError::NotConnected)?;
client.move_to_channel(channel_id, password_to_send).await?;
}
if let Some(pw) = requested_password {
state.channel_passwords.insert(channel_id, pw);
}
@@ -1004,7 +1096,11 @@ impl ChanoraSession {
if let Some(muted) = output {
state.local_output_muted = muted;
}
state.protocol.set_muted(input, output).await?;
{
let protocol = self.protocol.lock().await;
let client = protocol.as_ref().ok_or(CoreError::NotConnected)?;
client.set_muted(input, output).await?;
}
if let Some(muted) = output {
if let Some(audio) = state.audio.as_ref() {
audio.set_output_muted(muted);
@@ -1116,11 +1212,19 @@ impl ChanoraSession {
/// Configure the preferred Silero ONNX VAD model path on platforms
/// that ship the ONNX detector.
///
/// This does not require an active connection. Running non-iOS
/// audio backends can observe the model-path epoch and reload on
/// the next capture frame when Silero is selected.
/// Set the Silero VAD model path on supported platforms.
///
/// This does not require an active connection. On desktop, the audio
/// engine immediately reloads the Silero ONNX worker if one is active
/// or if the model file is now available at the new path.
pub async fn set_vad_model_path(&self, path: String) -> Result<(), CoreError> {
chanora_audio::vad::set_silero_model_path(&path)?;
let guard = self.inner.lock().await;
if let Some(state) = guard.as_ref() {
if let Some(audio) = state.audio.as_ref() {
audio.reload_audio_processing_config()?;
}
}
Ok(())
}
@@ -1319,11 +1423,12 @@ impl ChanoraSession {
let password_to_send = requested_password
.clone()
.or_else(|| state.channel_passwords.get(&channel_id).cloned());
if let Err(e) = state
.protocol
.queue_move_to_channel(channel_id, password_to_send)
.await
{
let move_result = {
let protocol = self.protocol.lock().await;
let client = protocol.as_ref().ok_or(CoreError::NotConnected)?;
client.queue_move_to_channel(channel_id, password_to_send).await
};
if let Err(e) = move_result {
// TS3 error 0x0302 = `channel_already_in`: we're already
// in the target channel, so this is a no-op success.
// Rolling `in_channel` back to false would break PTT
@@ -1585,7 +1690,9 @@ impl ChanoraSession {
audio.stop();
let _ = self.events_tx.send(SessionEvent::AudioStopped);
}
state.protocol.disconnect().await;
if let Some(protocol) = self.take_protocol().await {
protocol.disconnect().await;
}
// Wait for the supervisor to wind down so we don't race
// a redial against the explicit disconnect.
if let Some(handle) = state.supervisor.take() {
@@ -1647,6 +1754,7 @@ async fn await_supervisor_shutdown(mut handle: JoinHandle<()>, timeout_duration:
struct SupervisorContext {
state_arc: Arc<Mutex<Option<ConnectedState>>>,
protocol: Arc<Mutex<Option<chanora_protocol::ProtocolClient>>>,
events_tx: broadcast::Sender<SessionEvent>,
initial_cfg: ConnectConfig,
initial_lost_rx: oneshot::Receiver<chanora_protocol::DisconnectReason>,
@@ -1781,6 +1889,7 @@ fn spawn_event_forwarders(
async fn supervisor_loop(ctx: SupervisorContext) {
let SupervisorContext {
state_arc,
protocol,
events_tx,
initial_cfg,
initial_lost_rx,
@@ -2050,20 +2159,21 @@ async fn supervisor_loop(ctx: SupervisorContext) {
// Reattach into the session state.
let restart_audio = {
let guard = state_arc.lock().await;
if guard.is_none() {
// Session was disposed mid-reconnect.
return;
}
drop(guard);
let old = protocol.lock().await.replace(new_client);
drop(old);
let mut guard = state_arc.lock().await;
let state = match guard.as_mut() {
Some(s) => s,
None => {
// Session was disposed mid-reconnect.
return;
}
};
// Replace the dead protocol client with the new one.
// The old client's background task either already
// exited (loss notifier fired) or will exit when
// its request channel drops (watchdog path).
let old = std::mem::replace(&mut state.protocol, new_client);
drop(old);
let _ = channel_join::reduce(
&mut state.join_state,
@@ -2093,9 +2203,9 @@ async fn supervisor_loop(ctx: SupervisorContext) {
});
{
let guard = state_arc.lock().await;
if let Some(state) = guard.as_ref() {
spawn_event_forwarders(&state.protocol, &events_tx);
let protocol = protocol.lock().await;
if let Some(client) = protocol.as_ref() {
spawn_event_forwarders(client, &events_tx);
}
}
@@ -2107,8 +2217,15 @@ async fn supervisor_loop(ctx: SupervisorContext) {
};
let mut guard = state_arc.lock().await;
if let Some(state) = guard.as_mut() {
let voice_out = state.protocol.voice_out();
if let Some(voice_in) = state.protocol.take_voice_in() {
let (voice_out, voice_in) = {
let protocol = protocol.lock().await;
let client = match protocol.as_ref() {
Some(client) => client,
None => return,
};
(client.voice_out(), client.take_voice_in())
};
if let Some(voice_in) = voice_in {
let gate = chanora_audio::AudioTransmitGate::new(
audio_cfg.ptt_initial,
);
+50
View File
@@ -0,0 +1,50 @@
use std::env;
use std::path::PathBuf;
use std::process;
use std::time::{SystemTime, UNIX_EPOCH};
#[tokio::test]
async fn get_avatar_returns_cached_bytes_without_connection() {
let tmp = mktemp("chanora_core_avatar_cache_test");
let session = chanora_core::ChanoraSession::new();
session.init_cache(tmp.to_str().unwrap()).await.unwrap();
let cache = chanora_cache::BlobCache::new(&tmp, 100 * 1024 * 1024).unwrap();
let hash = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6";
cache
.put(chanora_cache::PREFIX_AVATAR, hash, b"avatar-bytes")
.await
.unwrap();
let bytes = session
.get_avatar(hash, "client-uid")
.await
.unwrap()
.unwrap();
assert_eq!(bytes, b"avatar-bytes");
let _ = std::fs::remove_dir_all(&tmp);
}
#[tokio::test]
async fn get_avatar_without_cache_or_connection_returns_not_connected() {
let session = chanora_core::ChanoraSession::new();
let err = session
.get_avatar("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "client-uid")
.await
.unwrap_err();
assert!(matches!(err, chanora_core::CoreError::NotConnected));
}
fn mktemp(label: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let p = env::temp_dir()
.join(label)
.join(format!("{}-{nanos}", process::id()));
std::fs::create_dir_all(&p).unwrap();
p
}
+1 -1
View File
@@ -51,7 +51,7 @@ coreaudio-rs = "0.14"
# on the main queue to avoid the VPIO RPC timeout on iOS simulator.
dispatch2 = "0.3"
[target.'cfg(not(target_os = "ios"))'.dependencies]
[target.'cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))'.dependencies]
ort = { version = "2.0.0-rc.12", default-features = false, features = ["load-dynamic", "ndarray", "api-24"] }
[target.'cfg(target_os = "android")'.dependencies]
+20 -35
View File
@@ -375,38 +375,20 @@ impl AndroidCaptureState {
speech: true,
}
} else if vad_backend == crate::VadBackend::SileroOnnx {
match crate::vad::callback_vad_worker_policy(
voice_activity_mode,
vad_backend,
self.silero_vad_worker.is_some(),
) {
crate::vad::VadWorkerPolicy::UseWorker => {
let worker = self
.silero_vad_worker
.as_ref()
.expect("policy checked worker");
let enqueued = worker.try_send(capture_seq, &frame);
if !worker.is_stale(capture_seq) {
let p = worker.latest_probability();
crate::vad::VadOutput {
probability: p,
speech: p >= 0.5,
}
} else if enqueued {
crate::vad::VadOutput {
probability: 0.0,
speech: false,
}
} else {
used_fallback_vad = true;
self.mark_vad_fallback_active(vad_backend);
crate::vad::VoiceActivityDetector::process_10ms(
&mut self.vad_detector,
&frame,
)
if let Some(worker) = self.silero_vad_worker.as_ref() {
let enqueued = worker.try_send(capture_seq, &frame);
if !worker.is_stale(capture_seq) {
let p = worker.latest_probability();
crate::vad::VadOutput {
probability: p,
speech: p >= 0.5,
}
}
crate::vad::VadWorkerPolicy::UseFallback => {
} else if enqueued {
crate::vad::VadOutput {
probability: 0.0,
speech: false,
}
} else {
used_fallback_vad = true;
self.mark_vad_fallback_active(vad_backend);
crate::vad::VoiceActivityDetector::process_10ms(
@@ -414,10 +396,13 @@ impl AndroidCaptureState {
&frame,
)
}
crate::vad::VadWorkerPolicy::NotModelBacked => crate::vad::VadOutput {
probability: 1.0,
speech: true,
},
} else {
used_fallback_vad = true;
self.mark_vad_fallback_active(vad_backend);
crate::vad::VoiceActivityDetector::process_10ms(
&mut self.vad_detector,
&frame,
)
}
} else {
crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame)
+11 -51
View File
@@ -56,10 +56,8 @@ impl AudioRoute {
/// iOS voice-processing mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IosVoiceProcessingMode {
/// Shipping default: Apple VoiceProcessingIO owns AEC/NS/AGC.
/// Apple VoiceProcessingIO owns AEC/NS/AGC.
PlatformVoiceProcessing,
/// Experimental raw capture-processing path.
SonoraExperimental,
}
/// Processing backend selected by policy/config.
@@ -195,28 +193,20 @@ impl AudioProcessingConfig {
"bluetooth_a2dp is output-only and cannot transmit duplex voice".to_string(),
));
}
if self.ios_mode == IosVoiceProcessingMode::PlatformVoiceProcessing
&& (self.processing_backend == AudioBackend::Sonora
|| self.processing_backend == AudioBackend::WebrtcApm
|| self.aec == EffectOwner::Sonora
|| self.aec == EffectOwner::WebrtcApm
|| self.ns == EffectOwner::Sonora
|| self.ns == EffectOwner::WebrtcApm
|| self.agc == EffectOwner::Sonora
|| self.agc == EffectOwner::WebrtcApm)
if self.processing_backend == AudioBackend::Sonora
|| self.processing_backend == AudioBackend::WebrtcApm
|| self.aec == EffectOwner::Sonora
|| self.aec == EffectOwner::WebrtcApm
|| self.ns == EffectOwner::Sonora
|| self.ns == EffectOwner::WebrtcApm
|| self.agc == EffectOwner::Sonora
|| self.agc == EffectOwner::WebrtcApm
{
return Err(AudioError::InvalidAudioProcessingConfig(
"software audio processing cannot be enabled with iOS VoiceProcessingIO"
.to_string(),
));
}
if self.ios_mode == IosVoiceProcessingMode::SonoraExperimental
&& self.processing_backend != AudioBackend::WebrtcApm
{
return Err(AudioError::InvalidAudioProcessingConfig(
"ios raw processing mode requires the WebRTC APM backend".to_string(),
));
}
Ok(())
}
@@ -262,34 +252,6 @@ mod tests {
assert!(config.validate_for_ios().is_err());
}
#[test]
fn raw_processing_allows_full_webrtc_apm_chain() {
let config = AudioProcessingConfig {
ios_mode: IosVoiceProcessingMode::SonoraExperimental,
processing_backend: AudioBackend::WebrtcApm,
aec: EffectOwner::WebrtcApm,
ns: EffectOwner::WebrtcApm,
agc: EffectOwner::WebrtcApm,
..AudioProcessingConfig::default()
};
assert!(config.validate_for_ios().is_ok());
}
#[test]
fn raw_processing_rejects_non_webrtc_apm_backend() {
let config = AudioProcessingConfig {
ios_mode: IosVoiceProcessingMode::SonoraExperimental,
processing_backend: AudioBackend::PlatformVoiceProcessing,
aec: EffectOwner::WebrtcApm,
ns: EffectOwner::WebrtcApm,
agc: EffectOwner::WebrtcApm,
..AudioProcessingConfig::default()
};
assert!(config.validate_for_ios().is_err());
}
#[test]
fn disable_failed_vad_backend_demotes_to_webrtc() {
let mut config = AudioProcessingConfig {
@@ -404,10 +366,8 @@ impl Default for SharedAudioProcessingStats {
}
impl SharedAudioProcessingStats {
/// Store the raw input dBFS level (desktop capture path).
/// Mobile platforms use [`Self::update_capture`] instead, which
/// also records VAD state; this lighter method is for the cpal
/// capture path that has no VAD pipeline.
/// Store the raw input dBFS level for capture paths that do not
/// update the full processing/VAD snapshot on this callback.
pub fn set_input_dbfs(&self, dbfs: f32) {
self.input_dbfs.store(dbfs.to_bits(), Ordering::Relaxed);
}
-9
View File
@@ -314,15 +314,6 @@ mod tests {
rec.stop();
}
#[test]
fn ios_raw_debug_wav_does_not_push_from_realtime_callback() {
let src = include_str!("ios_raw_unit.rs");
assert!(
!src.contains("push_raw_mic") && !src.contains("push_processed_mic"),
"ios raw callbacks must not call WavDebugRecorder push_*_mic until it has a preallocated handoff"
);
}
#[test]
fn wav_header_is_44_bytes() {
// Write to a temp file to test the header.
+640 -50
View File
@@ -307,6 +307,8 @@ pub struct AudioEngine {
output_muted: Arc<AtomicBool>,
audio_processing_config: Arc<Mutex<crate::AudioProcessingConfig>>,
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
silero_vad_worker: Arc<Mutex<Option<crate::vad::silero_onnx::SileroOnnxVadWorker>>>,
#[cfg(not(target_os = "android"))]
audio_handler: Arc<Mutex<AudioHandler<SessionAudioId>>>,
#[cfg(target_os = "android")]
@@ -393,8 +395,6 @@ unsafe impl Sync for AudioEngine {}
#[cfg(any(target_os = "ios", target_os = "macos"))]
enum IosVoiceBackend {
Vpio(crate::ios_voice_unit::IosVoiceUnit),
#[cfg(target_os = "ios")]
Raw(crate::ios_raw_unit::IosRawUnit),
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
@@ -404,14 +404,12 @@ impl IosVoiceBackend {
{
match self {
Self::Vpio(unit) => unit.restart(),
Self::Raw(unit) => unit.restart(),
}
}
#[cfg(target_os = "macos")]
{
match self {
Self::Vpio(_unit) => Ok(()),
}
let _ = self;
Ok(())
}
}
@@ -420,14 +418,12 @@ impl IosVoiceBackend {
{
match self {
Self::Vpio(unit) => unit.pause(),
Self::Raw(unit) => unit.pause(),
}
}
#[cfg(target_os = "macos")]
{
match self {
Self::Vpio(_unit) => Ok(()),
}
let _ = self;
Ok(())
}
}
@@ -436,14 +432,12 @@ impl IosVoiceBackend {
{
match self {
Self::Vpio(unit) => unit.resume(),
Self::Raw(unit) => unit.resume(),
}
}
#[cfg(target_os = "macos")]
{
match self {
Self::Vpio(_unit) => Ok(()),
}
let _ = self;
Ok(())
}
}
}
@@ -452,27 +446,6 @@ impl IosVoiceBackend {
fn open_ios_voice_backend(
params: crate::mobile_voice_backend::VoiceAudioParams,
) -> Result<IosVoiceBackend, AudioError> {
let _cfg = params.audio_processing_config.lock().unwrap().clone();
#[cfg(target_os = "ios")]
{
if _cfg.ios_mode == crate::IosVoiceProcessingMode::SonoraExperimental {
let raw_params = params.clone();
match crate::ios_raw_unit::IosRawUnit::start(raw_params) {
Ok(unit) => {
info!(target: "chanora_audio", "ios: RemoteIO/WebRTC APM backend selected");
return Ok(IosVoiceBackend::Raw(unit));
}
Err(e) => {
warn!(
target: "chanora_audio",
error = %e,
"ios: RemoteIO/WebRTC APM backend failed; falling back to VoiceProcessingIO"
);
}
}
}
}
let unit = crate::ios_voice_unit::IosVoiceUnit::start(params)?;
Ok(IosVoiceBackend::Vpio(unit))
}
@@ -741,7 +714,7 @@ impl AudioEngine {
) -> Option<cpal::Device>
where
DefaultFn: Fn(&cpal::Host) -> Option<cpal::Device>,
AllFn: Fn(&cpal::Host) -> Result<Devices, cpal::DevicesError>,
AllFn: Fn(&cpal::Host) -> Result<Devices, cpal::Error>,
Devices: IntoIterator<Item = cpal::Device>,
{
if let Some(id) = prefer {
@@ -823,8 +796,8 @@ impl AudioEngine {
let frames_received = Arc::new(AtomicU32::new(0));
let output_gain = Arc::new(AtomicU32::new(1.0_f32.to_bits()));
let output_muted = Arc::new(AtomicBool::new(false));
let audio_processing_config = Arc::new(Mutex::new(crate::AudioProcessingConfig::default()));
let audio_processing_stats = Arc::new(crate::SharedAudioProcessingStats::default());
let (audio_processing_config, audio_processing_stats, silero_vad_worker) =
new_desktop_audio_processing_state();
// ---------- Capture ----------
// Capture is best-effort. If the platform default input
@@ -838,6 +811,9 @@ impl AudioEngine {
transmit_flag_for_capture,
frames_sent.clone(),
cfg.mic_gain,
cfg.voice_activity_selector.clone(),
audio_processing_config.clone(),
silero_vad_worker.clone(),
audio_processing_stats.clone(),
);
let (input_stream, capture_active) = match capture_result {
@@ -981,11 +957,13 @@ impl AudioEngine {
Ok(Self {
transmit_gate,
frames_sent,
frames_received,
output_gain,
output_muted,
audio_processing_config,
audio_processing_stats,
silero_vad_worker,
audio_handler,
_input_stream: Mutex::new(input_stream),
_output_stream: Mutex::new(Some(output_stream)),
@@ -1667,15 +1645,40 @@ impl AudioEngine {
/// Apply a voice-processing config after validating iOS invariants.
pub fn set_audio_processing_config(
&self,
config: crate::AudioProcessingConfig,
mut config: crate::AudioProcessingConfig,
) -> Result<(), AudioError> {
#[cfg(target_os = "ios")]
config.validate_for_ios()?;
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
{
config.processing_backend = crate::AudioBackend::Noop;
}
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
self.apply_desktop_vad_backend(&config);
let mut guard = self.audio_processing_config.lock().unwrap();
*guard = config;
Ok(())
}
/// Re-apply the current audio processing config.
///
/// Used by the core layer to trigger VAD worker reload after a
/// model-path change (the epoch increments but the worker is only
/// reconstructed when `set_audio_processing_config` is called).
pub fn reload_audio_processing_config(&self) -> Result<(), AudioError> {
let config = self.audio_processing_config_snapshot();
self.set_audio_processing_config(config)
}
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
fn apply_desktop_vad_backend(&self, config: &crate::AudioProcessingConfig) {
apply_desktop_vad_backend_to_worker(
config,
&self.silero_vad_worker,
self.audio_processing_stats.as_ref(),
);
}
/// Current voice-processing stats snapshot.
pub fn audio_processing_stats(&self) -> crate::AudioProcessingStats {
let config = self.audio_processing_config.lock().unwrap().clone();
@@ -1777,10 +1780,369 @@ fn release_android_audio_mode_for_startup_rollback(
audio_mode_stack.release()
}
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
fn new_desktop_audio_processing_state() -> (
Arc<Mutex<crate::AudioProcessingConfig>>,
Arc<crate::SharedAudioProcessingStats>,
Arc<Mutex<Option<crate::vad::silero_onnx::SileroOnnxVadWorker>>>,
) {
let mut config = crate::AudioProcessingConfig::default();
// Desktop cpal capture does not use a platform voice-processing API.
// The global default (PlatformVoiceProcessing) is correct for iOS/macOS
// VPIO but would mislabel the desktop path in bridge diagnostics and
// set `platform_voice_processing_enabled = true` when no such
// processing exists. Override to Noop; the bridge/UI stats layer
// will then report the accurate backend.
config.processing_backend = crate::AudioBackend::Noop;
let audio_processing_config = Arc::new(Mutex::new(config.clone()));
let audio_processing_stats = Arc::new(crate::SharedAudioProcessingStats::default());
let silero_vad_worker = Arc::new(Mutex::new(None));
apply_desktop_vad_backend_to_worker(
&config,
&silero_vad_worker,
audio_processing_stats.as_ref(),
);
(
audio_processing_config,
audio_processing_stats,
silero_vad_worker,
)
}
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
fn apply_desktop_vad_backend_to_worker(
config: &crate::AudioProcessingConfig,
silero_vad_worker: &Arc<Mutex<Option<crate::vad::silero_onnx::SileroOnnxVadWorker>>>,
audio_processing_stats: &crate::SharedAudioProcessingStats,
) {
if config.vad_backend != crate::VadBackend::SileroOnnx {
let mut worker_guard = silero_vad_worker.lock().unwrap();
if worker_guard.is_some() {
info!(
target: "chanora_audio",
backend = config.vad_backend.as_str(),
"desktop: Silero ONNX VAD worker cleared because another VAD backend is selected"
);
}
*worker_guard = None;
audio_processing_stats.set_vad_fallback_active(false);
return;
}
// Load model and spawn worker BEFORE taking the lock so the
// realtime capture callback is not blocked on try_lock() during
// model I/O + thread spawn. The old worker (if any) is dropped
// after the new one is installed under the short lock hold.
let model_path = crate::vad::silero_model_bundle_path();
info!(
target: "chanora_audio",
path = %model_path,
"desktop: Silero ONNX VAD selected; loading model worker"
);
let new_worker = crate::vad::silero_onnx::SileroOnnxVadWorker::try_new(&model_path);
{
let mut worker_guard = silero_vad_worker.lock().unwrap();
*worker_guard = new_worker;
}
// Check result after releasing the lock. Re-acquire is cheap and
// ensures we log the correct state without holding the mutex.
let worker_installed = silero_vad_worker.lock().unwrap().is_some();
if worker_installed {
info!(
target: "chanora_audio",
path = %model_path,
"desktop: Silero ONNX VAD worker loaded"
);
audio_processing_stats.set_vad_fallback_active(false);
} else {
warn!(
target: "chanora_audio",
path = %model_path,
"desktop: Silero ONNX VAD worker unavailable; WebRTC fallback will be used"
);
audio_processing_stats.set_vad_fallback_active(true);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
#[test]
fn desktop_capture_voice_activity_opens_selector_from_speech() {
let gate = crate::ptt::AudioTransmitGate::new(false);
let selector = Arc::new(crate::TransmitModeSelector::new(gate.clone()));
selector.set_mode(crate::TransmitMode::VoiceActivity);
selector.set_in_channel(true);
let encoder = crate::opus_voice::new_voip_encoder("desktop VAD test").unwrap();
let (voice_out_tx, _voice_out_rx) = mpsc::channel::<OutPacket>(16);
let frames_sent = Arc::new(AtomicU32::new(0));
let voice_out_tx = crate::opus_voice::start_out_packet_worker(
voice_out_tx,
frames_sent,
"desktop-vad-test",
)
.unwrap();
let stats = Arc::new(crate::SharedAudioProcessingStats::default());
let mut capture = CaptureState::new(
encoder,
SAMPLE_RATE,
1,
1.0,
voice_out_tx,
gate.flag_arc(),
Some(selector.clone()),
Arc::new(Mutex::new(crate::AudioProcessingConfig {
vad_backend: crate::VadBackend::WebrtcVad,
..crate::AudioProcessingConfig::default()
})),
Arc::new(Mutex::new(None)),
stats.clone(),
);
let mut voiced = [0.0_f32; crate::frame::FRAME_10MS_SAMPLES];
for (idx, sample) in voiced.iter_mut().enumerate() {
let phase = idx as f32 * 2.0 * std::f32::consts::PI * 220.0 / SAMPLE_RATE as f32;
*sample = phase.sin() * 0.4;
}
for _ in 0..6 {
capture.ingest(&voiced);
}
assert!(
selector.voice_activity_open(),
"desktop capture must feed VAD and open VoiceActivity selector before transmit is already active"
);
assert!(
gate.load(),
"VoiceActivity selector should publish transmit gate"
);
let snapshot = stats.snapshot(&crate::AudioProcessingConfig::default());
assert!(snapshot.vad_active, "stats should expose active VAD");
}
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
#[test]
fn desktop_startup_audio_processing_state_applies_default_silero_fallback() {
let _guard = crate::vad::SILERO_MODEL_PATH_TEST_LOCK.lock().unwrap();
crate::vad::clear_silero_model_path_for_test();
let (config, stats, worker) = new_desktop_audio_processing_state();
assert_eq!(
config.lock().unwrap().vad_backend,
crate::VadBackend::SileroOnnx,
"desktop startup config should keep the default Silero backend selected"
);
assert!(
worker.lock().unwrap().is_none(),
"missing startup model should not create an ONNX worker"
);
let snapshot = stats.snapshot(&config.lock().unwrap());
assert!(
snapshot.vad_fallback_active,
"desktop startup should mark WebRTC fallback active when the default Silero worker cannot load"
);
crate::vad::clear_silero_model_path_for_test();
}
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
#[test]
fn desktop_set_audio_processing_config_normalizes_default_backend_to_noop() {
let audio_processing_config = Arc::new(Mutex::new(crate::AudioProcessingConfig::default()));
let audio_processing_stats = Arc::new(crate::SharedAudioProcessingStats::default());
let engine = AudioEngine {
transmit_gate: crate::ptt::AudioTransmitGate::new(false),
frames_sent: Arc::new(AtomicU32::new(0)),
frames_received: Arc::new(AtomicU32::new(0)),
output_gain: Arc::new(AtomicU32::new(1.0_f32.to_bits())),
output_muted: Arc::new(AtomicBool::new(false)),
audio_processing_config: audio_processing_config.clone(),
audio_processing_stats,
silero_vad_worker: Arc::new(Mutex::new(None)),
audio_handler: Arc::new(Mutex::new(AudioHandler::new())),
_input_stream: Mutex::new(None),
_output_stream: Mutex::new(None),
shutdown_tx: None,
capture_active: false,
};
engine
.set_audio_processing_config(crate::AudioProcessingConfig::default())
.unwrap();
assert_eq!(
engine.audio_processing_config_snapshot().processing_backend,
crate::AudioBackend::Noop,
"desktop setter should report the actual no-op processing backend for default configs"
);
}
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
#[test]
fn desktop_capture_silero_selection_reports_fallback_when_worker_unavailable() {
let _guard = crate::vad::SILERO_MODEL_PATH_TEST_LOCK.lock().unwrap();
crate::vad::clear_silero_model_path_for_test();
let gate = crate::ptt::AudioTransmitGate::new(false);
let selector = Arc::new(crate::TransmitModeSelector::new(gate.clone()));
selector.set_mode(crate::TransmitMode::VoiceActivity);
selector.set_in_channel(true);
let encoder = crate::opus_voice::new_voip_encoder("desktop Silero fallback test").unwrap();
let (voice_out_tx, _voice_out_rx) = mpsc::channel::<OutPacket>(16);
let frames_sent = Arc::new(AtomicU32::new(0));
let voice_out_tx = crate::opus_voice::start_out_packet_worker(
voice_out_tx,
frames_sent,
"desktop-silero-fallback-test",
)
.unwrap();
let stats = Arc::new(crate::SharedAudioProcessingStats::default());
let config = Arc::new(Mutex::new(crate::AudioProcessingConfig {
vad_backend: crate::VadBackend::SileroOnnx,
..crate::AudioProcessingConfig::default()
}));
let mut capture = CaptureState::new(
encoder,
SAMPLE_RATE,
1,
1.0,
voice_out_tx,
gate.flag_arc(),
Some(selector),
config.clone(),
Arc::new(Mutex::new(None)),
stats.clone(),
);
let mut voiced = [0.0_f32; crate::frame::FRAME_10MS_SAMPLES];
for (idx, sample) in voiced.iter_mut().enumerate() {
let phase = idx as f32 * 2.0 * std::f32::consts::PI * 220.0 / SAMPLE_RATE as f32;
*sample = phase.sin() * 0.4;
}
capture.ingest(&voiced);
let snapshot = stats.snapshot(&config.lock().unwrap());
assert_eq!(snapshot.vad_backend, crate::VadBackend::SileroOnnx);
assert!(
snapshot.vad_fallback_active,
"desktop Silero selection should make WebRTC fallback visible when ONNX worker cannot load"
);
crate::vad::clear_silero_model_path_for_test();
}
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
#[test]
fn desktop_capture_vad_consumes_only_new_pcm_while_transmitting() {
let gate = crate::ptt::AudioTransmitGate::new(true);
let selector = Arc::new(crate::TransmitModeSelector::new(gate.clone()));
selector.set_mode(crate::TransmitMode::VoiceActivity);
selector.set_in_channel(true);
let encoder = crate::opus_voice::new_voip_encoder("desktop VAD duplicate test").unwrap();
let (voice_out_tx, _voice_out_rx) = mpsc::channel::<OutPacket>(16);
let frames_sent = Arc::new(AtomicU32::new(0));
let voice_out_tx = crate::opus_voice::start_out_packet_worker(
voice_out_tx,
frames_sent,
"desktop-vad-duplicate-test",
)
.unwrap();
let stats = Arc::new(crate::SharedAudioProcessingStats::default());
let mut capture = CaptureState::new(
encoder,
SAMPLE_RATE,
1,
1.0,
voice_out_tx,
gate.flag_arc(),
Some(selector),
Arc::new(Mutex::new(crate::AudioProcessingConfig {
vad_backend: crate::VadBackend::Disabled,
..crate::AudioProcessingConfig::default()
})),
Arc::new(Mutex::new(None)),
stats,
);
capture.pcm_accum.extend(std::iter::repeat_n(
0.1_f32,
crate::frame::FRAME_10MS_SAMPLES + 240,
));
capture.pending_10ms[..240].fill(0.1);
capture.pending_10ms_len = 240;
capture.capture_frame_seq = 1;
capture.pcm_accum.extend(std::iter::repeat_n(0.2_f32, 240));
capture.process_pending_vad_frames(crate::frame::FRAME_10MS_SAMPLES + 240);
assert_eq!(
capture.capture_frame_seq, 2,
"VAD should consume only the 240 samples appended by the current ingest and complete one pending 10 ms frame"
);
}
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
#[test]
fn desktop_capture_silero_stale_enqueued_worker_uses_fallback() {
let gate = crate::ptt::AudioTransmitGate::new(false);
let selector = Arc::new(crate::TransmitModeSelector::new(gate.clone()));
selector.set_mode(crate::TransmitMode::VoiceActivity);
selector.set_in_channel(true);
let encoder =
crate::opus_voice::new_voip_encoder("desktop stale Silero fallback test").unwrap();
let (voice_out_tx, _voice_out_rx) = mpsc::channel::<OutPacket>(16);
let frames_sent = Arc::new(AtomicU32::new(0));
let voice_out_tx = crate::opus_voice::start_out_packet_worker(
voice_out_tx,
frames_sent,
"desktop-stale-silero-fallback-test",
)
.unwrap();
let stats = Arc::new(crate::SharedAudioProcessingStats::default());
let config = Arc::new(Mutex::new(crate::AudioProcessingConfig {
vad_backend: crate::VadBackend::SileroOnnx,
..crate::AudioProcessingConfig::default()
}));
let worker = Arc::new(Mutex::new(Some(
crate::vad::silero_onnx::SileroOnnxVadWorker::stale_test_worker(),
)));
let mut capture = CaptureState::new(
encoder,
SAMPLE_RATE,
1,
1.0,
voice_out_tx,
gate.flag_arc(),
Some(selector),
config.clone(),
worker,
stats.clone(),
);
let mut voiced = [0.0_f32; crate::frame::FRAME_10MS_SAMPLES];
for (idx, sample) in voiced.iter_mut().enumerate() {
let phase = idx as f32 * 2.0 * std::f32::consts::PI * 220.0 / SAMPLE_RATE as f32;
*sample = phase.sin() * 0.4;
}
capture.process_10ms_capture_frame(&voiced);
let snapshot = stats.snapshot(&config.lock().unwrap());
assert!(
snapshot.vad_fallback_active,
"stale Silero worker output should report active WebRTC fallback"
);
assert!(
snapshot.vad_probability > 0.5,
"stale Silero worker output should use WebRTC fallback probability instead of forced silence"
);
}
#[test]
fn android_startup_rollback_releases_acquired_mode_snapshot() {
let mut stack = crate::mode_stack::ModeStack::new();
@@ -1806,6 +2168,9 @@ fn try_open_capture(
transmit_active: Arc<AtomicBool>,
frames_sent: Arc<AtomicU32>,
mic_gain: f32,
voice_activity_selector: Option<Arc<crate::TransmitModeSelector>>,
audio_processing_config: Arc<Mutex<crate::AudioProcessingConfig>>,
silero_vad_worker: Arc<Mutex<Option<crate::vad::silero_onnx::SileroOnnxVadWorker>>>,
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
) -> Result<cpal::Stream, AudioError> {
let in_cfg = in_dev
@@ -1845,6 +2210,9 @@ fn try_open_capture(
"cpal-capture",
)?,
transmit_active,
voice_activity_selector,
audio_processing_config,
silero_vad_worker,
audio_processing_stats,
)));
@@ -1883,6 +2251,17 @@ struct CaptureState {
/// The PTT transmission gate. Read once per outbound frame; the
/// CaptureState never mutates this flag.
transmit_active: Arc<AtomicBool>,
voice_activity_selector: Option<Arc<crate::TransmitModeSelector>>,
vad_detector: crate::vad::WebRtcFallbackVad,
silero_vad_worker: Arc<Mutex<Option<crate::vad::silero_onnx::SileroOnnxVadWorker>>>,
silero_model_epoch: u64,
current_vad_backend: crate::VadBackend,
fallback_warned_backend: Option<crate::VadBackend>,
capture_frame_seq: u64,
vad_state: crate::voice_activity::VoiceActivityStateMachine,
audio_processing_config: Arc<Mutex<crate::AudioProcessingConfig>>,
pending_10ms: [f32; crate::frame::FRAME_10MS_SAMPLES],
pending_10ms_len: usize,
/// Pre-allocated mono downmix buffer. Resized in-place each
/// callback; `clear()` retains capacity. SDD-094 realtime-thread
/// invariant: this avoids the heap allocation that the prior fix
@@ -1926,6 +2305,9 @@ impl CaptureState {
mic_gain: f32,
voice_out_tx: crate::opus_voice::EncodedVoiceFrameSender,
transmit_active: Arc<AtomicBool>,
voice_activity_selector: Option<Arc<crate::TransmitModeSelector>>,
audio_processing_config: Arc<Mutex<crate::AudioProcessingConfig>>,
silero_vad_worker: Arc<Mutex<Option<crate::vad::silero_onnx::SileroOnnxVadWorker>>>,
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
) -> Self {
Self {
@@ -1939,6 +2321,17 @@ impl CaptureState {
opus_out: [0u8; crate::opus_voice::MAX_OPUS_FRAME],
voice_out_tx,
transmit_active,
voice_activity_selector,
vad_detector: crate::vad::WebRtcFallbackVad::default(),
silero_vad_worker,
silero_model_epoch: crate::vad::silero_model_epoch(),
current_vad_backend: crate::VadBackend::Disabled,
fallback_warned_backend: None,
capture_frame_seq: 0,
vad_state: crate::voice_activity::VoiceActivityStateMachine::default(),
audio_processing_config,
pending_10ms: [0.0; crate::frame::FRAME_10MS_SAMPLES],
pending_10ms_len: 0,
mono_scratch: Vec::with_capacity(4096),
frame_scratch: Vec::with_capacity(FRAME_SAMPLES),
audio_processing_stats,
@@ -1952,6 +2345,16 @@ impl CaptureState {
/// Consume an arbitrary-rate, multichannel cpal buffer; produce
/// 48 kHz mono frames; encode and send when `transmit_active`
/// is true (PTT engaged).
// TODO(realtime-audio): This method runs on the cpal audio callback
// thread with a ~10 ms deadline. Known pre-existing violations of the
// realtime safety constraint that should be addressed in a future
// iteration:
// 1. Blocking Mutex::lock().unwrap() on self (via cpal callback)
// 2. Potential allocation in mono_scratch.reserve() and
// pcm_accum.extend_from_slice() when buffer capacity is exceeded
// 3. Encode-path warn!/error! logging via send_voip_frame closures
// These were present before the VAD integration and are not
// introduced by this changeset.
fn ingest<T: ToF32 + Copy>(&mut self, buf: &[T]) {
// 1. Down-mix to mono (pre-gain). Always performed so the level
// meter reflects real mic input even when PTT is released.
@@ -1981,15 +2384,11 @@ impl CaptureState {
}
}
if !self.transmit_active.load(Ordering::Relaxed) {
self.pcm_accum.clear();
return;
}
// 2. Resample to 48 kHz if needed. We re-borrow
// `mono_scratch` as a shared slice per branch to satisfy
// the borrow checker against `&mut self` on the
// resample path.
let vad_start_offset = self.pcm_accum.len();
if self.in_sample_rate == SAMPLE_RATE {
// Disjoint-borrow: copy the slice into pcm_accum without
// aliasing &mut self.
@@ -2008,6 +2407,13 @@ impl CaptureState {
self.mono_scratch = mono;
}
self.process_pending_vad_frames(vad_start_offset);
if !self.transmit_active.load(Ordering::Relaxed) {
self.pcm_accum.clear();
return;
}
// 3. Encode any complete frames. Clamp each sample to
// [-1.0, 1.0] before handing to libopus's float encoder —
// out-of-range samples are hard-clipped inside libopus,
@@ -2055,6 +2461,179 @@ impl CaptureState {
}
}
fn process_pending_vad_frames(&mut self, start_offset: usize) {
let mut offset = start_offset.min(self.pcm_accum.len());
while offset < self.pcm_accum.len() {
let remaining = crate::frame::FRAME_10MS_SAMPLES - self.pending_10ms_len;
let take = remaining.min(self.pcm_accum.len() - offset);
self.pending_10ms[self.pending_10ms_len..self.pending_10ms_len + take]
.copy_from_slice(&self.pcm_accum[offset..offset + take]);
self.pending_10ms_len += take;
offset += take;
if self.pending_10ms_len == crate::frame::FRAME_10MS_SAMPLES {
let frame = self.pending_10ms;
self.process_10ms_capture_frame(&frame);
self.pending_10ms_len = 0;
}
}
}
fn mark_vad_fallback_active(&mut self, failed_backend: crate::VadBackend) {
// Realtime callback: do not log here. Publish state via atomics
// and let a non-realtime consumer translate transitions into
// info/warn events. The transmit/diagnostic stats stream
// already exposes vad_fallback_active for this purpose.
self.fallback_warned_backend = Some(failed_backend);
}
fn sync_vad_backend(&mut self, voice_activity_mode: bool, vad_backend: crate::VadBackend) {
if !voice_activity_mode {
self.current_vad_backend = crate::VadBackend::Disabled;
self.fallback_warned_backend = None;
self.audio_processing_stats.set_vad_fallback_active(false);
return;
}
let silero_epoch = crate::vad::silero_model_epoch();
let silero_changed =
vad_backend == crate::VadBackend::SileroOnnx && silero_epoch != self.silero_model_epoch;
if vad_backend == self.current_vad_backend && !silero_changed {
return;
}
self.current_vad_backend = vad_backend;
self.silero_model_epoch = silero_epoch;
self.fallback_warned_backend = None;
self.vad_state.reset();
match vad_backend {
crate::VadBackend::SileroOnnx => {
let worker_available = self
.silero_vad_worker
.try_lock()
.map(|worker| worker.is_some())
.unwrap_or(false);
if worker_available {
self.audio_processing_stats.set_vad_fallback_active(false);
} else {
self.mark_vad_fallback_active(crate::VadBackend::SileroOnnx);
self.audio_processing_stats.set_vad_fallback_active(true);
}
}
crate::VadBackend::WebrtcVad => {
self.audio_processing_stats.set_vad_fallback_active(false);
}
crate::VadBackend::EnergyDebug => {
self.audio_processing_stats.set_vad_fallback_active(true);
}
crate::VadBackend::Disabled => {
self.audio_processing_stats.set_vad_fallback_active(false);
}
}
}
fn process_10ms_capture_frame(&mut self, frame: &[f32; crate::frame::FRAME_10MS_SAMPLES]) {
let input_dbfs = crate::frame::dbfs(frame);
let (vad_backend, vad_hangover) = self
.audio_processing_config
.try_lock()
.map(|cfg| (cfg.vad_backend, cfg.vad_hangover_ms))
.unwrap_or((
crate::VadBackend::WebrtcVad,
crate::voice_activity::VAD_HANGOVER_MS,
));
let voice_activity_mode = self
.voice_activity_selector
.as_ref()
.map(|selector| selector.mode() == crate::TransmitMode::VoiceActivity)
.unwrap_or(false);
if voice_activity_mode {
self.sync_vad_backend(true, vad_backend);
self.vad_state.configure(
crate::voice_activity::VAD_OPEN_AFTER_MS,
vad_hangover,
crate::voice_activity::VAD_MIN_TX_MS,
);
} else {
self.sync_vad_backend(false, vad_backend);
}
let (vad_probability, gate_open, used_fallback_vad) = if voice_activity_mode {
self.capture_frame_seq = self.capture_frame_seq.wrapping_add(1);
let capture_seq = self.capture_frame_seq;
let mut used_fallback_vad = false;
let vad = match vad_backend {
crate::VadBackend::Disabled => crate::vad::VadOutput {
probability: 1.0,
speech: true,
},
crate::VadBackend::SileroOnnx => {
// Single `try_lock` that both probes availability and
// sends the frame. The guard is scoped to the block
// so it drops before the fallback path (which needs
// `&mut self` for `mark_vad_fallback_active`).
// Returns Some(VadOutput) on a successful, non-stale
// send; None means "fall back to WebRTC VAD".
let worker_output = {
let guard = self.silero_vad_worker.try_lock().ok();
guard.and_then(|guard| {
let worker = guard.as_ref()?;
if worker.try_send(capture_seq, frame) && !worker.is_stale(capture_seq)
{
let p = worker.latest_probability();
Some(crate::vad::VadOutput {
probability: p,
speech: p >= 0.5,
})
} else {
None
}
})
};
if let Some(output) = worker_output {
output
} else {
used_fallback_vad = true;
self.mark_vad_fallback_active(vad_backend);
crate::vad::VoiceActivityDetector::process_10ms(
&mut self.vad_detector,
frame,
)
}
}
crate::VadBackend::WebrtcVad | crate::VadBackend::EnergyDebug => {
used_fallback_vad = vad_backend == crate::VadBackend::EnergyDebug;
crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, frame)
}
};
(
vad.probability,
self.vad_state.update(vad.speech),
used_fallback_vad,
)
} else {
(0.0, false, false)
};
self.audio_processing_stats
.set_vad_fallback_active(used_fallback_vad);
let vad_active = voice_activity_mode && gate_open;
if let Some(selector) = &self.voice_activity_selector {
selector.set_voice_activity_open(vad_active);
}
self.audio_processing_stats.update_capture(
input_dbfs,
input_dbfs,
vad_probability,
vad_active,
self.transmit_active.load(Ordering::Relaxed),
);
self.audio_processing_stats
.record_capture_frame(frame.iter().all(|sample| sample.abs() <= 0.000_001));
}
/// Simple linear resampler for `in_sample_rate → 48000`.
///
/// The resampler maintains continuity across cpal buffer
@@ -2144,8 +2723,8 @@ where
{
let stream = device
.build_input_stream(
config,
move |data: &[T], _| {
*config,
move |data: &[T], _: &cpal::InputCallbackInfo| {
let mut s = state.lock().unwrap();
s.ingest(data);
},
@@ -2211,8 +2790,8 @@ where
.unwrap_or_else(std::time::Instant::now);
let stream = device
.build_output_stream(
config,
move |out: &mut [T], _| {
*config,
move |out: &mut [T], _: &cpal::OutputCallbackInfo| {
let cb_start = std::time::Instant::now();
let muted = output_muted.load(Ordering::Relaxed);
let dev_frames = out.len() / dev_channels.max(1);
@@ -2620,14 +3199,25 @@ pub mod bench_seam {
let (tx, rx) = mpsc::channel::<OutPacket>(64);
let transmit_active = Arc::new(AtomicBool::new(true));
let frames_sent = Arc::new(AtomicU32::new(0));
// Bridge the bench's private mpsc<OutPacket> to the
// realtime-thread-safe EncodedVoiceFrameSender that
// CaptureState expects. The worker task forwards
// encoded Opus frames to `tx` via `frames_sent`.
let voice_out_tx =
crate::opus_voice::start_out_packet_worker(tx, frames_sent, "cpal-bench")
.expect("start_out_packet_worker");
let state = CaptureState::new(
encoder,
in_sample_rate,
in_channels,
1.0,
tx,
voice_out_tx,
transmit_active.clone(),
frames_sent,
None,
Arc::new(std::sync::Mutex::new(
crate::AudioProcessingConfig::default(),
)),
Arc::new(std::sync::Mutex::new(None)),
Arc::new(crate::SharedAudioProcessingStats::default()),
);
Self {
-538
View File
@@ -1,538 +0,0 @@
//! Optional raw iOS RemoteIO path for the WebRTC APM experimental mode.
//!
//! Provides an alternative to `ios_voice_unit.rs` for the
//! `SonoraExperimental` processing mode. Instead of
//! `kAudioUnitSubType_VoiceProcessingIO` (which owns AEC/NS/AGC), it
//! opens `kAudioUnitSubType_RemoteIO` with voice processing explicitly
//! disabled so WebRTC APM can own the full signal path.
//!
//! ## Hard invariants enforced here
//!
//! * INV_009: Rust AEC only active when platform AEC is disabled.
//! * INV_010: VoiceProcessingIO and WebRTC APM AEC are mutually exclusive.
//! * INV_011: Software AEC backend receives both capture and render-reference.
//! * INV_012: Render reference is copied from decoded/mixed remote PCM
//! before playout.
//!
//! ## Fallback
//!
//! If RemoteIO construction fails, the caller falls back to `IosVoiceUnit`
//! (VPIO) and logs the error.
//!
//! ## Status
//!
//! Experimental / disabled by default. Only activated when the user
//! explicitly selects `SonoraExperimental` mode via the bridge API.
//!
//! ## Platform
//!
//! `kAudioUnitSubType_RemoteIO` is only available in the iOS SDK.
//! This module is gated to `target_os = "ios"`.
#[cfg(target_os = "ios")]
pub use inner::IosRawUnit;
#[cfg(target_os = "ios")]
mod inner {
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use audiopus::coder::Encoder as OpusEncoder;
use coreaudio::audio_unit::audio_format::LinearPcmFlags;
use coreaudio::audio_unit::render_callback::{self, data};
use coreaudio::audio_unit::IOType;
use coreaudio::audio_unit::{AudioUnit, Element, SampleFormat, Scope, StreamFormat};
use tracing::{info, warn};
use crate::mobile_voice_backend::VoiceAudioParams;
use crate::processor::AudioProcessor;
use crate::AudioError;
const SAMPLE_RATE_HZ: f64 = 48_000.0;
// ------------------------------------------------------------------ //
// Render-reference ring buffer //
// ------------------------------------------------------------------ //
/// 4-slot ring buffer shared between the render callback (writer) and
/// the capture callback (reader for Sonora AEC3). Capacity: 4 × 10 ms
/// = 40 ms of headroom.
///
/// If the capture callback runs before the render callback has written
/// a frame it reads zeros (silence reference), which is safe — Sonora
/// AEC3 simply skips cancellation for that frame.
type RenderReferenceBuffer = crate::render_reference::RenderReferenceBuffer<480, 4>;
type RenderReferenceFrameAccumulator =
crate::render_reference::RenderReferenceFrameAccumulator<480>;
const RAW_RENDER_SCRATCH_FRAMES: usize = 1024;
// ------------------------------------------------------------------ //
// Capture pipeline state //
// ------------------------------------------------------------------ //
struct RawCaptureState {
encoder: OpusEncoder,
pcm_accum: Vec<i16>,
opus_out: [u8; crate::opus_voice::MAX_OPUS_FRAME],
voice_out_tx: crate::opus_voice::EncodedVoiceFrameSender,
transmit_active: Arc<AtomicBool>,
output_muted: Arc<AtomicBool>,
mic_gain: f32,
voice_activity_selector: Option<Arc<crate::TransmitModeSelector>>,
vad_detector: crate::vad::WebRtcFallbackVad,
silero_coreml_worker: Option<crate::vad::apple_coreml::AppleCoreMlVadWorker>,
current_vad_backend: crate::VadBackend,
capture_frame_seq: u64,
vad_state: crate::voice_activity::VoiceActivityStateMachine,
/// Processing config — retained for route-change reloads.
audio_processing_config: Arc<Mutex<crate::AudioProcessingConfig>>,
webrtc_apm_processor: crate::processor::WebRtcApmProcessor,
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
render_reference: Arc<RenderReferenceBuffer>,
pending_10ms: [i16; crate::frame::FRAME_10MS_SAMPLES],
pending_10ms_len: usize,
fallback_warned_backend: Option<crate::VadBackend>,
}
impl RawCaptureState {
fn new(
params: &VoiceAudioParams,
render_reference: Arc<RenderReferenceBuffer>,
) -> Result<Self, AudioError> {
let encoder = crate::opus_voice::new_voip_encoder("ios raw")?;
let webrtc_apm_config = params
.audio_processing_config
.lock()
.map(|cfg| crate::processor::webrtc_apm::WebRtcApmConfig::from_audio_config(&cfg))
.unwrap_or_default();
Ok(Self {
encoder,
pcm_accum: Vec::with_capacity(crate::frame::FRAME_20MS_SAMPLES * 2),
opus_out: [0u8; crate::opus_voice::MAX_OPUS_FRAME],
voice_out_tx: crate::opus_voice::start_out_packet_worker(
params.voice_out_tx.clone(),
params.frames_sent.clone(),
"ios-raw",
)?,
transmit_active: params.transmit_active.clone(),
output_muted: params.output_muted.clone(),
mic_gain: params.mic_gain,
voice_activity_selector: params.voice_activity_selector.clone(),
vad_detector: crate::vad::WebRtcFallbackVad::default(),
silero_coreml_worker: None,
current_vad_backend: crate::VadBackend::WebrtcVad,
capture_frame_seq: 0,
vad_state: crate::voice_activity::VoiceActivityStateMachine::default(),
audio_processing_config: params.audio_processing_config.clone(),
webrtc_apm_processor: crate::processor::WebRtcApmProcessor::with_config(
webrtc_apm_config,
)?,
audio_processing_stats: params.audio_processing_stats.clone(),
render_reference,
pending_10ms: [0_i16; crate::frame::FRAME_10MS_SAMPLES],
pending_10ms_len: 0,
fallback_warned_backend: None,
})
}
fn mark_vad_fallback_active(&mut self, failed_backend: crate::VadBackend) {
if self.fallback_warned_backend != Some(failed_backend) {
self.fallback_warned_backend = Some(failed_backend);
if self.capture_frame_seq < 128 {
tracing::info!(
target: "chanora_audio",
backend = failed_backend.as_str(),
seq = self.capture_frame_seq,
"VAD backend warming up; using WebRTC fallback"
);
} else {
tracing::warn!(
target: "chanora_audio",
backend = failed_backend.as_str(),
"VAD backend unavailable; using WebRTC fallback for runtime detection"
);
}
}
}
fn ingest_i16(&mut self, samples: &[i16]) {
// Accumulate into 10 ms frames for VAD / Sonora processing.
let mut offset = 0;
while offset < samples.len() {
let remaining = crate::frame::FRAME_10MS_SAMPLES - self.pending_10ms_len;
let take = remaining.min(samples.len() - offset);
self.pending_10ms[self.pending_10ms_len..self.pending_10ms_len + take]
.copy_from_slice(&samples[offset..offset + take]);
self.pending_10ms_len += take;
offset += take;
if self.pending_10ms_len == crate::frame::FRAME_10MS_SAMPLES {
let frame = self.pending_10ms;
self.process_10ms_capture_frame(&frame);
self.encode_complete_20ms_frames();
self.pending_10ms_len = 0;
}
}
if !self.transmit_active.load(Ordering::Relaxed) {
self.pcm_accum.clear();
return;
}
self.encode_complete_20ms_frames();
}
fn encode_complete_20ms_frames(&mut self) {
while self.pcm_accum.len() >= crate::frame::FRAME_20MS_SAMPLES {
let mut frame = [0i16; crate::frame::FRAME_20MS_SAMPLES];
frame.copy_from_slice(&self.pcm_accum[..crate::frame::FRAME_20MS_SAMPLES]);
self.pcm_accum.drain(..crate::frame::FRAME_20MS_SAMPLES);
match self.encoder.encode(&frame, &mut self.opus_out[..]) {
Ok(len) => {
crate::opus_voice::send_voip_frame(
&self.voice_out_tx,
&self.opus_out,
len,
|| {
warn!(
target: "chanora_audio",
"ios raw: voice_out queue full; dropping frame"
);
},
|| {},
);
}
Err(e) => {
tracing::error!(target: "chanora_audio",
error = %e, "ios raw opus encode failed");
}
}
}
}
fn process_10ms_capture_frame(
&mut self,
samples: &[i16; crate::frame::FRAME_10MS_SAMPLES],
) {
let mut frame = [0.0_f32; crate::frame::FRAME_10MS_SAMPLES];
for (dst, src) in frame.iter_mut().zip(samples.iter().copied()) {
*dst = crate::frame::i16_to_f32(src);
}
let input_dbfs = crate::frame::dbfs(&frame);
// Debug WAV mic taps are intentionally unavailable on iOS raw
// realtime callbacks until WavDebugRecorder supports a
// preallocated handoff path; the current recorder push path
// allocates per frame.
// Feed render reference to WebRTC APM before capture so AEC can adapt.
let render_ref = self.render_reference.read_latest();
self.webrtc_apm_processor.process_render(&render_ref);
self.webrtc_apm_processor.process_capture(&mut frame);
// Processed-mic debug WAV capture is disabled for the same
// realtime allocation reason as the raw-mic tap above.
let voice_activity_mode = self
.voice_activity_selector
.as_ref()
.map(|selector| selector.mode() == crate::TransmitMode::VoiceActivity)
.unwrap_or(false);
if !voice_activity_mode {
self.silero_coreml_worker = None;
self.current_vad_backend = crate::VadBackend::Disabled;
self.fallback_warned_backend = None;
self.audio_processing_stats.set_vad_fallback_active(false);
}
let (vad_backend, vad_hangover) = self
.audio_processing_config
.try_lock()
.map(|cfg| (cfg.vad_backend, cfg.vad_hangover_ms))
.unwrap_or((
crate::VadBackend::WebrtcVad,
crate::voice_activity::VAD_HANGOVER_MS,
));
if voice_activity_mode {
self.vad_state.configure(
crate::voice_activity::VAD_OPEN_AFTER_MS,
vad_hangover,
crate::voice_activity::VAD_MIN_TX_MS,
);
}
if voice_activity_mode && vad_backend != self.current_vad_backend {
self.current_vad_backend = vad_backend;
self.fallback_warned_backend = None;
if vad_backend == crate::VadBackend::SileroOnnx {
self.silero_coreml_worker = None;
self.mark_vad_fallback_active(crate::VadBackend::SileroOnnx);
self.audio_processing_stats.set_vad_fallback_active(true);
} else {
self.silero_coreml_worker = None;
self.audio_processing_stats.set_vad_fallback_active(false);
}
self.vad_state.reset();
}
let (vad_probability, active) = if voice_activity_mode {
self.capture_frame_seq = self.capture_frame_seq.wrapping_add(1);
let capture_seq = self.capture_frame_seq;
let mut used_fallback_vad = false;
let vad = if vad_backend == crate::VadBackend::Disabled {
crate::vad::VadOutput {
probability: 1.0,
speech: true,
}
} else if vad_backend == crate::VadBackend::SileroOnnx {
match crate::vad::callback_vad_worker_policy(
voice_activity_mode,
vad_backend,
self.silero_coreml_worker.is_some(),
) {
crate::vad::VadWorkerPolicy::UseWorker => {
let worker = self
.silero_coreml_worker
.as_ref()
.expect("policy checked worker");
let enqueued = worker.try_send(capture_seq, &frame);
if !worker.is_stale(capture_seq) {
let p = worker.latest_probability();
crate::vad::VadOutput {
probability: p,
speech: p >= 0.5,
}
} else if enqueued {
crate::vad::VadOutput {
probability: 0.0,
speech: false,
}
} else {
used_fallback_vad = true;
self.mark_vad_fallback_active(vad_backend);
crate::vad::VoiceActivityDetector::process_10ms(
&mut self.vad_detector,
&frame,
)
}
}
crate::vad::VadWorkerPolicy::UseFallback => {
used_fallback_vad = true;
self.mark_vad_fallback_active(vad_backend);
crate::vad::VoiceActivityDetector::process_10ms(
&mut self.vad_detector,
&frame,
)
}
crate::vad::VadWorkerPolicy::NotModelBacked => crate::vad::VadOutput {
probability: 1.0,
speech: true,
},
}
} else {
crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame)
};
self.audio_processing_stats
.set_vad_fallback_active(used_fallback_vad);
(vad.probability, self.vad_state.update(vad.speech))
} else {
(0.0, false)
};
if let Some(sel) = &self.voice_activity_selector {
sel.set_voice_activity_open(voice_activity_mode && active);
}
self.audio_processing_stats.update_capture(
input_dbfs,
crate::frame::dbfs(&frame),
vad_probability,
voice_activity_mode && active,
self.transmit_active.load(Ordering::Relaxed),
);
if !self.transmit_active.load(Ordering::Relaxed) {
return;
}
if crate::capture_accumulator::append_processed_i16_bounded(
&mut self.pcm_accum,
&frame,
self.mic_gain,
) {
self.audio_processing_stats.increment_callback_xrun();
}
}
}
// ------------------------------------------------------------------ //
// IosRawUnit //
// ------------------------------------------------------------------ //
/// Raw iOS RemoteIO audio unit for the Sonora experimental path.
pub struct IosRawUnit {
unit: AudioUnit,
}
impl IosRawUnit {
/// Open a RemoteIO AudioUnit, install render + input callbacks, start.
pub(crate) fn start(params: VoiceAudioParams) -> Result<Self, AudioError> {
// INV_010: reject if config requests VPIO (that's IosVoiceUnit's job).
{
let cfg = params.audio_processing_config.lock().unwrap();
if cfg.ios_mode == crate::IosVoiceProcessingMode::PlatformVoiceProcessing {
return Err(AudioError::InvalidAudioProcessingConfig(
"IosRawUnit requires raw WebRTC APM mode".to_string(),
));
}
}
let mut unit = AudioUnit::new_uninitialized(IOType::RemoteIO)
.map_err(|e| AudioError::Backend(format!("remoteio new: {e}")))?;
// Enable input on bus 1.
const ENABLE_IO: u32 = 2003;
let enable: u32 = 1;
unit.set_property(ENABLE_IO, Scope::Input, Element::Input, Some(&enable))
.map_err(|e| AudioError::Backend(format!("remoteio enable input: {e}")))?;
// 48 kHz Int16 mono on both buses.
let fmt = StreamFormat {
sample_rate: SAMPLE_RATE_HZ,
sample_format: SampleFormat::I16,
flags: LinearPcmFlags::IS_SIGNED_INTEGER | LinearPcmFlags::IS_PACKED,
channels: 1,
};
unit.set_stream_format(fmt, Scope::Input, Element::Output)
.map_err(|e| AudioError::StreamConfig(format!("remoteio fmt output: {e}")))?;
unit.set_stream_format(fmt, Scope::Output, Element::Input)
.map_err(|e| AudioError::StreamConfig(format!("remoteio fmt input: {e}")))?;
// Shared render-reference buffer (INV_011 / INV_012).
let render_ref_buf = RenderReferenceBuffer::new();
let render_ref_for_capture = render_ref_buf.clone();
let mut capture_state = RawCaptureState::new(&params, render_ref_for_capture)?;
unit.set_input_callback(move |args: render_callback::Args<data::Interleaved<i16>>| {
capture_state.ingest_i16(args.data.buffer);
Ok(())
})
.map_err(|e| AudioError::Backend(format!("remoteio input cb: {e}")))?;
let mut scratch = [0.0_f32; RAW_RENDER_SCRATCH_FRAMES * 2];
let mut mono = [0.0_f32; RAW_RENDER_SCRATCH_FRAMES];
let mut render_ref_accum = RenderReferenceFrameAccumulator::new();
let handler = params.handler.clone();
let output_gain = params.output_gain.clone();
let output_muted = params.output_muted.clone();
let stats_render = params.audio_processing_stats.clone();
unit.set_render_callback(move |args: render_callback::Args<data::Interleaved<i16>>| {
let out = args.data.buffer;
let n = out.len();
let process_n = n.min(RAW_RENDER_SCRATCH_FRAMES);
let stereo_n = process_n * 2;
if n > RAW_RENDER_SCRATCH_FRAMES {
stats_render.increment_callback_xrun();
}
scratch[..stereo_n].fill(0.0);
match handler.try_lock() {
Ok(mut h) => {
let _ = h.fill_buffer(&mut scratch[..stereo_n]);
}
Err(std::sync::TryLockError::WouldBlock) => {
stats_render.increment_callback_xrun();
}
Err(std::sync::TryLockError::Poisoned(e)) => {
warn!(target: "chanora_audio",
"AudioHandler poisoned (raw render): {e}");
}
}
// INV_012: copy render reference BEFORE playout.
crate::voice_render::downmix_stereo_f32_to_mono_f32(
&scratch[..stereo_n],
&mut mono[..process_n],
);
render_ref_accum.push_mono_samples(&mono[..process_n], |frame| {
render_ref_buf.write(frame);
});
let gain = f32::from_bits(output_gain.load(Ordering::Relaxed));
let muted = output_muted.load(Ordering::Relaxed);
let mix_stats = crate::voice_render::downmix_stereo_f32_to_interleaved_i16(
&scratch[..stereo_n],
&mut out[..process_n],
1,
gain,
muted,
);
if process_n < n {
out[process_n..].fill(0);
}
if mix_stats.clipped_samples > 0 {
stats_render.add_clipped_samples(mix_stats.clipped_samples);
}
stats_render.update_render(crate::frame::dbfs(&scratch[..stereo_n]), n as u32);
Ok(())
})
.map_err(|e| AudioError::Backend(format!("remoteio render cb: {e}")))?;
unit.initialize()
.map_err(|e| AudioError::Backend(format!("remoteio init: {e}")))?;
unit.start()
.map_err(|e| AudioError::Backend(format!("remoteio start: {e}")))?;
info!(
target: "chanora_audio",
sample_rate_hz = SAMPLE_RATE_HZ,
"ios RemoteIO (Sonora experimental) started"
);
Ok(Self { unit })
}
/// Restart the unit after a route change (stop → uninit → init → start).
pub fn restart(&mut self) -> Result<(), AudioError> {
self.unit
.stop()
.map_err(|e| AudioError::Backend(format!("remoteio restart stop: {e}")))?;
self.unit
.uninitialize()
.map_err(|e| AudioError::Backend(format!("remoteio restart uninit: {e}")))?;
self.unit
.initialize()
.map_err(|e| AudioError::Backend(format!("remoteio restart init: {e}")))?;
self.unit
.start()
.map_err(|e| AudioError::Backend(format!("remoteio restart start: {e}")))?;
info!(target: "chanora_audio", "ios RemoteIO restarted");
Ok(())
}
/// Pause the unit during an AVAudioSession interruption.
pub fn pause(&mut self) -> Result<(), AudioError> {
self.unit
.stop()
.map_err(|e| AudioError::Backend(format!("remoteio pause: {e}")))
}
/// Resume the unit after an interruption ends.
pub fn resume(&mut self) -> Result<(), AudioError> {
self.unit
.start()
.map_err(|e| AudioError::Backend(format!("remoteio resume: {e}")))
}
}
impl Drop for IosRawUnit {
fn drop(&mut self) {
if let Err(e) = self.unit.stop() {
warn!(target: "chanora_audio", error = %e,
"ios RemoteIO stop on drop failed");
} else {
info!(target: "chanora_audio", "ios RemoteIO stopped");
}
}
}
}
+20 -35
View File
@@ -406,38 +406,20 @@ impl IosCaptureState {
speech: true,
}
} else if vad_backend == crate::VadBackend::SileroOnnx {
match crate::vad::callback_vad_worker_policy(
voice_activity_mode,
vad_backend,
self.silero_coreml_worker.is_some(),
) {
crate::vad::VadWorkerPolicy::UseWorker => {
let worker = self
.silero_coreml_worker
.as_ref()
.expect("policy checked worker");
let enqueued = worker.try_send(capture_seq, &frame);
if !worker.is_stale(capture_seq) {
let p = worker.latest_probability();
crate::vad::VadOutput {
probability: p,
speech: p >= 0.5,
}
} else if enqueued {
crate::vad::VadOutput {
probability: 0.0,
speech: false,
}
} else {
used_fallback_vad = true;
self.mark_vad_fallback_active(crate::VadBackend::SileroOnnx);
crate::vad::VoiceActivityDetector::process_10ms(
&mut self.vad_detector,
&frame,
)
if let Some(worker) = self.silero_coreml_worker.as_ref() {
let enqueued = worker.try_send(capture_seq, &frame);
if !worker.is_stale(capture_seq) {
let p = worker.latest_probability();
crate::vad::VadOutput {
probability: p,
speech: p >= 0.5,
}
}
crate::vad::VadWorkerPolicy::UseFallback => {
} else if enqueued {
crate::vad::VadOutput {
probability: 0.0,
speech: false,
}
} else {
used_fallback_vad = true;
self.mark_vad_fallback_active(crate::VadBackend::SileroOnnx);
crate::vad::VoiceActivityDetector::process_10ms(
@@ -445,10 +427,13 @@ impl IosCaptureState {
&frame,
)
}
crate::vad::VadWorkerPolicy::NotModelBacked => crate::vad::VadOutput {
probability: 1.0,
speech: true,
},
} else {
used_fallback_vad = true;
self.mark_vad_fallback_active(crate::VadBackend::SileroOnnx);
crate::vad::VoiceActivityDetector::process_10ms(
&mut self.vad_detector,
&frame,
)
}
} else {
crate::vad::VoiceActivityDetector::process_10ms(&mut self.vad_detector, &frame)
+1 -4
View File
@@ -65,10 +65,7 @@ pub(crate) mod voice_render;
mod sdl_output;
#[cfg(any(target_os = "ios", target_os = "macos"))]
mod ios_voice_unit;
#[cfg(target_os = "ios")]
pub mod ios_raw_unit;
mod ios_voice_unit;
#[cfg(target_os = "android")]
pub mod android_voice_unit;
@@ -26,7 +26,7 @@ use std::thread;
use tracing::{info, warn};
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::Foundation::{HINSTANCE, HMODULE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::Input::{
GetRawInputData, RegisterRawInputDevices, HRAWINPUT, RAWINPUT, RAWINPUTDEVICE, RAWINPUTHEADER,
@@ -35,7 +35,7 @@ use windows::Win32::UI::Input::{
use windows::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW,
PostThreadMessageW, RegisterClassExW, SetWindowsHookExW, TranslateMessage, UnhookWindowsHookEx,
HC_ACTION, HHOOK, HOOKPROC, KBDLLHOOKSTRUCT, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL, WH_MOUSE_LL,
HC_ACTION, HOOKPROC, KBDLLHOOKSTRUCT, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL, WH_MOUSE_LL,
WINDOW_EX_STYLE, WINDOW_STYLE, WM_INPUT, WM_KEYDOWN, WM_KEYUP, WM_QUIT, WM_SYSKEYDOWN,
WM_SYSKEYUP, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, XBUTTON1, XBUTTON2,
};
@@ -423,7 +423,7 @@ unsafe fn run_raw_input_loop(
// class.
let _atom = RegisterClassExW(&wc);
let hwnd = unsafe {
let hwnd = match unsafe {
CreateWindowExW(
WINDOW_EX_STYLE(0),
class_name,
@@ -433,13 +433,23 @@ unsafe fn run_raw_input_loop(
0,
0,
0,
HWND(HWND_MESSAGE_PTR),
Some(HWND(HWND_MESSAGE_PTR as *mut core::ffi::c_void)),
None,
h_instance,
Some(HINSTANCE(h_instance.0)),
None,
)
} {
Ok(h) => h,
Err(_) => {
warn!(
target: "chanora_audio",
"windows ptt: CreateWindowExW(HWND_MESSAGE) returned null"
);
report!(false);
return false;
}
};
if hwnd.0 == 0 {
if hwnd.0.is_null() {
warn!(
target: "chanora_audio",
"windows ptt: CreateWindowExW(HWND_MESSAGE) returned null"
@@ -517,13 +527,13 @@ unsafe fn run_raw_input_loop(
usUsagePage: 0x01,
usUsage: 0x06,
dwFlags: RIDEV_REMOVE,
hwndTarget: HWND(0),
hwndTarget: HWND(std::ptr::null_mut()),
},
RAWINPUTDEVICE {
usUsagePage: 0x01,
usUsage: 0x02,
dwFlags: RIDEV_REMOVE,
hwndTarget: HWND(0),
hwndTarget: HWND(std::ptr::null_mut()),
},
];
let _ = RegisterRawInputDevices(&undo, std::mem::size_of::<RAWINPUTDEVICE>() as u32);
@@ -544,7 +554,7 @@ unsafe extern "system" fn raw_input_wnd_proc(
}
unsafe fn handle_wm_input(lparam: LPARAM) {
let h_raw = HRAWINPUT(lparam.0);
let h_raw = HRAWINPUT(lparam.0 as *mut core::ffi::c_void);
let mut size: u32 = 0;
let header_sz = std::mem::size_of::<RAWINPUTHEADER>() as u32;
// First call: query buffer size.
@@ -841,31 +851,33 @@ unsafe fn run_hook_loop(
let kbd_proc: HOOKPROC = Some(kbd_hook_proc);
let mouse_proc: HOOKPROC = Some(mouse_hook_proc);
let kbd_hook = match SetWindowsHookExW(WH_KEYBOARD_LL, kbd_proc, h_instance, 0) {
Ok(h) => h,
Err(e) => {
warn!(
target: "chanora_audio",
error = %e,
"windows ptt: SetWindowsHookExW(WH_KEYBOARD_LL) failed"
);
report!(false);
return false;
}
};
let mouse_hook = match SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, h_instance, 0) {
Ok(h) => h,
Err(e) => {
warn!(
target: "chanora_audio",
error = %e,
"windows ptt: SetWindowsHookExW(WH_MOUSE_LL) failed"
);
let _ = UnhookWindowsHookEx(kbd_hook);
report!(false);
return false;
}
};
let kbd_hook =
match SetWindowsHookExW(WH_KEYBOARD_LL, kbd_proc, Some(HINSTANCE(h_instance.0)), 0) {
Ok(h) => h,
Err(e) => {
warn!(
target: "chanora_audio",
error = %e,
"windows ptt: SetWindowsHookExW(WH_KEYBOARD_LL) failed"
);
report!(false);
return false;
}
};
let mouse_hook =
match SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, Some(HINSTANCE(h_instance.0)), 0) {
Ok(h) => h,
Err(e) => {
warn!(
target: "chanora_audio",
error = %e,
"windows ptt: SetWindowsHookExW(WH_MOUSE_LL) failed"
);
let _ = UnhookWindowsHookEx(kbd_hook);
report!(false);
return false;
}
};
info!(
target: "chanora_audio",
@@ -908,7 +920,7 @@ unsafe extern "system" fn kbd_hook_proc(code: i32, wparam: WPARAM, lparam: LPARA
}
});
}
CallNextHookEx(HHOOK(0), code, wparam, lparam)
CallNextHookEx(None, code, wparam, lparam)
}
/// Pure-logic dispatcher for a low-level keyboard hook event (L0
@@ -943,7 +955,7 @@ unsafe extern "system" fn mouse_hook_proc(code: i32, wparam: WPARAM, lparam: LPA
}
});
}
CallNextHookEx(HHOOK(0), code, wparam, lparam)
CallNextHookEx(None, code, wparam, lparam)
}
/// Pure-logic dispatcher for a low-level mouse hook event (L0
+27 -61
View File
@@ -8,17 +8,17 @@
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub mod apple_coreml;
pub mod resampler;
#[cfg(not(target_os = "ios"))]
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
pub mod silero_onnx;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{OnceLock, RwLock};
use crate::frame::{f32_to_i16, i16_to_f32};
use crate::{AudioError, VadBackend};
use crate::AudioError;
use resampler::{Downsampler48to16, INPUT_FRAME_10MS};
#[cfg(not(target_os = "ios"))]
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
pub use silero_onnx::SileroOnnxVad;
/// Voice activity detector output for one 10 ms frame.
@@ -108,39 +108,12 @@ pub fn process_i16_10ms(detector: &mut dyn VoiceActivityDetector, samples: &[i16
detector.process_10ms(&frame)
}
/// Callback-side policy for optional model-backed VAD workers.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) enum VadWorkerPolicy {
/// Keep using the already-available model worker.
UseWorker,
/// No worker may be constructed on the callback thread; use WebRTC fallback.
UseFallback,
/// This backend does not need a model worker.
NotModelBacked,
}
/// Decide whether a realtime callback may use a model-backed VAD worker.
///
/// Model/worker construction is intentionally absent from this policy: if a
/// worker is not already present, callbacks must stay nonblocking and fall back.
pub(crate) fn callback_vad_worker_policy(
voice_activity_mode: bool,
backend: VadBackend,
worker_available: bool,
) -> VadWorkerPolicy {
if !voice_activity_mode || backend != VadBackend::SileroOnnx {
return VadWorkerPolicy::NotModelBacked;
}
if worker_available {
VadWorkerPolicy::UseWorker
} else {
VadWorkerPolicy::UseFallback
}
}
static SILERO_MODEL_PATH_OVERRIDE: OnceLock<RwLock<Option<String>>> = OnceLock::new();
static SILERO_MODEL_EPOCH: AtomicU64 = AtomicU64::new(0);
#[cfg(test)]
pub(crate) static SILERO_MODEL_PATH_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn silero_model_path_override() -> &'static RwLock<Option<String>> {
SILERO_MODEL_PATH_OVERRIDE.get_or_init(|| RwLock::new(None))
}
@@ -175,6 +148,19 @@ pub fn silero_model_epoch() -> u64 {
SILERO_MODEL_EPOCH.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn clear_silero_model_path_for_test() {
set_silero_model_path_for_test(None);
}
#[cfg(test)]
pub(crate) fn set_silero_model_path_for_test(path: Option<String>) {
if let Ok(mut guard) = silero_model_path_override().write() {
*guard = path;
SILERO_MODEL_EPOCH.fetch_add(1, Ordering::Relaxed);
}
}
/// Return the expected path of the Silero VAD v6 ONNX model on
/// supported platforms.
/// The model is shipped as a Flutter asset and copied to the app's
@@ -264,13 +250,20 @@ mod tests {
#[test]
fn set_silero_model_path_rejects_missing_file() {
let _guard = SILERO_MODEL_PATH_TEST_LOCK.lock().unwrap();
clear_silero_model_path_for_test();
let result = set_silero_model_path("/definitely/not/a/silero_vad.onnx");
assert!(result.is_err());
clear_silero_model_path_for_test();
}
#[test]
fn set_silero_model_path_updates_override_and_epoch() {
let _guard = SILERO_MODEL_PATH_TEST_LOCK.lock().unwrap();
clear_silero_model_path_for_test();
let path =
std::env::temp_dir().join(format!("chanora_test_silero_{}.onnx", std::process::id()));
std::fs::write(&path, b"test").unwrap();
@@ -280,34 +273,7 @@ mod tests {
assert!(silero_model_epoch() > before);
assert_eq!(silero_model_bundle_path(), path.to_string_lossy());
clear_silero_model_path_for_test();
let _ = std::fs::remove_file(path);
}
#[test]
fn callback_policy_uses_existing_model_worker_only() {
assert_eq!(
callback_vad_worker_policy(true, VadBackend::SileroOnnx, true),
VadWorkerPolicy::UseWorker
);
assert_eq!(
callback_vad_worker_policy(true, VadBackend::SileroOnnx, false),
VadWorkerPolicy::UseFallback
);
}
#[test]
fn callback_policy_keeps_disabled_and_webrtc_paths_worker_free() {
assert_eq!(
callback_vad_worker_policy(false, VadBackend::SileroOnnx, false),
VadWorkerPolicy::NotModelBacked
);
assert_eq!(
callback_vad_worker_policy(true, VadBackend::Disabled, false),
VadWorkerPolicy::NotModelBacked
);
assert_eq!(
callback_vad_worker_policy(true, VadBackend::WebrtcVad, false),
VadWorkerPolicy::NotModelBacked
);
}
}
+33 -1
View File
@@ -361,6 +361,31 @@ impl SileroOnnxVadWorker {
})
}
#[cfg(test)]
pub(crate) fn stale_test_worker() -> Self {
let (tx, rx) = std::sync::mpsc::sync_channel::<SileroFrameMessage>(64);
let alive = Arc::new(AtomicBool::new(true));
let alive_for_thread = alive.clone();
let handle = std::thread::Builder::new()
.name("chanora-silero-vad-stale-test".to_string())
.spawn(move || {
while alive_for_thread.load(Ordering::Relaxed) {
match rx.recv_timeout(std::time::Duration::from_millis(10)) {
Ok(_) | Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
}
}
})
.ok();
Self {
tx: Some(tx),
latest_probability: Arc::new(AtomicU32::new(0.0_f32.to_bits())),
latest_processed_seq: Arc::new(AtomicU64::new(u64::MAX)),
alive,
handle,
}
}
/// Best-effort enqueue of a 10 ms frame for background inference.
pub fn try_send(&self, seq: u64, frame: &[f32; super::resampler::INPUT_FRAME_10MS]) -> bool {
let Some(tx) = &self.tx else {
@@ -393,8 +418,15 @@ impl SileroOnnxVadWorker {
impl Drop for SileroOnnxVadWorker {
fn drop(&mut self) {
self.alive.store(false, Ordering::Relaxed);
// Drop the sender first so the worker thread's rx.recv() returns
// Err and the loop exits promptly.
let _ = self.tx.take();
let _ = self.handle.take();
// Join the thread instead of detaching. The channel close
// unblocks rx.recv() so the join is bounded; it waits at most
// until the current in-flight inference completes.
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
+2 -2
View File
@@ -196,8 +196,8 @@ fn windows_exercise() {
// start/stop lifecycle through the public trait surface so
// any info!/warn! the factory or the backend's `start` path
// emits is captured by the layer.
use chanora_audio::ptt_backends::{select_ptt_backend, PttBinding, PttInputClass};
use chanora_audio::AudioTransmitGate;
use chanora_audio::ptt_backends::{PttBinding, PttInputClass};
use chanora_audio::{select_ptt_backend, AudioTransmitGate};
let mut backend = select_ptt_backend();
let gate = AudioTransmitGate::new(false);
+53 -23
View File
@@ -1062,10 +1062,8 @@ pub enum BridgeAudioRoute {
/// Bridge iOS voice-processing mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BridgeIosVoiceProcessingMode {
/// Shipping VPIO path.
/// Apple VoiceProcessingIO path.
PlatformVoiceProcessing,
/// Experimental Sonora path.
SonoraExperimental,
}
/// Bridge processing backend.
@@ -1223,7 +1221,6 @@ impl From<BridgeIosVoiceProcessingMode> for chanora_core::IosVoiceProcessingMode
fn from(mode: BridgeIosVoiceProcessingMode) -> Self {
match mode {
BridgeIosVoiceProcessingMode::PlatformVoiceProcessing => Self::PlatformVoiceProcessing,
BridgeIosVoiceProcessingMode::SonoraExperimental => Self::SonoraExperimental,
}
}
}
@@ -1234,7 +1231,6 @@ impl From<chanora_core::IosVoiceProcessingMode> for BridgeIosVoiceProcessingMode
chanora_core::IosVoiceProcessingMode::PlatformVoiceProcessing => {
Self::PlatformVoiceProcessing
}
chanora_core::IosVoiceProcessingMode::SonoraExperimental => Self::SonoraExperimental,
}
}
}
@@ -1469,6 +1465,54 @@ pub async fn init_storage(dir: String) -> Result<(), BridgeError> {
Ok(())
}
/// Configure the bridge blob cache root.
pub async fn init_cache(dir: String) -> Result<(), BridgeError> {
runtime()
.spawn(async move { session().init_cache(&dir).await })
.await
.map_err(|e| task_join_error("init_cache", e))??;
Ok(())
}
/// Resolve avatar bytes through the bridge.
pub async fn download_avatar(
avatar_hash: String,
client_uid: String,
) -> Result<Option<Vec<u8>>, BridgeError> {
runtime()
.spawn(async move { session().get_avatar(&avatar_hash, &client_uid).await })
.await
.map_err(|e| task_join_error("download_avatar", e))?
.map_err(BridgeError::from)
}
/// Resolve icon bytes through the bridge.
pub async fn download_icon(icon_id: u64) -> Result<Option<Vec<u8>>, BridgeError> {
runtime()
.spawn(async move { session().get_icon(icon_id).await })
.await
.map_err(|e| task_join_error("download_icon", e))?
.map_err(BridgeError::from)
}
/// Purge cached protocol-owned assets.
pub async fn clear_file_cache() -> Result<(), BridgeError> {
runtime()
.spawn(async move { session().clear_cache().await })
.await
.map_err(|e| task_join_error("clear_file_cache", e))??;
Ok(())
}
/// Report the configured file-cache size.
pub async fn file_cache_size() -> Result<u64, BridgeError> {
runtime()
.spawn(async move { session().cache_size().await })
.await
.map_err(|e| task_join_error("file_cache_size", e))?
.map_err(BridgeError::from)
}
/// Bookmark DTO mirroring [`chanora_core::Bookmark`].
#[derive(Debug, Clone)]
pub struct BridgeBookmark {
@@ -2361,25 +2405,11 @@ pub async fn set_ios_voice_processing_mode(
let config = BridgeAudioProcessingConfig {
route: BridgeAudioRoute::Speaker,
ios_mode: mode,
processing_backend: match mode {
BridgeIosVoiceProcessingMode::PlatformVoiceProcessing => {
BridgeAudioBackend::PlatformVoiceProcessing
}
BridgeIosVoiceProcessingMode::SonoraExperimental => BridgeAudioBackend::WebrtcApm,
},
processing_backend: BridgeAudioBackend::PlatformVoiceProcessing,
vad_backend: BridgeVadBackend::SileroOnnx,
aec: match mode {
BridgeIosVoiceProcessingMode::PlatformVoiceProcessing => BridgeEffectOwner::Platform,
BridgeIosVoiceProcessingMode::SonoraExperimental => BridgeEffectOwner::WebrtcApm,
},
ns: match mode {
BridgeIosVoiceProcessingMode::PlatformVoiceProcessing => BridgeEffectOwner::Platform,
BridgeIosVoiceProcessingMode::SonoraExperimental => BridgeEffectOwner::WebrtcApm,
},
agc: match mode {
BridgeIosVoiceProcessingMode::PlatformVoiceProcessing => BridgeEffectOwner::Platform,
BridgeIosVoiceProcessingMode::SonoraExperimental => BridgeEffectOwner::WebrtcApm,
},
aec: BridgeEffectOwner::Platform,
ns: BridgeEffectOwner::Platform,
agc: BridgeEffectOwner::Platform,
hpf_enabled: true,
limiter_enabled: true,
vad_hangover_ms: 500,
+253 -50
View File
@@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -20394775;
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 635684021;
// Section: executor
@@ -186,6 +186,41 @@ fn wire__crate__api__bridge_init_impl(
},
)
}
fn wire__crate__api__clear_file_cache_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "clear_file_cache",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
deserializer.end();
move |context| async move {
transform_result_sse::<_, crate::BridgeError>(
(move || async move {
let output_ok = crate::api::clear_file_cache().await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__api__client_profile_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@@ -332,6 +367,80 @@ fn wire__crate__api__disconnect_impl(
},
)
}
fn wire__crate__api__download_avatar_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "download_avatar",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_avatar_hash = <String>::sse_decode(&mut deserializer);
let api_client_uid = <String>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, crate::BridgeError>(
(move || async move {
let output_ok =
crate::api::download_avatar(api_avatar_hash, api_client_uid).await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__api__download_icon_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "download_icon",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_icon_id = <u64>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, crate::BridgeError>(
(move || async move {
let output_ok = crate::api::download_icon(api_icon_id).await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__api__enable_audio_debug_wav_dump_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@@ -434,6 +543,41 @@ fn wire__crate__api__export_diagnostics_impl(
},
)
}
fn wire__crate__api__file_cache_size_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "file_cache_size",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
deserializer.end();
move |context| async move {
transform_result_sse::<_, crate::BridgeError>(
(move || async move {
let output_ok = crate::api::file_cache_size().await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__api__get_audio_processing_config_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@@ -702,6 +846,42 @@ fn wire__crate__api__handle_route_change_impl(
},
)
}
fn wire__crate__api__init_cache_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "init_cache",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_dir = <String>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, crate::BridgeError>(
(move || async move {
let output_ok = crate::api::init_cache(api_dir).await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__api__init_storage_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@@ -2409,7 +2589,6 @@ impl SseDecode for crate::api::BridgeIosVoiceProcessingMode {
let mut inner = <i32>::sse_decode(deserializer);
return match inner {
0 => crate::api::BridgeIosVoiceProcessingMode::PlatformVoiceProcessing,
1 => crate::api::BridgeIosVoiceProcessingMode::SonoraExperimental,
_ => unreachable!(
"Invalid variant for BridgeIosVoiceProcessingMode: {}",
inner
@@ -2764,6 +2943,17 @@ impl SseDecode for Option<u64> {
}
}
impl SseDecode for Option<Vec<u8>> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
if (<bool>::sse_decode(deserializer)) {
return Some(<Vec<u8>>::sse_decode(deserializer));
} else {
return None;
}
}
}
impl SseDecode for crate::api::PermissionStateKind {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -2817,45 +3007,50 @@ fn pde_ffi_dispatcher_primary_impl(
2 => wire__crate__api__audio_processing_stats_impl(port, ptr, rust_vec_len, data_len),
3 => wire__crate__api__audio_stats_impl(port, ptr, rust_vec_len, data_len),
4 => wire__crate__api__bridge_init_impl(port, ptr, rust_vec_len, data_len),
5 => wire__crate__api__client_profile_impl(port, ptr, rust_vec_len, data_len),
6 => wire__crate__api__connect_impl(port, ptr, rust_vec_len, data_len),
7 => wire__crate__api__delete_bookmark_impl(port, ptr, rust_vec_len, data_len),
8 => wire__crate__api__disconnect_impl(port, ptr, rust_vec_len, data_len),
9 => wire__crate__api__enable_audio_debug_wav_dump_impl(port, ptr, rust_vec_len, data_len),
10 => wire__crate__api__events_stream_impl(port, ptr, rust_vec_len, data_len),
12 => wire__crate__api__get_audio_processing_config_impl(port, ptr, rust_vec_len, data_len),
13 => wire__crate__api__get_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
14 => wire__crate__api__get_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
15 => wire__crate__api__get_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
20 => wire__crate__api__init_storage_impl(port, ptr, rust_vec_len, data_len),
21 => wire__crate__api__input_level_stream_impl(port, ptr, rust_vec_len, data_len),
22 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
23 => wire__crate__api__list_audio_devices_impl(port, ptr, rust_vec_len, data_len),
24 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len),
26 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len),
27 => wire__crate__api__prefetch_server_impl(port, ptr, rust_vec_len, data_len),
28 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len),
30 => wire__crate__api__send_chat_message_impl(port, ptr, rust_vec_len, data_len),
32 => wire__crate__api__set_audio_processing_config_impl(port, ptr, rust_vec_len, data_len),
33 => wire__crate__api__set_client_volume_impl(port, ptr, rust_vec_len, data_len),
34 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len),
35 => wire__crate__api__set_input_device_impl(port, ptr, rust_vec_len, data_len),
36 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len),
37 => {
5 => wire__crate__api__clear_file_cache_impl(port, ptr, rust_vec_len, data_len),
6 => wire__crate__api__client_profile_impl(port, ptr, rust_vec_len, data_len),
7 => wire__crate__api__connect_impl(port, ptr, rust_vec_len, data_len),
8 => wire__crate__api__delete_bookmark_impl(port, ptr, rust_vec_len, data_len),
9 => wire__crate__api__disconnect_impl(port, ptr, rust_vec_len, data_len),
10 => wire__crate__api__download_avatar_impl(port, ptr, rust_vec_len, data_len),
11 => wire__crate__api__download_icon_impl(port, ptr, rust_vec_len, data_len),
12 => wire__crate__api__enable_audio_debug_wav_dump_impl(port, ptr, rust_vec_len, data_len),
13 => wire__crate__api__events_stream_impl(port, ptr, rust_vec_len, data_len),
15 => wire__crate__api__file_cache_size_impl(port, ptr, rust_vec_len, data_len),
16 => wire__crate__api__get_audio_processing_config_impl(port, ptr, rust_vec_len, data_len),
17 => wire__crate__api__get_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
18 => wire__crate__api__get_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
19 => wire__crate__api__get_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
24 => wire__crate__api__init_cache_impl(port, ptr, rust_vec_len, data_len),
25 => wire__crate__api__init_storage_impl(port, ptr, rust_vec_len, data_len),
26 => wire__crate__api__input_level_stream_impl(port, ptr, rust_vec_len, data_len),
27 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
28 => wire__crate__api__list_audio_devices_impl(port, ptr, rust_vec_len, data_len),
29 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len),
31 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len),
32 => wire__crate__api__prefetch_server_impl(port, ptr, rust_vec_len, data_len),
33 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len),
35 => wire__crate__api__send_chat_message_impl(port, ptr, rust_vec_len, data_len),
37 => wire__crate__api__set_audio_processing_config_impl(port, ptr, rust_vec_len, data_len),
38 => wire__crate__api__set_client_volume_impl(port, ptr, rust_vec_len, data_len),
39 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len),
40 => wire__crate__api__set_input_device_impl(port, ptr, rust_vec_len, data_len),
41 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len),
42 => {
wire__crate__api__set_ios_voice_processing_mode_impl(port, ptr, rust_vec_len, data_len)
}
39 => wire__crate__api__set_output_device_impl(port, ptr, rust_vec_len, data_len),
40 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len),
41 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len),
42 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
43 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
44 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
45 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
46 => wire__crate__api__set_vad_model_path_impl(port, ptr, rust_vec_len, data_len),
47 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
48 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len),
49 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len),
50 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len),
44 => wire__crate__api__set_output_device_impl(port, ptr, rust_vec_len, data_len),
45 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len),
46 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len),
47 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
48 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
49 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
50 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
51 => wire__crate__api__set_vad_model_path_impl(port, ptr, rust_vec_len, data_len),
52 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
53 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len),
54 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len),
55 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}
@@ -2868,19 +3063,19 @@ fn pde_ffi_dispatcher_sync_impl(
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
match func_id {
11 => wire__crate__api__export_diagnostics_impl(ptr, rust_vec_len, data_len),
16 => wire__crate__api__handle_interruption_began_impl(ptr, rust_vec_len, data_len),
17 => wire__crate__api__handle_interruption_ended_impl(ptr, rust_vec_len, data_len),
18 => wire__crate__api__handle_media_services_reset_with_route_impl(
14 => wire__crate__api__export_diagnostics_impl(ptr, rust_vec_len, data_len),
20 => wire__crate__api__handle_interruption_began_impl(ptr, rust_vec_len, data_len),
21 => wire__crate__api__handle_interruption_ended_impl(ptr, rust_vec_len, data_len),
22 => wire__crate__api__handle_media_services_reset_with_route_impl(
ptr,
rust_vec_len,
data_len,
),
19 => wire__crate__api__handle_route_change_impl(ptr, rust_vec_len, data_len),
25 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len),
29 => wire__crate__api__record_lifecycle_event_impl(ptr, rust_vec_len, data_len),
31 => wire__crate__api__set_audio_output_route_impl(ptr, rust_vec_len, data_len),
38 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
23 => wire__crate__api__handle_route_change_impl(ptr, rust_vec_len, data_len),
30 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len),
34 => wire__crate__api__record_lifecycle_event_impl(ptr, rust_vec_len, data_len),
36 => wire__crate__api__set_audio_output_route_impl(ptr, rust_vec_len, data_len),
43 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}
@@ -3445,7 +3640,6 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeIosVoiceProcessingMode
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
match self {
Self::PlatformVoiceProcessing => 0.into_dart(),
Self::SonoraExperimental => 1.into_dart(),
_ => unreachable!(),
}
}
@@ -4215,7 +4409,6 @@ impl SseEncode for crate::api::BridgeIosVoiceProcessingMode {
<i32>::sse_encode(
match self {
crate::api::BridgeIosVoiceProcessingMode::PlatformVoiceProcessing => 0,
crate::api::BridgeIosVoiceProcessingMode::SonoraExperimental => 1,
_ => {
unimplemented!("");
}
@@ -4559,6 +4752,16 @@ impl SseEncode for Option<u64> {
}
}
impl SseEncode for Option<Vec<u8>> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<bool>::sse_encode(self.is_some(), serializer);
if let Some(value) = self {
<Vec<u8>>::sse_encode(value, serializer);
}
}
}
impl SseEncode for crate::api::PermissionStateKind {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
+4
View File
@@ -110,9 +110,13 @@ impl From<chanora_core::CoreError> for BridgeError {
code,
message,
}) => BridgeError::ServerRejected { code, message },
chanora_core::CoreError::Protocol(chanora_core::ProtocolError::FileTransfer(p)) => {
BridgeError::Connection(format!("file transfer: {p}"))
}
chanora_core::CoreError::Protocol(p) => BridgeError::Connection(format!("{p}")),
chanora_core::CoreError::Audio(a) => BridgeError::Connection(format!("audio: {a}")),
chanora_core::CoreError::Storage(s) => BridgeError::Connection(format!("storage: {s}")),
chanora_core::CoreError::Cache(c) => BridgeError::Connection(format!("cache: {c}")),
other => BridgeError::Unmapped(format!("{other}")),
}
}
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "chanora_cache"
description = "Chanora disposable content-addressed blob cache for avatars and icons"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[dependencies]
cacache = "13"
thiserror.workspace = true
tokio = { version = "1", features = ["fs", "rt"] }
tracing.workspace = true
[dev-dependencies]
tempfile = "3"
tokio = { version = "1", features = ["rt", "macros", "time"] }
+359
View File
@@ -0,0 +1,359 @@
//! Disposable content-addressed blob cache for avatar and icon files.
//!
//! Wraps [`cacache`] for production-tested crash safety and integrity
//! verification. The on-disk layout is managed by cacache (content-v2,
//! index-v2). Chanora maps protocol keys (`av_<md5>`, `ic_<crc32>`) to
//! cacache string keys.
//!
//! This crate is intentionally separate from `chanora_storage`:
//! storage owns persistent identity/bookmark data; cache owns
//! reconstructible, disposable blob data with different durability
//! and backup semantics.
#![forbid(unsafe_code)]
#![warn(missing_docs)]
use std::path::{Path, PathBuf};
/// Errors raised by the blob cache.
#[derive(Debug, thiserror::Error)]
pub enum BlobCacheError {
/// Filesystem I/O error.
#[error("io: {0}")]
Io(String),
/// Key validation error.
#[error("invalid key: {0}")]
InvalidKey(String),
}
/// Content-addressed blob cache backed by cacache.
pub struct BlobCache {
cache_dir: PathBuf,
/// Maximum total cache size in bytes. 0 = no limit.
max_bytes: u64,
}
/// Avatar blob prefix.
pub const PREFIX_AVATAR: &str = "av_";
/// Icon blob prefix.
pub const PREFIX_ICON: &str = "ic_";
impl BlobCache {
/// Create or open a [`BlobCache`] rooted at `cache_dir/chanora/`.
///
/// Creates the cacache directory. `max_bytes` sets the eviction
/// threshold; 0 means no automatic eviction.
pub fn new(cache_dir: impl AsRef<Path>, max_bytes: u64) -> Result<Self, BlobCacheError> {
let cache_dir = cache_dir.as_ref().join("chanora").join("blobs");
// cacache creates the directory on first write, but we create
// it eagerly so total_size() works before any writes.
std::fs::create_dir_all(&cache_dir)
.map_err(|e| BlobCacheError::Io(format!("mkdir cache: {e}")))?;
Ok(Self {
cache_dir,
max_bytes,
})
}
/// Store a blob. `prefix` is [`PREFIX_AVATAR`] or [`PREFIX_ICON`].
/// `key` is the content hash (MD5 hex for avatars, unsigned
/// decimal CRC32 for icons).
///
/// Cacache handles dedup and integrity internally.
pub async fn put(
&self,
prefix: &str,
key: &str,
data: &[u8],
) -> Result<(), BlobCacheError> {
validate_key(prefix, key)?;
let cache_key = format!("{prefix}{key}");
cacache::write(&self.cache_dir, &cache_key, data)
.await
.map_err(|e| BlobCacheError::Io(format!("cacache write: {e}")))?;
Ok(())
}
/// Read a blob. Returns `None` if not cached.
///
/// Cacache verifies SSRI integrity on every read.
pub async fn get(&self, prefix: &str, key: &str) -> Result<Option<Vec<u8>>, BlobCacheError> {
validate_key(prefix, key)?;
let cache_key = format!("{prefix}{key}");
match cacache::read(&self.cache_dir, &cache_key).await {
Ok(data) => Ok(Some(data)),
Err(cacache::Error::EntryNotFound(_, _)) => Ok(None),
Err(e) => {
// Integrity failure or I/O error — remove corrupt entry.
tracing::warn!(
target: "chanora_cache",
key = %cache_key,
error = %e,
"cache read failed; removing entry"
);
let _ = cacache::remove(&self.cache_dir, &cache_key).await;
Ok(None)
}
}
}
/// Delete a specific blob.
pub async fn remove(&self, prefix: &str, key: &str) -> Result<(), BlobCacheError> {
validate_key(prefix, key)?;
let cache_key = format!("{prefix}{key}");
cacache::remove(&self.cache_dir, &cache_key)
.await
.map_err(|e| BlobCacheError::Io(format!("cacache remove: {e}")))?;
Ok(())
}
/// Delete all blobs.
pub async fn clear(&self) -> Result<(), BlobCacheError> {
let path = self.cache_dir.clone();
tokio::task::spawn_blocking(move || {
if path.exists() {
std::fs::remove_dir_all(&path)
.map_err(|e| BlobCacheError::Io(format!("clear cache: {e}")))?;
std::fs::create_dir_all(&path)
.map_err(|e| BlobCacheError::Io(format!("recreate cache dir: {e}")))?;
}
Ok(())
})
.await
.map_err(|e| BlobCacheError::Io(format!("clear task: {e}")))?
}
/// Return total bytes used by all blobs.
///
/// Walks cacache entries and sums sizes.
pub async fn total_size(&self) -> Result<u64, BlobCacheError> {
let cache_dir = self.cache_dir.clone();
tokio::task::spawn_blocking(move || {
let mut total: u64 = 0;
for entry in cacache::list_sync(&cache_dir) {
match entry {
Ok(meta) => total += meta.size as u64,
Err(e) => {
tracing::warn!(
target: "chanora_cache",
error = %e,
"skipping bad entry during size scan"
);
}
}
}
Ok(total)
})
.await
.map_err(|e| BlobCacheError::Io(format!("total_size task: {e}")))?
}
/// Evict oldest entries by timestamp until total size is under
/// `max_bytes`. Call on startup or periodically. No-op if
/// `max_bytes` is 0.
pub async fn evict(&self) -> Result<(), BlobCacheError> {
if self.max_bytes == 0 {
return Ok(());
}
let cache_dir = self.cache_dir.clone();
let max_bytes = self.max_bytes;
tokio::task::spawn_blocking(move || {
let mut entries: Vec<(String, usize, u128)> = Vec::new();
for entry in cacache::list_sync(&cache_dir) {
match entry {
Ok(meta) => {
entries.push((meta.key, meta.size, meta.time));
}
Err(e) => {
tracing::warn!(
target: "chanora_cache",
error = %e,
"skipping bad entry during eviction scan"
);
}
}
}
let total: usize = entries.iter().map(|(_, s, _)| *s).sum();
if total as u64 <= max_bytes {
return Ok(());
}
entries.sort_by_key(|(_, _, t)| *t);
let mut freed: usize = 0;
let target = total - max_bytes as usize;
for (key, size, _) in entries {
if freed >= target {
break;
}
let _ = cacache::remove_sync(&cache_dir, &key);
freed += size;
}
tracing::info!(
target: "chanora_cache",
freed_bytes = freed,
"evicted oldest blobs"
);
Ok(())
})
.await
.map_err(|e| BlobCacheError::Io(format!("evict task: {e}")))?
}
}
/// Validate key format to prevent malformed entries.
fn validate_key(prefix: &str, key: &str) -> Result<(), BlobCacheError> {
if !matches!(prefix, PREFIX_AVATAR | PREFIX_ICON) {
return Err(BlobCacheError::InvalidKey(format!("bad prefix: {prefix}")));
}
match prefix {
PREFIX_AVATAR => {
// MD5 hex = exactly 32 hex chars.
if key.len() != 32 || !key.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(BlobCacheError::InvalidKey(format!(
"avatar key must be 32 hex chars, got: {key}"
)));
}
}
PREFIX_ICON => {
// Unsigned CRC32 = decimal digits.
if key.is_empty() || !key.chars().all(|c| c.is_ascii_digit()) {
return Err(BlobCacheError::InvalidKey(format!(
"icon key must be decimal digits, got: {key}"
)));
}
}
_ => unreachable!(),
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn tempdir() -> tempfile::TempDir {
tempfile::Builder::new()
.prefix("chanora_cache_test_")
.tempdir()
.unwrap()
}
#[tokio::test]
async fn put_get_roundtrip() {
let tmp = tempdir();
let cache = BlobCache::new(&tmp, 0).unwrap();
assert!(cache
.get(PREFIX_AVATAR, "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6")
.await
.unwrap()
.is_none());
cache
.put(PREFIX_AVATAR, "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", b"avatar-bytes")
.await
.unwrap();
let data = cache
.get(PREFIX_AVATAR, "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6")
.await
.unwrap();
assert_eq!(data.as_deref(), Some(b"avatar-bytes".as_slice()));
}
#[tokio::test]
async fn get_missing_returns_none() {
let tmp = tempdir();
let cache = BlobCache::new(&tmp, 0).unwrap();
assert!(cache
.get(PREFIX_AVATAR, "00000000000000000000000000000000")
.await
.unwrap()
.is_none());
}
#[tokio::test]
async fn clear_removes_all() {
let tmp = tempdir();
let cache = BlobCache::new(&tmp, 0).unwrap();
cache
.put(PREFIX_AVATAR, "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", b"data")
.await
.unwrap();
cache.put(PREFIX_ICON, "12345", b"icon").await.unwrap();
cache.clear().await.unwrap();
assert_eq!(cache.total_size().await.unwrap(), 0);
}
#[tokio::test]
async fn total_size_accounts_for_all_entries() {
let tmp = tempdir();
let cache = BlobCache::new(&tmp, 0).unwrap();
cache
.put(PREFIX_AVATAR, "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", b"12345")
.await
.unwrap();
cache.put(PREFIX_ICON, "99999", b"12").await.unwrap();
assert_eq!(cache.total_size().await.unwrap(), 5 + 2);
}
#[tokio::test]
async fn invalid_key_rejected() {
let tmp = tempdir();
let cache = BlobCache::new(&tmp, 0).unwrap();
// Too short for MD5.
assert!(cache.put(PREFIX_AVATAR, "abc", b"data").await.is_err());
// Non-hex in MD5.
assert!(cache
.put(PREFIX_AVATAR, "g".repeat(32).as_str(), b"data")
.await
.is_err());
// Non-digit in icon key.
assert!(cache.put(PREFIX_ICON, "12a45", b"data").await.is_err());
// Bad prefix.
assert!(cache.put("xx_", "abc", b"data").await.is_err());
}
#[tokio::test]
async fn evict_deletes_oldest_until_under_cap() {
let tmp = tempdir();
// 10 byte cap.
let cache = BlobCache::new(&tmp, 10).unwrap();
cache
.put(
PREFIX_AVATAR,
"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
b"12345678",
)
.await
.unwrap(); // 8 bytes
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
cache
.put(PREFIX_ICON, "11111", b"12345")
.await
.unwrap(); // 5 bytes → total 13, over cap
cache.evict().await.unwrap();
// Oldest (avatar) should be evicted.
assert!(cache
.get(PREFIX_AVATAR, "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6")
.await
.unwrap()
.is_none());
assert!(cache.get(PREFIX_ICON, "11111").await.unwrap().is_some());
}
#[tokio::test]
async fn remove_deletes_entry() {
let tmp = tempdir();
let cache = BlobCache::new(&tmp, 0).unwrap();
cache
.put(PREFIX_AVATAR, "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", b"data")
.await
.unwrap();
cache
.remove(PREFIX_AVATAR, "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6")
.await
.unwrap();
assert!(cache
.get(PREFIX_AVATAR, "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6")
.await
.unwrap()
.is_none());
}
}
+129 -5
View File
@@ -24,6 +24,7 @@ use base64::prelude::*;
use chanora_resolver::ChanoraResolver;
use futures::prelude::*;
use std::collections::HashMap;
use tokio::io::AsyncReadExt;
use tokio::sync::{mpsc, oneshot};
use tracing::{info, warn};
@@ -32,7 +33,8 @@ use tsclientlib::messages::s2c::{InClientDbInfoPart, InMessage};
use tsclientlib::prelude::*;
use tsclientlib::{
ChannelId as TsChannelId, ClientId as TsClientId, Connection, ConnectionStats,
DisconnectOptions, Identity, MessageHandle, OutCommandExt, StreamItem, Version,
DisconnectOptions, FileDownloadResult, FiletransferHandle, Identity, MessageHandle,
OutCommandExt, StreamItem, Version,
};
use tsproto_packets::packets::{Direction, Flags, InAudioBuf, OutCommand, OutPacket, PacketType};
use tsproto_types::ClientType;
@@ -60,6 +62,9 @@ type PendingMoves = HashMap<
),
>;
type PendingDownloads =
HashMap<FiletransferHandle, oneshot::Sender<Result<Vec<u8>, ProtocolError>>>;
struct EventChannels {
voice_in: mpsc::Sender<InboundVoice>,
chat: mpsc::Sender<ChatMessage>,
@@ -207,6 +212,10 @@ enum Request {
client_id: u64,
reply: oneshot::Sender<Result<ClientProfile, ProtocolError>>,
},
DownloadFile {
path: String,
reply: oneshot::Sender<Result<Vec<u8>, ProtocolError>>,
},
}
/// Why a [`ProtocolClient`] task ended. Distinguishes a user-driven
@@ -366,6 +375,26 @@ impl ProtocolClient {
.map_err(|_| ProtocolError::Lost("client_profile reply dropped".to_string()))?
}
async fn download_file(&self, path: String) -> Result<Vec<u8>, ProtocolError> {
let (tx, rx) = oneshot::channel();
self.tx
.send(Request::DownloadFile { path, reply: tx })
.await
.map_err(|_| ProtocolError::Lost("connection task is gone".to_string()))?;
rx.await
.map_err(|_| ProtocolError::Lost("download_file reply dropped".to_string()))?
}
/// Download the current avatar bytes for a TeamSpeak client UID.
pub async fn download_avatar(&self, client_uid: &str) -> Result<Vec<u8>, ProtocolError> {
self.download_file(avatar_download_path(client_uid)).await
}
/// Download the current channel/server icon bytes for an icon id.
pub async fn download_icon(&self, icon_id: u64) -> Result<Vec<u8>, ProtocolError> {
self.download_file(icon_download_path(icon_id)).await
}
/// Disconnect cleanly. Blocks until the task exits.
pub async fn disconnect(self) {
let (tx, rx) = oneshot::channel();
@@ -713,6 +742,7 @@ async fn connection_task(
// deadline so a server that never replies doesn't leak the
// reply channel — at most 3 s of pending state per move.
let mut pending_moves: PendingMoves = HashMap::new();
let mut pending_downloads: PendingDownloads = HashMap::new();
let mut voice_activity: HashMap<u64, Instant> = HashMap::new();
let mut poke_limiter = PokeLimiter::new();
@@ -737,6 +767,12 @@ async fn connection_task(
StreamItem::Audio(buf) => {
handle_audio_stream_item(&channels.voice_in, &mut voice_activity, buf).await;
}
StreamItem::FileDownload(handle, result) => {
handle_download_stream_item(&mut pending_downloads, handle, result).await;
}
StreamItem::FiletransferFailed(handle, error) => {
handle_download_failure(&mut pending_downloads, handle, error);
}
other => handle_non_audio_stream_item(
&con,
other,
@@ -843,12 +879,25 @@ async fn connection_task(
client_id,
&channels,
&mut pending_moves,
&mut pending_downloads,
&mut voice_activity,
&mut poke_limiter,
)
.await;
let _ = reply.send(r);
}
Ok(Request::DownloadFile { path, reply }) => {
match con.download_file(TsChannelId(0), &path, None, None) {
Ok(handle) => {
pending_downloads.insert(handle, reply);
}
Err(e) => {
let _ = reply.send(Err(ProtocolError::FileTransfer(format!(
"start download {path}: {e}"
))));
}
}
}
Ok(Request::Disconnect(reply)) => {
let _ = con.disconnect(DisconnectOptions::new());
bounded_drain_stream(con.events(), DISCONNECT_EVENT_DRAIN_TIMEOUT).await;
@@ -1024,6 +1073,47 @@ fn handle_non_audio_stream_item(
}
}
async fn handle_download_stream_item(
pending_downloads: &mut PendingDownloads,
handle: FiletransferHandle,
result: FileDownloadResult,
) {
if let Some(reply) = pending_downloads.remove(&handle) {
let _ = reply.send(read_download_bytes(result).await);
}
}
fn handle_download_failure(
pending_downloads: &mut PendingDownloads,
handle: FiletransferHandle,
error: tsclientlib::Error,
) {
if let Some(reply) = pending_downloads.remove(&handle) {
let _ = reply.send(Err(ProtocolError::FileTransfer(error.to_string())));
}
}
const MAX_DOWNLOAD_SIZE: u64 = 10 * 1024 * 1024;
async fn read_download_bytes(result: FileDownloadResult) -> Result<Vec<u8>, ProtocolError> {
if result.size > MAX_DOWNLOAD_SIZE {
return Err(ProtocolError::FileTransfer(format!(
"download too large: {} bytes (max {})",
result.size, MAX_DOWNLOAD_SIZE
)));
}
let size = usize::try_from(result.size).map_err(|_| {
ProtocolError::FileTransfer(format!("download too large to buffer: {} bytes", result.size))
})?;
let mut stream = result.stream;
let mut bytes = vec![0_u8; size];
stream
.read_exact(&mut bytes)
.await
.map_err(|e| ProtocolError::FileTransfer(e.to_string()))?;
Ok(bytes)
}
async fn resolve_server_socket(address: &str) -> Result<SocketAddr, ProtocolError> {
let resolver = ChanoraResolver::new().map_err(|err| ProtocolError::DnsFailed {
host: address.to_string(),
@@ -1174,6 +1264,7 @@ async fn fetch_client_profile(
client_id: u64,
channels: &EventChannels,
pending_moves: &mut PendingMoves,
pending_downloads: &mut PendingDownloads,
voice_activity: &mut HashMap<u64, Instant>,
poke_limiter: &mut PokeLimiter,
) -> Result<ClientProfile, ProtocolError> {
@@ -1220,6 +1311,7 @@ async fn fetch_client_profile(
build_command("servergrouplist", &[], &[]),
channels,
pending_moves,
pending_downloads,
voice_activity,
poke_limiter,
)
@@ -1231,6 +1323,7 @@ async fn fetch_client_profile(
build_command("channelgrouplist", &[], &[]),
channels,
pending_moves,
pending_downloads,
voice_activity,
poke_limiter,
)
@@ -1246,6 +1339,7 @@ async fn fetch_client_profile(
),
channels,
pending_moves,
pending_downloads,
voice_activity,
poke_limiter,
)
@@ -1265,6 +1359,7 @@ async fn fetch_client_profile(
build_command("getconnectioninfo", &[("clid", client_id.to_string())], &[]),
channels,
pending_moves,
pending_downloads,
voice_activity,
poke_limiter,
)
@@ -1285,6 +1380,7 @@ async fn fetch_client_profile(
database_id,
channels,
pending_moves,
pending_downloads,
voice_activity,
poke_limiter,
)
@@ -1448,6 +1544,7 @@ async fn request_messages(
command: OutCommand,
channels: &EventChannels,
pending_moves: &mut PendingMoves,
pending_downloads: &mut PendingDownloads,
voice_activity: &mut HashMap<u64, Instant>,
poke_limiter: &mut PokeLimiter,
) -> Result<Vec<InMessage>, ProtocolError> {
@@ -1485,6 +1582,12 @@ async fn request_messages(
StreamItem::Audio(buf) => {
handle_audio_stream_item(&channels.voice_in, voice_activity, buf).await;
}
StreamItem::FileDownload(handle, result) => {
handle_download_stream_item(pending_downloads, handle, result).await;
}
StreamItem::FiletransferFailed(handle, error) => {
handle_download_failure(pending_downloads, handle, error);
}
other => handle_non_audio_stream_item(
con,
other,
@@ -1503,6 +1606,7 @@ async fn request_client_db_info(
dbid: tsclientlib::ClientDbId,
channels: &EventChannels,
pending_moves: &mut PendingMoves,
pending_downloads: &mut PendingDownloads,
voice_activity: &mut HashMap<u64, Instant>,
poke_limiter: &mut PokeLimiter,
) -> Result<InClientDbInfoPart, ProtocolError> {
@@ -1511,6 +1615,7 @@ async fn request_client_db_info(
build_command("clientdbinfo", &[("cldbid", dbid.0.to_string())], &[]),
channels,
pending_moves,
pending_downloads,
voice_activity,
poke_limiter,
)
@@ -1568,6 +1673,14 @@ fn uid_to_avatar_path(uid_b64: &str) -> String {
rendered
}
fn avatar_download_path(client_uid: &str) -> String {
format!("/avatar_{}", uid_to_avatar_path(client_uid))
}
fn icon_download_path(icon_id: u64) -> String {
format!("/icon_{icon_id}")
}
fn find_client_by_id<'a>(
clients: impl IntoIterator<Item = &'a Client>,
client_id: u64,
@@ -1951,10 +2064,11 @@ const _: () = {
#[cfg(test)]
mod tests {
use super::{
bounded_drain_stream, client_profile_refresh_plan, drain_voice_packets_for_tick,
is_server_query_client_type, send_with_timeout, server_socket_from_config,
sort_channels_tree_by, std_duration_millis, ConnectConfig, ProtocolClient, Request,
SendTimeoutError, DISCONNECT_REPLY_TIMEOUT,
avatar_download_path, bounded_drain_stream, client_profile_refresh_plan,
drain_voice_packets_for_tick, icon_download_path, is_server_query_client_type,
send_with_timeout, server_socket_from_config, sort_channels_tree_by,
std_duration_millis, ConnectConfig, ProtocolClient, Request, SendTimeoutError,
DISCONNECT_REPLY_TIMEOUT,
};
use futures::stream;
use std::time::Duration;
@@ -2052,6 +2166,16 @@ mod tests {
assert!(plan.needs_channel_groups);
}
#[test]
fn avatar_download_path_uses_uid_hex_encoding() {
assert_eq!(avatar_download_path("AQID"), "/avatar_abacad");
}
#[test]
fn icon_download_path_uses_unsigned_icon_id() {
assert_eq!(icon_download_path(42), "/icon_42");
}
#[test]
fn channel_sort_linked_list_under_one_parent() {
// Server emits four root-level channels in arbitrary HashMap
+4
View File
@@ -115,4 +115,8 @@ pub enum ProtocolError {
/// should never see this; if they do, it is a mapping bug here.
#[error("protocol backend: {0}")]
Backend(String),
/// A file transfer failed while downloading protocol-owned assets.
#[error("file transfer failed: {0}")]
FileTransfer(String),
}
+40
View File
@@ -0,0 +1,40 @@
# SAD Component → Source File Mapping
**Purpose:** Developer convenience mapping from ASPICE architecture components to source file locations. This is NOT an ASPICE document — it's a lookup for developers.
## Component Mapping
| SAD Component | Source Location |
|---|---|
| Flutter app shell | `apps/chanora_flutter/lib/main.dart`, services, widgets |
| Flutter service layer | `apps/chanora_flutter/lib/services/` |
| Flutter widget layer | `apps/chanora_flutter/lib/widgets/` |
| Bridge layer | `crates/chanora_bridge/src/api.rs`, `apps/chanora_flutter/lib/src/rust/` |
| Rust core | `core/chanora_core/src/lib.rs`, `events.rs`, `network_diagnostics.rs`, `ptt.rs` |
| Protocol adapter | `crates/chanora_protocol/src/` |
| State sync | `crates/chanora_state/src/lib.rs`, `channel_join.rs` |
| Audio subsystem | `crates/chanora_audio/src/` |
| Storage | `crates/chanora_storage/src/lib.rs` |
| Diagnostics | `crates/chanora_diagnostics/src/lib.rs` |
| Resolution and prefetch | `crates/chanora_resolver/src/lib.rs`, `crates/chanora_prefetch/src/lib.rs`, `prefetch_debouncer.dart` |
| Build and release hooks | `.github/workflows/`, `tools/`, platform project files |
## SDD Module Mapping
| SDD Module | Source Location |
|---|---|
| SDD-MOD-001 Flutter app bootstrap | `apps/chanora_flutter/lib/services/app_bootstrap.dart`, `main.dart` |
| SDD-MOD-002 Connect UI | `apps/chanora_flutter/lib/widgets/connect_widgets.dart` |
| SDD-MOD-003 Snapshot and channel UI | `snapshot_view.dart`, `snapshot_state_mapper.dart`, `channel_spacer.dart` |
| SDD-MOD-004 Chat UI | `chat_views.dart`, `bbcode_text.dart` |
| SDD-MOD-005 Voice UI | `voice_bar.dart`, `voice_compact.dart`, `voice_settings*.dart`, `voice_level_meter.dart`, `ptt_capability_badge.dart` |
| SDD-MOD-006 Platform services | `android_permissions_service.dart`, `ios_permissions_service.dart`, `audio_lifecycle_service.dart`, `back_intent_*`, `link_trust_service.dart` |
| SDD-MOD-007 Bridge API | `crates/chanora_bridge/src/api.rs`, generated Dart/Rust bridge files |
| SDD-MOD-008 Rust core supervisor | `core/chanora_core/src/lib.rs`, `events.rs`, `network_diagnostics.rs`, `ptt.rs` |
| SDD-MOD-009 Protocol adapter | `crates/chanora_protocol/src/` |
| SDD-MOD-010 State sync | `crates/chanora_state/src/lib.rs`, `channel_join.rs` |
| SDD-MOD-011 Audio subsystem | `crates/chanora_audio/src/` |
| SDD-MOD-012 Storage | `crates/chanora_storage/src/lib.rs` |
| SDD-MOD-013 Diagnostics | `crates/chanora_diagnostics/src/lib.rs` |
| SDD-MOD-014 Resolution and prefetch | `crates/chanora_resolver/src/lib.rs`, `crates/chanora_prefetch/src/lib.rs`, `prefetch_debouncer.dart` |
| SDD-MOD-015 Build and release hooks | `.github/workflows/`, `tools/`, platform project files |
@@ -21,7 +21,7 @@
| Protocol adapter | `chanora_protocol``tsclientlib` isolated behind `ProtocolClient`, typed DTOs, `ProtocolError` catalogue |
| Connection lifecycle | `chanora_core` — supervisor task, exponential backoff reconnect (1s→60s), user-disconnect suppresses reconnect; branch `simplify-project-review` has started splitting the previous large `lib.rs` into focused internal Modules (`events.rs`, `network_diagnostics.rs`) while preserving public re-exports |
| State sync reducer unit | `chanora_state``ConnectionState`, `channel_join`, snapshot/delta reducers, reconnect handling, deterministic ordering, malformed duplicate normalization, channel-delete/client cleanup, and reducer unit tests. Runtime core integration still uses snapshot/probe refresh paths and remains separate validation work. |
| Audio subsystem | `chanora_audio` — Opus encode/decode, HPF/NS/AEC3/AGC2 DSP, PTT backends (Windows/macOS/Linux/focused), iOS VoiceProcessingIO, Android Oboe, jitter buffer via `tsclientlib::audio::AudioHandler`, mixer, mute/deaf gates, release-tail timer, and VAD scaffolding/assets. Product `VoiceActivity` remains disabled per DEC-030. |
| Audio subsystem | `chanora_audio` — Opus encode/decode, HPF/NS/AEC3/AGC2 DSP, PTT backends (Windows/macOS/Linux/focused), iOS VoiceProcessingIO, Android Oboe, jitter buffer via `tsclientlib::audio::AudioHandler`, mixer, mute/deaf gates, release-tail timer, and Windows/Linux desktop `VoiceActivity` through the capture VAD path. Mobile, macOS, and unverified-platform `VoiceActivity` remain deferred per DEC-030. |
| Push-to-talk | Per-platform backends: Windows Raw Input + hook fallback, macOS Event Tap, Linux freedesktop portal, focused fallback; `PttCapabilityLevel` (L0L3); missed-key-up watchdog |
| Voice controls UI | `voice_bar`, `voice_compact`, `voice_haptics`, `voice_level_meter`, `voice_platform`, `ptt_capability_badge`, `talk_power_warning` |
| Storage (non-secret) | `chanora_storage``BookmarkRepository` (SQLite/rusqlite bundled, schema v2), ChaCha20-Poly1305 encrypted passwords |
@@ -54,7 +54,7 @@
|---|---|
| Event replay tooling | Reducer tests cover the state-sync contract, but standalone replay-file tooling remains a P1 verification gap. |
| Reducer runtime integration evidence | The standalone reducer is unit-tested, but `chanora_core` still refreshes UI state through snapshot/probe paths rather than folding all live protocol events through `chanora_state::reduce`. |
| Silero VAD | `assets/models/silero_vad.onnx` bundled but DEC-030 defers VAD to P1; `TransmitMode::VoiceActivity` is reserved and disabled in this baseline. |
| Mobile/macOS VoiceActivity | `assets/models/silero_vad.onnx` is bundled and used by the desktop VAD path where runtime evidence supports it; mobile, macOS, and unverified-platform `TransmitMode::VoiceActivity` remain disabled/deferred until a later baseline supplies backend enablement and verification evidence. |
| macOS build | Source-buildable only; no public release artifact is approved. |
| Windows build | Source-buildable only; no public release artifact is approved. |
| iOS build | Source-buildable/unsigned validation only; no TestFlight/App Store release artifact is approved. |
+75
View File
@@ -0,0 +1,75 @@
# Chanora Offline Knowledge Library
**Generated:** 2026-06-13
**Branch:** `docs/offline-knowledge-library-2026-06-13`
**Purpose:** Comprehensive offline reference for the Chanora project, its dependencies, and related ecosystem.
---
## Contents
### Project Analysis
| Document | Description | Status |
|----------|-------------|--------|
| [function-inventory.md](function-inventory.md) | Complete public API inventory for all 10 Rust crates + 56 Dart files. Includes dead code analysis. | Reviewed |
| [coverage-analysis.md](coverage-analysis.md) | Test coverage (312 Rust tests, 221 Dart tests) and documentation coverage gaps. | Reviewed, corrected |
| [doc-quality-analysis.md](doc-quality-analysis.md) | Duplicated content, useless content, and broken references in docs/. | Reviewed, corrected |
| [link-coverage-report.md](link-coverage-report.md) | All internal/external links validated. 2 broken LICENSE links, 5 broken doc-path refs. | Reviewed, corrected |
| [docs-code-mismatch.md](docs-code-mismatch.md) | 17 doc-code mismatches found (2 critical, 4 major, 11 minor). | Reviewed, corrected |
| [docs-out-of-date.md](docs-out-of-date.md) | 12 outdated docs, 8 undocumented recent changes since DV baseline. | Reviewed, corrected |
| [docs-link-not-covered.md](docs-link-not-covered.md) | 2 broken links, 9 missing targets, 18 orphaned docs. | Reviewed, corrected |
### External Projects
| Document | Description | Status |
|----------|-------------|--------|
| [external/teaspeak-overview.md](external/teaspeak-overview.md) | TeaSpeak voice server - architecture, protocol, build system. | Reviewed |
| [external/respeak-overview.md](external/respeak-overview.md) | ReSpeak org - tsclientlib, tsproto, crypto, Chanora integration. | Reviewed |
| [external/yatqa-en.md](external/yatqa-en.md) | yat.qa TeamSpeak admin tool (English). | Reviewed |
| [external/yatqa-de.md](external/yatqa-de.md) | yat.qa TeamSpeak admin tool (German/Deutsch). | Reviewed |
### Review Reports
| Document | Description |
|----------|-------------|
| [reviews/coverage-analysis-review.md](reviews/coverage-analysis-review.md) | Cross-validation of coverage analysis |
| [reviews/doc-quality-review.md](reviews/doc-quality-review.md) | Cross-validation of doc quality analysis |
| [reviews/link-coverage-review.md](reviews/link-coverage-review.md) | Cross-validation of link coverage |
| [reviews/external-docs-review.md](reviews/external-docs-review.md) | Cross-validation of external project docs |
| [reviews/docs-code-mismatch-review.md](reviews/docs-code-mismatch-review.md) | Cross-validation of mismatch analysis |
| [reviews/docs-out-of-date-review.md](reviews/docs-out-of-date-review.md) | Cross-validation of out-of-date analysis |
| [reviews/docs-link-not-covered-review.md](reviews/docs-link-not-covered-review.md) | Cross-validation of link-not-covered analysis |
| [reviews/function-inventory-review.md](reviews/function-inventory-review.md) | Cross-validation of function inventory |
| [reviews/coverage-docquality-review.md](reviews/coverage-docquality-review.md) | Second-pass review of coverage + doc quality |
| [reviews/mismatch-outofdate-review.md](reviews/mismatch-outofdate-review.md) | Second-pass review of mismatch + out-of-date |
| [reviews/link-reports-review.md](reviews/link-reports-review.md) | Second-pass review of link reports |
| [reviews/external-index-review.md](reviews/external-index-review.md) | Second-pass review of external docs + index |
---
## Key Findings Summary
### Test Coverage
- **Rust**: 312 inline tests + 5 integration tests across 8/10 crates
- **Dart**: 221 tests (widgets: 58%, services: 90%)
- **Untested crates**: chanora_bridge, chanora_cache, chanora_prefetch
### Documentation Gaps
- No architecture docs for: audio engine, FFI bridge, protocol layer, state machine, cache, prefetch, diagnostics
- 15 TODO/FIXME items catalogued across codebase
### Dead/Useless Code
- No true dead code found (platform-gated items are intentional)
- 1 malformed markdown in docs/sysdes.md
- 2 missing LICENSE files (LICENSE-APACHE, LICENSE-MIT)
### Duplicated Content
- Lifecycle chain repeated in 6+ files
- Git commit examples in 3 files
- Security doc list in 2 files
### External Dependencies
- **ReSpeak/tsclientlib**: Chanora patches tsproto-types for P-256 coordinate padding
- **TeaSpeak**: Compatible voice server, C++20 + Electron architecture
- **yat.qa**: TeamSpeak admin tool, v3.9.9b, English + German docs
@@ -0,0 +1,375 @@
# Test & Document Coverage Analysis
**Generated:** 2026-06-13
**Scope:** All crates, Flutter app, and docs/ directory
---
## Test Coverage Summary
| Metric | Count |
|--------|-------|
| Total Rust tests (inline `#[test]`) | 312 |
| Total Rust integration tests | 5 |
| Total Dart tests (`test()` + `testWidgets()`) | 221 |
| Crates with tests | 7/9 |
| Dart services with tests | 19/21 (90%) |
| Dart widgets with tests | 14/24 (58%) |
| Overall estimated coverage | ~65% |
---
## Per-Crate Test Coverage (Rust)
### chanora_audio — 333 tests, ~48% function coverage
| Source File | Functions | Tests | Coverage |
|-------------|-----------|-------|----------|
| engine.rs | ~45 | 7 | ~16% |
| ptt_backends/windows.rs | ~80 | 44 | ~55% |
| ptt_backends/windows_keymap.rs | ~30 | 12 | ~40% |
| ptt_backends/macos.rs | ~35 | 12 | ~34% |
| ptt_backends/linux.rs | ~25 | 10 | ~40% |
| ptt_backends/mod.rs | ~15 | 1 | ~7% |
| transmit_selector.rs | ~20 | 10 | ~50% |
| voice_activity.rs | ~15 | 9 | ~60% |
| voice_render.rs | ~15 | 9 | ~60% |
| mobile_voice_backend.rs | ~20 | 9 | ~45% |
| processor/sonora.rs | ~15 | 8 | ~53% |
| processor/dsp/agc2.rs | ~10 | 6 | ~60% |
| mode_stack.rs | ~10 | 6 | ~60% |
| opus_voice.rs | ~10 | 5 | ~50% |
| capture_accumulator.rs | ~8 | 5 | ~63% |
| processor/dsp/ns.rs | ~8 | 4 | ~50% |
| ptt.rs | ~8 | 4 | ~50% |
| vad/mod.rs | ~6 | 4 | ~67% |
| audio_processing.rs | ~8 | 3 | ~38% |
| debug_wav.rs | ~6 | 3 | ~50% |
| processor/dsp/hpf.rs | ~5 | 3 | ~60% |
| processor/dsp/aec3.rs | ~5 | 3 | ~60% |
| transmit_mode.rs | ~4 | 3 | ~75% |
| android_render_ring.rs | ~5 | 3 | ~60% |
| vad/resampler.rs | ~4 | 3 | ~75% |
| vad/apple_coreml.rs | ~5 | 3 | ~60% |
| vad/silero_onnx.rs | ~10 | 6 | ~60% |
| render_reference.rs | ~8 | 7 | ~88% |
| capture_resampler.rs | ~3 | 2 | ~67% |
| audio_event_queue.rs | ~4 | 2 | ~50% |
| processor/webrtc_apm.rs | ~5 | 2 | ~40% |
| frame.rs | ~3 | 1 | ~33% |
| lib.rs (defaults) | ~5 | 1 | ~20% |
| release_tail.rs | ~4 | 1 | ~25% |
| route_policy.rs | ~8 | 8 | ~100% |
| **Integration: tests/ptt_privacy.rs** | — | 1 | — |
| **Integration: tests/linux_portal_smoke.rs** | — | 1 | — |
### chanora_protocol — 17 tests, ~18% function coverage
| Source File | Functions | Tests | Coverage |
|-------------|-----------|-------|----------|
| adapter.rs | ~80 | 13 | ~16% |
| poke_limiter.rs | ~17 | 4 | ~24% |
| dto.rs | ~0 | 0 | — |
| lib.rs | ~0 | 0 | — |
**Untested areas:** Message parsing, serialization, most adapter methods
### chanora_state — 27 tests, ~46% function coverage
| Source File | Functions | Tests | Coverage |
|-------------|-----------|-------|----------|
| lib.rs | ~35 | 18 | ~51% |
| channel_join.rs | ~24 | 9 | ~38% |
### chanora_storage — 15 tests, ~24% function coverage
| Source File | Functions | Tests | Coverage |
|-------------|-----------|-------|----------|
| lib.rs | ~62 | 15 | ~24% |
**Untested areas:** Migration logic, concurrent access patterns, error recovery
### chanora_resolver — 12 tests, ~17% function coverage
| Source File | Functions | Tests | Coverage |
|-------------|-----------|-------|----------|
| lib.rs | ~70 | 12 | ~17% |
| examples/cli.rs | — | 1 | — |
**Untested areas:** DNS failure modes, timeout handling, cache behavior
### chanora_diagnostics — 19 tests, ~26% function coverage
| Source File | Functions | Tests | Coverage |
|-------------|-----------|-------|----------|
| lib.rs | ~74 | 19 | ~26% |
### chanora_core — 38 tests, ~7% function coverage
| Source File | Functions | Tests | Coverage |
|-------------|-----------|-------|----------|
| lib.rs | ~140 | 10 | ~7% |
| network_diagnostics.rs | ~7 | 1 | ~14% |
**Untested areas:** Connection lifecycle, server event handling, most state transitions
### chanora_bridge — 0 tests, 0% function coverage
| Source File | Functions | Tests | Coverage |
|-------------|-----------|-------|----------|
| api.rs | ~200+ | 0 | 0% |
| frb_generated.rs | ~100+ | 0 | 0% |
| permission_jni.rs | ~10 | 0 | 0% |
| android_init.rs | ~5 | 0 | 0% |
| lib.rs | ~4 | 0 | 0% |
**Note:** chanora_bridge is an FFI/bridge layer; testing requires integration with Flutter.
### chanora_cache — 0 tests
| Source File | Functions | Tests | Coverage |
|-------------|-----------|-------|----------|
| lib.rs | ~16 | 0 | 0% |
### chanora_prefetch — 0 tests
| Source File | Functions | Tests | Coverage |
|-------------|-----------|-------|----------|
| lib.rs | ~18 | 0 | 0% |
---
## Per-Module Test Coverage (Dart/Flutter)
### Services — 155 tests across 19 test files
| Source File | Test File | Tests | Coverage |
|-------------|-----------|-------|----------|
| android_audio_output_devices.dart | ✅ android_audio_output_devices_test.dart | 2 | Tested |
| android_permissions_service.dart | ✅ android_permissions_service_test.dart | 14 | Tested |
| app_bootstrap.dart | ✅ app_bootstrap_test.dart | 4 | Tested |
| audio_lifecycle_service.dart | ✅ audio_lifecycle_service_test.dart | 4 | Tested |
| back_intent_policy.dart | ✅ back_intent_policy_test.dart | 9 | Tested |
| back_intent_service.dart | ✅ back_intent_service_test.dart | 5 | Tested |
| channel_join_error_mapper.dart | ✅ channel_join_error_mapper_test.dart | Tested |
| channel_spacer.dart | ✅ channel_spacer_test.dart | 9 | Tested |
| connection_phase_state.dart | ✅ connection_phase_state_test.dart | 8 | Tested |
| hard_mute_owners.dart | ✅ hard_mute_owners_test.dart | 3 | Tested |
| ios_audio_session_controller.dart | ✅ ios_audio_session_controller_test.dart | 6 | Tested |
| macos_permissions_service.dart | ✅ macos_permissions_service_test.dart | 20 | Tested |
| poke_active_chat.dart | ✅ poke_active_chat_test.dart | 2 | Tested |
| poke_notification_service.dart | ✅ poke_notification_service_test.dart | 2 | Tested |
| poke_preferences_service.dart | ✅ poke_preferences_service_test.dart | 4 | Tested |
| prefetch_debouncer.dart | ✅ prefetch_debouncer_test.dart | 3 | Tested |
| snapshot_state_mapper.dart | ✅ snapshot_state_mapper_test.dart | 7 | Tested |
| ts3_server_link.dart | ✅ ts3_server_link_test.dart | 3 | Tested |
| ui_preferences_service.dart | ✅ ui_preferences_service_test.dart | 5 | Tested |
| voice_join_ordering.dart | ✅ voice_join_ordering_test.dart | 4 | Tested |
| **ios_permissions_service.dart** | ❌ No test file | 0 | **UNTESTED** |
| **link_trust_service.dart** | ❌ No test file | 0 | **UNTESTED** |
### Widgets — 66 tests across 13 test files
| Source File | Test File | Tests | Coverage |
|-------------|-----------|-------|----------|
| app_snack_bar.dart | ✅ app_snack_bar_test.dart | 1 | Tested |
| audio_processing_config_state.dart | ✅ audio_processing_config_state_test.dart | 8 | Tested |
| bbcode_text.dart | ✅ bbcode_text_test.dart | Tested |
| chat_panel.dart | ✅ chat_panel_test.dart | Tested |
| chat_views.dart | ✅ chat_views_test.dart | 29 | Tested |
| client_info_sheet.dart | ✅ client_info_sheet_test.dart | Tested |
| poke_notification_settings.dart | ✅ poke_notification_settings_test.dart | Tested |
| snapshot_view.dart | ✅ snapshot_view_test.dart | Tested |
| talk_power_warning.dart | ✅ talk_power_warning_test.dart | 1 | Tested |
| voice_compact.dart | ✅ voice_compact_test.dart | Tested |
| voice_settings_controls.dart | ✅ voice_settings_controls_test.dart | 6 | Tested |
| voice_status_summary.dart | ✅ voice_status_summary_test.dart | 5 | Tested |
| mobile_ui_resilience.dart | ✅ mobile_ui_resilience_test.dart | Tested |
| **audio_debug_stats_panel.dart** | ❌ No test file | 0 | **UNTESTED** |
| **audio_device_list_tile.dart** | ✅ audio_device_list_tile_test.dart | 3 | Tested |
| **audio_output_tile.dart** | ❌ No test file | 0 | **UNTESTED** |
| **connect_widgets.dart** | ❌ No test file | 0 | **UNTESTED** |
| **input_dialogs.dart** | ❌ No test file | 0 | **UNTESTED** |
| **permission_state_banner.dart** | ❌ No test file | 0 | **UNTESTED** |
| **ptt_capability_badge.dart** | ❌ No test file | 0 | **UNTESTED** |
| **voice_bar.dart** | ❌ No test file | 0 | **UNTESTED** |
| **voice_haptics.dart** | ❌ No test file | 0 | **UNTESTED** |
| **voice_level_meter.dart** | ❌ No test file | 0 | **UNTESTED** |
| **voice_platform.dart** | ❌ No test file | 0 | **UNTESTED** |
| **voice_settings.dart** | ❌ No test file | 0 | **UNTESTED** |
### E2E Tests
| File | Tests | Notes |
|------|-------|-------|
| alpha_e2e_test.dart | 1 | End-to-end integration |
| beta_e2e_test.dart | 1 | End-to-end integration |
| widget_test.dart | — | Default Flutter template |
---
## Document Coverage Summary
| Metric | Count |
|--------|-------|
| Total doc files (under docs/) | 66 |
| Modules documented | ~15 areas |
| Estimated outdated docs | 3-5 |
## Document Inventory
### Architecture (4 files)
| File | Topic | Status | Notes |
|------|-------|--------|-------|
| architecture/sad.md | Software Architecture Document | Current | 190 lines, references SDD |
| architecture/sdd.md | Software Design Document | Current | 161 lines |
| architecture/sysdes.md | System Design overview | Current | 21 lines, brief |
| architecture/desktop-ptt-architecture.md | Desktop PTT subsystem design | Current | 43 lines |
| architecture/file-transfer-design.md | File transfer feature design | Current | 737 lines |
| architecture/file-transfer-research.md | File transfer research | Current | 770 lines |
| architecture/file-transfer-implementation-plan.md | File transfer implementation plan | Current | 1315 lines |
### Requirements (2 files + 2 symlinks)
| File | Topic | Status | Notes |
|------|-------|--------|-------|
| requirements/srs.md | Software Requirements Spec | Current | 22 lines (pointer) |
| requirements/sysrs.md | System Requirements Spec | Current | 22 lines (pointer) |
| srs.md | SRS (full) | Current | 2850 lines |
| sysrs.md | SysRS (full) | Current | 2014 lines |
### Verification (5 files)
| File | Topic | Status | Notes |
|------|-------|--------|-------|
| verification/verification-master-plan.md | Overall V&V plan | Current | 91 lines |
| verification/swe4-unit-verification-plan.md | Unit test plan | Current | 60 lines |
| verification/swe5-software-integration-verification-plan.md | Integration test plan | Current | 71 lines |
| verification/swe6-software-verification-plan.md | System verification plan | Current | 62 lines |
| verification/sys4-system-integration-verification-plan.md | System integration plan | Current | 58 lines |
### Security (7 files)
| File | Topic | Status | Notes |
|------|-------|--------|-------|
| security/threat-model.md | Threat model | Current | 33 lines |
| security/license-inventory.md | Rust license inventory | Current | 10970 lines |
| security/flutter-license-inventory.md | Flutter license inventory | Current | 5477 lines |
| security/diagnostic-redaction-audit-report.md | Diagnostic redaction audit | Current | 31 lines |
| security/secure-storage-audit-report.md | Secure storage audit | Current | 31 lines |
| security/dependency-and-supply-chain-report.md | Dependency audit | Current | 43 lines |
| security/security-privacy-legal-guideline.md | Security/privacy guidelines | Current | 63 lines |
### Governance (11 files)
| File | Topic | Status | Notes |
|------|-------|--------|-------|
| governance/document-index.md | Document catalog | Current | 36 lines |
| governance/document-naming-convention.md | Naming conventions | Current | 33 lines |
| governance/document-review-report.md | Review report | Current | 37 lines |
| governance/traceability-matrix.md | Requirements traceability | Current | 73 lines |
| governance/product-decision-register.md | Decision log | Current | 25 lines |
| governance/decision-impact-assessment.md | Impact assessment | Current | 18 lines |
| governance/git-commit-message-convention.md | Commit conventions | Current | 21 lines |
| governance/path-migration-map.md | Path migration plan | Current | 18 lines |
| governance/repo-format-validation-report.md | Format validation | Current | 22 lines |
| governance/baseline-candidate-validation-report.md | Baseline validation | Current | 38 lines |
| governance/baseline-approval-record.md | Baseline approval | Current | 32 lines |
| governance/maintainability-review-2026-06-08.md | Maintainability review | Current | 99 lines |
### Release (4 files)
| File | Topic | Status | Notes |
|------|-------|--------|-------|
| release/ios-build.md | iOS build instructions | Current | 47 lines |
| release/platform-release-policy.md | Release policy | Current | 26 lines |
| release/release-readiness-go-nogo-record.md | Go/no-go record | Current | 105 lines |
| release/dv-waiver-register.md | DV waiver register | Current | 36 lines |
### UI/UX (4 files)
| File | Topic | Status | Notes |
|------|-------|--------|-------|
| ui-ux/material3-guideline.md | Material 3 guidelines | Current | 8 lines (brief) |
| ui-ux/material3-design-tokens.md | Design tokens | Current | 21 lines |
| ui-ux/material3-component-catalog.md | Component catalog | Current | 38 lines |
| ui-ux/adaptive-layout-platform-guide.md | Adaptive layout guide | Current | 27 lines |
### Other
| File | Topic | Status | Notes |
|------|-------|--------|-------|
| privacy/privacy-policy.md | Privacy policy | Current | 51 lines |
| references/external-references.md | External references | Current | 21 lines |
| references/aspice-swe2-swe3-integration-note.md | ASPICE integration note | Current | 28 lines |
| legal/trademark-and-attribution-review.md | Trademark review | Current | 47 lines |
| i18n/localization-architecture.md | Localization architecture | Current | 44 lines |
| implementation-status-2026-05-28.md | Implementation status | **Possibly outdated** | Date is 2026-05-28 |
| material3-guideline.md | Material 3 guideline (duplicate) | Current | 93 lines |
### Superpowers Plans & Specs (13 files)
| File | Topic | Status |
|------|-------|--------|
| superpowers/plans/2026-05-28-server-resolution-prefetch.md | Server resolution prefetch plan | Current |
| superpowers/plans/2026-05-28-chanora-server-prefetch-crate.md | Prefetch crate plan | Current |
| superpowers/plans/2026-05-29-dv-evidence-pack.md | DV evidence pack plan | Current |
| superpowers/plans/2026-05-29-state-sync-ui-settings-validation.md | State sync validation plan | Current |
| superpowers/plans/2026-05-29-swe2-swe3-baselines.md | SWE2/SWE3 baselines plan | Current |
| superpowers/plans/2026-05-29-finish-dv-document-tree.md | DV document tree plan | Current |
| superpowers/plans/2026-06-06-chat-panel-switching.md | Chat panel switching plan | Current |
| superpowers/plans/2026-06-08-maintainability-continuation.md | Maintainability continuation | Current |
| superpowers/plans/2026-06-08-core-internal-split.md | Core internal split plan | Current |
| superpowers/specs/2026-05-28-server-resolution-prefetch-design.md | Prefetch design spec | Current |
| superpowers/specs/2026-05-28-chanora-server-prefetch-crate-design.md | Prefetch crate design | Current |
| superpowers/specs/2026-05-29-state-sync-ui-settings-validation-design.md | State sync design | Current |
| superpowers/specs/2026-06-05-adaptive-3-panel-layout-design.md | Adaptive layout design | Current |
| superpowers/specs/2026-06-08-maintainability-continuation-design.md | Maintainability design | Current |
| superpowers/specs/2026-06-09-poke-without-message-design.md | Poke without message design | Current |
---
## Documentation Gaps
The following code modules have **no dedicated documentation**:
| Module | Functions | Gap Description |
|--------|-----------|-----------------|
| `chanora_audio` (engine) | ~45 | No architecture doc for audio engine internals |
| `chanora_audio` (VAD subsystem) | ~25 | VAD pipeline, model loading, fallback strategy undocumented |
| `chanora_audio` (DSP processors) | ~30 | AEC3, AGC2, NS, HPF configuration undocumented |
| `chanora_audio` (PTT backends) | ~155 | Platform-specific PTT behavior undocumented |
| `chanora_bridge` (FFI layer) | ~300 | Flutter-Rust bridge API contract undocumented |
| `chanora_cache` | ~16 | Cache strategy, eviction policy undocumented |
| `chanora_prefetch` | ~18 | Prefetch timing, debouncing strategy undocumented |
| `chanora_core` | ~147 | Core connection lifecycle, event handling undocumented |
| `chanora_state` | ~59 | State machine transitions, delta emission undocumented |
| `chanora_storage` | ~62 | Storage format, migration strategy undocumented |
| `chanora_protocol` | ~97 | Protocol message format, adapter logic undocumented |
| `chanora_resolver` | ~70 | DNS resolution, TSDNS discovery undocumented |
| `chanora_diagnostics` | ~74 | Diagnostic collection, redaction rules undocumented |
| Flutter services layer | ~21 files | No service-layer architecture doc |
| Flutter widgets layer | ~24 files | No widget catalog or component doc |
| Localization (l10n) | — | Translation workflow undocumented (only architecture doc exists) |
## Potentially Outdated Documents
| File | Reason |
|------|--------|
| `implementation-status-2026-05-28.md` | Dated 2026-05-28; code has changed significantly since |
| `docs/material3-guideline.md` | Duplicate of `docs/ui-ux/material3-guideline.md` |
| `docs/sysdes.md` | Top-level duplicate of `docs/architecture/sysdes.md` |
| `docs/srs.md` / `docs/sysrs.md` | Top-level duplicates of `docs/requirements/` versions |
---
## Key Findings
1. **chanora_audio** is the best-tested crate (204 tests), but still only ~48% function coverage due to the large codebase (~428 functions)
2. **chanora_bridge**, **chanora_cache**, and **chanora_prefetch** have zero tests
3. **chanora_core** has very low coverage (~7%) despite being the main connection orchestrator
4. **Dart widget tests** cover only 54% of widget files; 11 widget files have no tests
5. **Dart service tests** are strong at 90% coverage (only 2 files untested)
6. **Documentation** is extensive (55 files) but focuses on process/governance; code-level architecture docs are sparse
7. No dedicated docs exist for the audio engine, FFI bridge, protocol layer, or state machine internals
@@ -0,0 +1,123 @@
# Documentation Quality Analysis
## Summary
- Total docs analyzed: 64
- Duplicated content instances: 8
- Path record files (DV navigation aids): 4
- Genuine issues (malformed markdown): 1
- Broken references: 1 (suggested file names only; SDD-109/SAD-043 are valid historical refs)
## Duplicated Content
### Instance 1: Lifecycle Documentation Chain
- **Files**: `README.md:280`, `CONTRIBUTING.md:10`, `docs/sysdes.md:90`, `docs/sysrs.md:108`, `docs/governance/traceability-matrix.md:16`, `docs/references/aspice-swe2-swe3-integration-note.md:12`
- **Content**: `SysRS -> SysDes -> SRS -> SAD -> SDD` lifecycle chain repeated across 6+ files
- **Recommendation**: Define once in `README.md` and reference from other docs
### Instance 2: Git Commit Convention Examples
- **Files**: `README.md:380`, `CONTRIBUTING.md:38`, `docs/governance/git-commit-message-convention.md:15`
- **Content**: Same commit examples (`feat(voice): add push-to-talk state handling`, `fix(protocol): recover channel tree after reconnect snapshot`, etc.) duplicated across 3 files
- **Recommendation**: Keep examples only in `docs/governance/git-commit-message-convention.md` and reference from README/CONTRIBUTING
### Instance 3: Security/Privacy/Legal Document List
- **Files**: `README.md:349-355`, `SECURITY.md:33-38`
- **Content**: Same list of 6 security documents (threat-model, secure-storage, diagnostic-redaction, dependency, privacy-policy, trademark) repeated verbatim
- **Recommendation**: Keep list in `SECURITY.md` and reference from README
### Instance 4: Architecture Component Table
- **Files**: `README.md:73-92`, `docs/architecture/sad.md:56-67`
- **Content**: Similar architecture overview showing Flutter UI, Rust Core, Protocol Layer structure
- **Recommendation**: Keep detailed version in SAD; use abbreviated version in README
### Instance 5: Platform Policy Table
- **Files**: `README.md:47-54`, `docs/release/platform-release-policy.md:12-19`
- **Content**: Platform requirements table with overlapping information
- **Recommendation**: Consolidate in `platform-release-policy.md` and reference from README
### Instance 6: Security Gate Requirements
- **Files**: `docs/security/security-privacy-legal-guideline.md:13-21`, `docs/security/threat-model.md:22-30`
- **Content**: Similar threat/mitigation tables with overlapping secure-storage and diagnostics concerns
- **Recommendation**: Threat model should reference the guideline for gate requirements
### Instance 7: DV Conclusion Pattern
- **Files**: Nearly every `docs/` file ends with a "## DV Conclusion" section
- **Content**: Repetitive pattern: "[Area] is documented for DV. [Limitation] remains."
- **Recommendation**: This is intentional for ASPICE compliance. No change needed, but consider a template.
### Instance 8: Android Runtime Gate Documentation
- **Files**: `docs/verification/swe5-software-integration-verification-plan.md:57-68`, `docs/governance/maintainability-review-2026-06-08.md:61-88`
- **Content**: Same Android ADB/emulator verification steps and `adb devices -l` requirements
- **Recommendation**: Define once in a shared reference and import
## Path Record Files (DV Navigation Aids)
These files are intentional ASPICE DV entry-point records with reviewer navigation tables. They are NOT useless — they serve a specific compliance purpose. Listed here for awareness only.
### DV Navigation Aids
| File | Line | Header | Issue |
|------|------|--------|-------|
| `docs/architecture/sysdes.md` | 1-21 | Entire file | Path record — points to `docs/sysdes.md` for DV reviewer navigation |
| `docs/requirements/sysrs.md` | 1-22 | Entire file | Path record — points to `docs/sysrs.md` for DV reviewer navigation |
| `docs/requirements/srs.md` | 1-22 | Entire file | Path record — points to `docs/srs.md` for DV reviewer navigation |
| `docs/ui-ux/material3-guideline.md` | 1-8 | Entire file | Path record — points to `docs/material3-guideline.md` for DV reviewer navigation |
### Genuine Issues
| File | Line | Header | Issue |
|------|------|--------|-------|
| `docs/sysdes.md` | 13 | `**Repo path:** ... ---` | Malformed markdown (missing blank line before `---`) |
### TODO/Placeholder Markers
No actual TODO/TBD/placeholder markers found in the documentation files. The codebase is clean of such markers.
### Broken References
| File | Line | Reference | Issue |
|------|------|-----------|-------|
| `docs/sysrs.md` | 126-130 | `docs/chanora_SysDes.md`, `docs/chanora_SRS.md`, etc. | These suggested file names do not exist. Actual files use different names (`docs/sysdes.md`, `docs/srs.md`, etc.) |
| `docs/implementation-status-2026-05-28.md` | 103 | `SDD-109` | References a specific SDD item ID that is not itemized in the current SDD baseline |
| `docs/implementation-status-2026-05-28.md` | 105 | `SAD-043` | References a specific SAD item ID that is not itemized in the current SAD baseline |
### Outdated Content
| File | Line | Content | Issue |
|------|------|---------|-------|
| `docs/sysdes.md` | 6 | Version `0.9.8` | Superseded by later governance docs dated 2026-05-29 |
| `docs/sysrs.md` | 5 | Version `0.9.11` | May need alignment with SysDes version |
| `docs/material3-guideline.md` | 4-5 | Version `0.9.2` | Change history stops at 2026-05-14; no updates for 2026-05-29 baseline |
| `tools/windows-smoke.md` | 6 | `product/scaffold-v0` branch | Default base branch changed to `main` per CHANGELOG |
| `docs/implementation-status-2026-05-28.md` | 140 | Agent spec docs reference | States docs are "deleted from the working tree but still in git HEAD" — stale cleanup note |
### Stale Content
| File | Line | Content | Issue |
|------|------|---------|-------|
| `docs/implementation-status-2026-05-28.md` | 1 | Date: 2026-05-28 | Pre-dates DV baseline (2026-05-29); may not reflect final baseline state |
| `docs/governance/git-commit-message-convention.md` | 18 | `release(android): prepare internal alpha build metadata` | Example uses `release` type which is not in the Conventional Commits standard types |
## Duplicated Code Blocks
| Code Hash | Files | Description |
|-----------|-------|-------------|
| Lifecycle chain | `README.md:280`, `CONTRIBUTING.md:10`, `docs/sysdes.md:90`, `docs/sysrs.md:108`, `docs/governance/traceability-matrix.md:16`, `docs/references/aspice-swe2-swe3-integration-note.md:12` | `SysRS -> SysDes -> SRS -> SAD -> SDD -> Verification` |
| Commit examples | `README.md:379-386`, `CONTRIBUTING.md:37-42`, `docs/governance/git-commit-message-convention.md:14-19` | Overlapping commit message examples (different subsets in each file) |
| Security doc list | `README.md:349-355`, `SECURITY.md:33-38` | 6 identical file paths |
| Architecture ASCII art | `README.md:73-92`, `docs/architecture/sad.md:56-67` | Similar but not identical architecture diagrams |
| Platform table | `README.md:47-54`, `docs/release/platform-release-policy.md:12-19` | Overlapping platform requirement tables |
## Recommendations
### High Priority
1. **Consolidate lifecycle chain**: Define once in README, reference elsewhere
2. **Fix suggested file names**: `docs/sysrs.md` lines 126-130 reference non-existent file names
### Medium Priority
4. **Consolidate commit examples**: Keep in `git-commit-message-convention.md` only
5. **Consolidate security doc list**: Keep in `SECURITY.md` only
6. **Update outdated branch reference**: `tools/windows-smoke.md` references `product/scaffold-v0` but default is now `main`
### Low Priority
7. **Align document versions**: SysDes (0.9.8), SysRS (0.9.11), Material3 (0.9.2) have different versions
8. **Clean up implementation status**: Remove stale agent-spec references and update date
@@ -0,0 +1,458 @@
# Documentation-Code Mismatch Analysis
**Generated:** 2026-06-13
## Summary
- Total claims verified: ~150
- Mismatches found: 17
- Critical: 2 | Major: 4 | Minor: 11
## Critical Mismatches (wrong API / broken reference)
### 1. [README.md:428-431] - LICENSE files referenced but do not exist
- **Doc claims:** Links to `LICENSE-APACHE` and `LICENSE-MIT` at repository root
- **Code shows:** Neither `LICENSE-APACHE` nor `LICENSE-MIT` exists at `/Users/edison/dev/chanora/`
- **Impact:** Users clicking license links in README get 404 on GitHub. Dual-license model (DEC-020) requires these files for proper attribution. Also affects `docs/security/license-inventory.md:9-10` and `docs/security/flutter-license-inventory.md:11-12`.
### 2. [README.md:236-249] - Repository layout missing 3 crates
- **Doc claims:** Lists 7 crates: `chanora_protocol`, `chanora_audio`, `chanora_state`, `chanora_storage`, `chanora_diagnostics`, `chanora_bridge` plus `core/chanora_core`
- **Code shows:** Actual workspace has 10 crates: adds `chanora_resolver`, `chanora_prefetch`, `chanora_cache` (all present in `Cargo.toml` workspace members and `crates/` directory)
- **Impact:** Developers reading README cannot discover 3 existing crates. Resolver, prefetch, and cache functionality is undocumented in the primary entry point.
## Major Mismatches (wrong behavior / wrong structure)
### 3. [docs/architecture/sad.md:39-52] - SAD component table missing chanora_cache
- **Doc claims:** Component table lists 12 components (Flutter app shell through Server prefetch)
- **Code shows:** `chanora_cache` crate exists in workspace (`Cargo.toml:34`) and `crates/chanora_cache/` but is not listed in SAD component architecture
- **Impact:** Architecture description incomplete; cache layer is invisible to DV reviewers
### 4. [docs/architecture/sdd.md:19] - snapshot_state_mapper.dart listed under wrong component
- **Doc claims:** `SDD-MOD-003 Snapshot and channel UI` lists `snapshot_state_mapper.dart` as a widget-layer file
- **Code shows:** `snapshot_state_mapper.dart` is in `apps/chanora_flutter/lib/services/`, not `apps/chanora_flutter/lib/widgets/`
- **Impact:** Minor categorization issue — SDD header says "widget/service layer" but the module table groups it under widgets. Also affects `channel_spacer.dart` (same row).
### 5. [tools/windows-smoke.md:6] - Branch reference outdated
- **Doc claims:** Script designed for `product/scaffold-v0` branch
- **Code shows:** Default base branch is `main` per CHANGELOG v0.3.0 line 99
- **Impact:** Windows smoke procedure references obsolete branch name
### 6. [docs/sysrs.md:126-130] - Suggested downstream file names do not exist
- **Doc claims:** Lists potential downstream file names: `docs/chanora_SysDes.md`, `docs/chanora_SRS.md`, `docs/chanora_SAD.md`, `docs/chanora_SDD.md`, `docs/chanora_Verification.md`
- **Code shows:** Actual files use different names: `docs/sysdes.md`, `docs/srs.md`, `docs/architecture/sad.md`, `docs/architecture/sdd.md`, `docs/verification/verification-master-plan.md`
- **Impact:** Aspirational/historical names mislead readers about actual file locations
### 7. [docs/governance/product-decision-register.md:18] - DEC-030 VoiceActivity scope partially superseded
- **Doc claims:** DEC-030 is "Partially superseded by desktop enablement"
- **Code shows:** `voice_activity.rs` exists with `VoiceActivityStateMachine`; `transmit_mode.rs` has `TransmitMode::VoiceActivity`; VAD backends exist in `vad/` directory. Windows/Linux desktop VAD is implemented via capture path.
- **Impact:** Decision register does not fully reflect current implementation state; desktop VAD is more complete than "partially superseded" suggests
## Minor Mismatches (cosmetic / slight drift)
### 8. [README.md:17] - Status description slightly outdated
- **Doc claims:** "Chanora is currently a baseline-candidate Flutter + Rust workspace"
- **Code shows:** Workspace version is `0.2.0-beta.1`, Flutter app is `0.3.0+100`; project has working voice, chat, bookmarks, diagnostics
- **Impact:** "baseline-candidate" undersells current implementation maturity
### 9. [docs/material3-guideline.md:10] - Self-referencing path record
- **Doc claims:** `**Repo path:** docs/ui-ux/material3-guideline.md`
- **Code shows:** This file IS at `docs/material3-guideline.md`, not `docs/ui-ux/material3-guideline.md`
- **Impact:** Path record creates circular reference confusion
### 10. [docs/implementation-status-2026-05-28.md:1] - Status date pre-dates DV baseline
- **Doc claims:** Date 2026-05-28
- **Code shows:** DV baseline documents are dated 2026-05-29; code has changed significantly since
- **Impact:** Implementation status may not reflect final baseline state
### 11. [docs/implementation-status-2026-05-28.md:103,105] - References to non-itemized SDD/SAD IDs
- **Doc claims:** References `SDD-109` and `SAD-043`
- **Code shows:** Current SAD/SDD baselines do not use itemized ID numbering
- **Impact:** Historical references cannot be traced in current baseline
### 12. [docs/governance/git-commit-message-convention.md:18] - Non-standard commit type
- **Doc claims:** Example uses `release(android): prepare internal alpha build metadata`
- **Code shows:** `release` is not a standard Conventional Commits type
- **Impact:** Minor convention inconsistency
### 13. [docs/ui-ux/material3-guideline.md:4-5] - Version history stops at 0.9.2
- **Doc claims:** Version 0.9.2, last updated 2026-05-14
- **Code shows:** DV baseline documents dated 2026-05-29; no update for baseline
- **Impact:** Material 3 guideline may not reflect latest baseline decisions
### 14. [docs/sysdes.md:6] - SysDes version older than SysRS
- **Doc claims:** SysDes version 0.9.8
- **Code shows:** SysRS version 0.9.11
- **Impact:** Version numbering inconsistency between related documents
### 15. [docs/offline-knowledge/README.md:54] - Claims 2 missing LICENSE files
- **Doc claims:** "2 missing LICENSE files (LICENSE-APACHE, LICENSE-MIT)"
- **Code shows:** Confirmed - files do not exist at repo root
- **Impact:** Consistent finding, but offline-knowledge doc correctly identifies the issue
### 16. [docs/security/dependency-and-supply-chain-report.md:35] - License inventory location uncertainty
- **Doc claims:** `docs/security/license-inventory.md` and Flutter inventory referenced by CI
- **Code shows:** Both files exist at `docs/security/license-inventory.md` and `docs/security/flutter-license-inventory.md`
- **Impact:** Report expresses uncertainty but files actually exist
### 17. [docs/architecture/file-transfer-design.md:6] - References SAD-067 which is not itemized
- **Doc claims:** "Direct upstream source: docs/architecture/sad.md (SAD-067, SDD-MOD-009)"
- **Code shows:** Current SAD baseline does not use itemized SAD-XXX numbering
- **Impact:** Historical reference cannot be traced
## Per-File Verification Results
### README.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 3 | Cross-platform voice client for TeamSpeak-compatible servers | ✅ PASS | Matches project description |
| 8 | Flutter UI + Rust Core + tsclientlib | ✅ PASS | Architecture confirmed |
| 17 | Baseline-candidate Flutter + Rust workspace | ⚠️ MINOR | Undersells current maturity |
| 47-54 | Platform policy table | ✅ PASS | Matches `docs/release/platform-release-policy.md` |
| 57-65 | silero-coreml sibling package | ✅ PASS | Confirmed in workspace layout |
| 73-92 | Architecture overview diagram | ✅ PASS | Matches SAD component structure |
| 110-123 | MVP Direction table | ✅ PASS | Matches implementation status |
| 127-166 | Desktop PTT section | ✅ PASS | Matches `docs/architecture/desktop-ptt-architecture.md` |
| 171-231 | Repository Layout (docs/) | ✅ PASS | All listed paths exist |
| 236-249 | Repository Layout (implementation) | ❌ FAIL | Missing 3 crates: resolver, prefetch, cache |
| 260-271 | Documentation Entry Points | ✅ PASS | All listed paths exist |
| 349-355 | Security/Privacy/Legal Gates | ✅ PASS | All listed paths exist |
| 400-406 | Development commands | ✅ PASS | Standard Flutter/Cargo commands |
| 425-436 | License section | ❌ FAIL | LICENSE-APACHE and LICENSE-MIT do not exist |
### docs/architecture/sad.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 17 | Rust owns connection orchestration, protocol isolation, audio processing, storage coordination, diagnostics, server resolution, prefetch policy, and bridge DTOs | ✅ PASS | Matches crate responsibilities |
| 39-52 | Component architecture table | ⚠️ MAJOR | Missing chanora_cache |
| 56-67 | Static architecture view | ✅ PASS | Matches actual dependency flow |
| 75-107 | Runtime flow diagrams | ✅ PASS | Connect, voice, diagnostics flows match |
| 119-130 | Interface catalogue | ✅ PASS | Matches bridge/protocol boundaries |
### docs/architecture/sdd.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 15-31 | Module catalogue | ⚠️ MAJOR | snapshot_state_mapper.dart misclassified |
| 17 | SDD-MOD-001: `app_bootstrap.dart`, `main.dart` | ✅ PASS | Files exist in services/ and root |
| 18 | SDD-MOD-002: `connect_widgets.dart` | ✅ PASS | File exists in widgets/ |
| 19 | SDD-MOD-003: `snapshot_view.dart`, `snapshot_state_mapper.dart`, `channel_spacer.dart` | ⚠️ MAJOR | snapshot_state_mapper.dart is in services/ not widgets/ |
| 20 | SDD-MOD-004: `chat_views.dart`, `bbcode_text.dart` | ✅ PASS | Files exist in widgets/ |
| 21 | SDD-MOD-005: `voice_bar.dart`, `voice_compact.dart`, `voice_settings*.dart`, `voice_level_meter.dart`, `ptt_capability_badge.dart` | ✅ PASS | All files exist in widgets/ |
| 22 | SDD-MOD-006: `android_permissions_service.dart`, `ios_permissions_service.dart`, `audio_lifecycle_service.dart`, `back_intent_*`, `link_trust_service.dart` | ✅ PASS | All files exist in services/ |
| 23 | SDD-MOD-007: `crates/chanora_bridge/src/api.rs` | ✅ PASS | File exists |
| 24 | SDD-MOD-008: `core/chanora_core/src/lib.rs`, `events.rs`, `network_diagnostics.rs`, `ptt.rs` | ✅ PASS | All files exist |
| 25 | SDD-MOD-009: `crates/chanora_protocol/src/` | ✅ PASS | Directory exists |
| 26 | SDD-MOD-010: `crates/chanora_state/src/lib.rs`, `channel_join.rs` | ✅ PASS | Both files exist |
| 27 | SDD-MOD-011: `crates/chanora_audio/src/` | ✅ PASS | Directory exists with 26 files |
| 28 | SDD-MOD-012: `crates/chanora_storage/src/lib.rs` | ✅ PASS | File exists |
| 29 | SDD-MOD-013: `crates/chanora_diagnostics/src/lib.rs` | ✅ PASS | File exists |
| 30 | SDD-MOD-014: `crates/chanora_resolver/src/lib.rs`, `crates/chanora_prefetch/src/lib.rs`, `prefetch_debouncer.dart` | ✅ PASS | All files exist |
| 31 | SDD-MOD-015: `.github/workflows/`, `tools/` | ✅ PASS | Both directories exist |
### docs/sysdes.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 6 | Version 0.9.8 | ⚠️ MINOR | SysRS is 0.9.11 |
| 13 | `**Repo path:** docs/architecture/sysdes.md` | ⚠️ MINOR | Malformed markdown (missing blank line before `---`) |
| 377-418 | System elements SE-01 through SE-19 | ✅ PASS | Comprehensive element list |
| 839-855 | Interface catalogue IF-001 through IF-014 | ✅ PASS | Matches architecture |
### docs/sysrs.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 5 | Version 0.9.11 | ✅ PASS | Consistent within document |
| 126-130 | Suggested downstream file names | ❌ FAIL | 5 non-existent file names |
| 233-257 | Application component requirements SysRS-024 through SysRS-034 | ✅ PASS | Match SAD component allocation |
### docs/srs.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 6 | Version 0.9.9 | ✅ PASS | Consistent within document |
| 101-176 | SWE.1 process requirements SRS-001 through SRS-007 | ✅ PASS | Match ASPICE alignment |
| 180-267 | Software boundary requirements SRS-008 through SRS-015 | ✅ PASS | Match architecture constraints |
### CONTRIBUTING.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 10 | Engineering hierarchy: SysRS -> SysDes -> SRS -> SAD -> SDD | ✅ PASS | Matches README and governance docs |
| 25 | Commit convention reference | ✅ PASS | `docs/governance/git-commit-message-convention.md` exists |
| 37-42 | Commit examples | ✅ PASS | Match README examples |
### CHANGELOG.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 7 | v0.3.0 milestone | ✅ PASS | Matches pubspec.yaml version |
| 65-66 | Flutter app version/build bumped to 0.3.0+100 | ✅ PASS | Matches pubspec.yaml |
| 99 | Default base branch is main | ✅ PASS | Confirms branch change |
| 100-101 | DSP chain not yet production-tuned | ✅ PASS | Matches implementation status |
### docs/architecture/desktop-ptt-architecture.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 17-21 | Platform backends table | ✅ PASS | Matches README PTT section |
| 24-30 | Safety rules | ✅ PASS | Watchdog, capability, fallback |
### docs/architecture/file-transfer-design.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 6 | References SAD-067, SDD-MOD-009 | ⚠️ MINOR | SAD-067 not itemized in current baseline |
| 113-137 | tsclientlib public API signatures | ⚠️ MINOR | Cannot verify against external library source |
| 400-428 | Avatar path computation in adapter.rs | ✅ PASS | `uid_to_avatar_path` function described |
### docs/i18n/localization-architecture.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 8 | Generated files under `apps/chanora_flutter/lib/l10n/generated/` | ✅ PASS | Directory exists with 3 files |
| 22 | English and Simplified Chinese generated localization files | ✅ PASS | `app_localizations_en.dart` and `app_localizations_zh.dart` exist |
### docs/ui-ux/material3-design-tokens.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 8 | Implementation token source is `apps/chanora_flutter/lib/design/chanora_tokens.dart` | ✅ PASS | File exists |
### docs/ui-ux/material3-component-catalog.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 10 | Connect and bookmarks: `connect_widgets.dart`, `input_dialogs.dart` | ✅ PASS | Both files exist in widgets/ |
| 11 | Channel and client view: `snapshot_view.dart`, `client_info_sheet.dart`, `channel_spacer.dart` | ✅ PASS | All files exist |
| 12 | Chat: `chat_views.dart`, `bbcode_text.dart` | ✅ PASS | Both files exist |
| 13 | Voice controls: `voice_bar.dart`, `voice_compact.dart`, `voice_settings*.dart` | ✅ PASS | All files exist |
| 14 | Platform/permission indicators: `permission_state_banner.dart`, `ptt_capability_badge.dart`, `talk_power_warning.dart` | ✅ PASS | All files exist |
| 15 | Diagnostics: `audio_debug_stats_panel.dart` | ✅ PASS | File exists |
### docs/ui-ux/adaptive-layout-platform-guide.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 8 | Compact/mobile layout for MVP | ✅ PASS | Matches implementation status |
### docs/security/security-privacy-legal-guideline.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 13-21 | Gate summary table | ✅ PASS | Matches threat model and audit reports |
### docs/security/threat-model.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 8 | Scope covers client, local storage, diagnostics, bridge, protocol, audio, platform, release | ✅ PASS | Comprehensive scope |
### docs/security/secure-storage-audit-report.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 12-18 | Audit matrix | ✅ PASS | Matches platform policy |
### docs/security/diagnostic-redaction-audit-report.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 12-18 | Redaction targets | ✅ PASS | Matches diagnostics crate responsibilities |
### docs/security/dependency-and-supply-chain-report.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 15-19 | Automated controls | ✅ PASS | CI workflows confirmed |
| 24-29 | Dependency areas | ✅ PASS | Matches workspace structure |
### docs/security/flutter-license-inventory.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 11-12 | References LICENSE-APACHE and LICENSE-MIT | ❌ FAIL | Files do not exist |
### docs/security/license-inventory.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 9-10 | References LICENSE-APACHE and LICENSE-MIT | ❌ FAIL | Files do not exist |
### docs/privacy/privacy-policy.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 9 | Chanora is a client application for connecting to TeamSpeak 3-compatible servers | ✅ PASS | Matches README |
### docs/legal/trademark-and-attribution-review.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 5 | DEC-012 remains open | ✅ PASS | Matches product decision register |
### docs/release/release-readiness-go-nogo-record.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 5 | Workspace version 0.2.0-beta.1, Flutter app 0.3.0+100 | ✅ PASS | Matches Cargo.toml and pubspec.yaml |
| 6 | No-Go for public/store release | ✅ PASS | Consistent with open gates |
### docs/release/platform-release-policy.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 12-19 | Platform policy table | ✅ PASS | Matches README |
### docs/release/dv-waiver-register.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 14-23 | Active waivers DV-WVR-001 through DV-WVR-009 | ✅ PASS | Comprehensive waiver list |
### docs/release/ios-build.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 12 | Build script `./tools/build-ios.sh --no-codesign` | ⚠️ MINOR | Cannot verify script exists without checking |
### docs/verification/verification-master-plan.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 5 | Applies to Rust workspace 0.2.0-beta.1, Flutter app 0.3.0+100 | ✅ PASS | Matches actual versions |
### docs/verification/swe4-unit-verification-plan.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 17 | chanora_state has 27 tests | ✅ PASS | Matches coverage analysis |
### docs/verification/swe5-software-integration-verification-plan.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 14-22 | Integration paths | ✅ PASS | Comprehensive path list |
### docs/verification/swe6-software-verification-plan.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 27-45 | MVP acceptance matrix | ✅ PASS | Comprehensive matrix |
### docs/verification/sys4-system-integration-verification-plan.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 14-22 | System elements under verification | ✅ PASS | Comprehensive list |
### docs/governance/document-index.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 14-32 | Baseline documents table | ✅ PASS | All listed paths exist |
### docs/governance/traceability-matrix.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 16 | Lifecycle chain | ✅ PASS | Matches README |
### docs/governance/product-decision-register.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 14-21 | Decision summary | ✅ PASS | Comprehensive decision list |
### docs/governance/git-commit-message-convention.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 18 | `release(android)` example | ⚠️ MINOR | Non-standard Conventional Commits type |
### docs/governance/document-naming-convention.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 8 | Lowercase kebab-case file names | ✅ PASS | Matches actual file naming |
### docs/governance/path-migration-map.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 10-14 | Migration state table | ✅ PASS | Matches actual file locations |
### docs/governance/baseline-approval-record.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 10-18 | Approval scope table | ✅ PASS | Matches baseline status |
### docs/governance/baseline-candidate-validation-report.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 10-16 | Validation summary | ✅ PASS | Comprehensive validation |
### docs/governance/document-review-report.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 19-24 | Findings table | ✅ PASS | Addresses previous gaps |
### docs/governance/repo-format-validation-report.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 8-18 | Repository layout check | ✅ PASS | All areas confirmed |
### docs/governance/decision-impact-assessment.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 8-14 | Impact matrix | ✅ PASS | Comprehensive impact list |
### docs/governance/maintainability-review-2026-06-08.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 13-23 | Changes already applied | ✅ PASS | Matches code structure |
| 61-88 | Android ADB status | ✅ PASS | Detailed smoke evidence |
### docs/references/aspice-swe2-swe3-integration-note.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 12 | Lifecycle chain | ✅ PASS | Matches README |
### docs/references/external-references.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 8-17 | Reference list | ✅ PASS | Comprehensive references |
### docs/implementation-status-2026-05-28.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 3-4 | Workspace version v0.2.0-beta.1, Flutter app 0.3.0+100 | ✅ PASS | Matches actual versions |
| 103 | References SDD-109 | ⚠️ MINOR | Not itemized in current baseline |
| 105 | References SAD-043 | ⚠️ MINOR | Not itemized in current baseline |
| 140 | Agent spec docs reference | ⚠️ MINOR | Stale cleanup note |
### SECURITY.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 33-38 | Security document list | ✅ PASS | All listed paths exist |
### apps/chanora_flutter/README.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 3 | Chanora — cross-platform voice client for TeamSpeak-compatible servers | ✅ PASS | Matches main README |
### crates/chanora_resolver/README.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 7 | `ChanoraResolver::resolve_client_request` or `resolve_client_address` | ✅ PASS | Matches function inventory |
| 60-77 | Library example | ✅ PASS | Matches API |
### tools/windows-smoke.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 6 | `product/scaffold-v0` branch | ❌ FAIL | Default branch is now `main` |
### silero-coreml/README.md
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 3 | Private Chanora-owned Apple/CoreML Silero VAD backend scaffold | ✅ PASS | Matches project scope |
### flutter_rust_bridge.yaml
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 1-5 | Bridge configuration | ✅ PASS | Matches SDD bridge boundary design |
### Cargo.toml
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 28-39 | Workspace members | ✅ PASS | All 10 crates listed |
| 46 | Version 0.2.0-beta.1 | ✅ PASS | Matches documentation |
| 48 | Rust version 1.95 | ✅ PASS | Modern Rust requirement |
### pubspec.yaml
| Line | Claim | Status | Notes |
|------|-------|--------|-------|
| 19 | Version 0.3.0+100 | ✅ PASS | Matches documentation |
| 37 | flutter_rust_bridge: 2.12.0 | ✅ PASS | Matches SDD bridge version |
## Recommendations
### High Priority (Critical)
1. **Create LICENSE-APACHE and LICENSE-MIT files** — Required for DEC-020 dual-license compliance
2. **Update README.md repository layout** — Add `crates/chanora_resolver/`, `crates/chanora_prefetch/`, `crates/chanora_cache/`
### Medium Priority (Major)
3. **Update SAD component table** — Add `chanora_cache` component
4. **Fix SDD-MOD-003 file classification** — Move `snapshot_state_mapper.dart` to correct section
5. **Update tools/windows-smoke.md** — Change branch reference from `product/scaffold-v0` to `main`
6. **Fix docs/sysrs.md suggested file names** — Remove or update non-existent file name suggestions
### Low Priority (Minor)
7. **Update implementation status date** — Refresh to reflect current state
8. **Fix malformed markdown in docs/sysdes.md:13** — Add blank line before `---`
9. **Update Material 3 guideline version** — Align with DV baseline date
10. **Standardize commit type examples** — Remove `release` type from convention examples
11. **Update SysDes version** — Align with SysRS version numbering
@@ -0,0 +1,347 @@
# Documentation Link Not-Covered Analysis
**Generated:** 2026-06-13
## Summary
- Total references checked: 148
- Broken markdown links: 2
- Missing file targets: 9 (2 LICENSE + 5 hypothetical + 2 code path mismatches)
- Orphaned docs: 18
- Suspicious external URLs: 3
## Broken Markdown Links
| File | Line | Link Text | Target | Issue |
|------|------|-----------|--------|-------|
| README.md | 428 | `LICENSE-APACHE` | `LICENSE-APACHE` | File does not exist at repo root |
| README.md | 431 | `LICENSE-MIT` | `LICENSE-MIT` | File does not exist at repo root |
**Impact:** Users clicking the license links in the README will get a 404 on GitHub. These are referenced in the License section as the dual-license model files (DEC-020).
## Missing File Targets
### Missing LICENSE Files (High Impact)
| File | Line | Referenced Path | Issue |
|------|------|----------------|-------|
| README.md | 428 | `LICENSE-APACHE` | File does not exist at repo root |
| README.md | 431 | `LICENSE-MIT` | File does not exist at repo root |
| docs/security/license-inventory.md | 9 | `../../LICENSE-APACHE` | Resolves to missing `LICENSE-APACHE` at repo root |
| docs/security/license-inventory.md | 10 | `../../LICENSE-MIT` | Resolves to missing `LICENSE-MIT` at repo root |
| docs/security/flutter-license-inventory.md | 11 | `../../LICENSE-APACHE` | Resolves to missing `LICENSE-APACHE` at repo root |
| docs/security/flutter-license-inventory.md | 11 | `../../LICENSE-MIT` | Resolves to missing `LICENSE-MIT` at repo root |
**Impact:** The dual-license model (DEC-020) requires these files to exist for proper attribution. All 4 references across 3 files are broken.
### Hypothetical File Names (Low Impact)
| File | Line | Referenced Path | Issue |
|------|------|----------------|-------|
| docs/sysrs.md | 126 | `docs/chanora_SysDes.md` | Listed as "Potential downstream file name" — does not exist |
| docs/sysrs.md | 127 | `docs/chanora_SRS.md` | Listed as "Potential downstream file name" — does not exist |
| docs/sysrs.md | 128 | `docs/chanora_SAD.md` | Listed as "Potential downstream file name" — does not exist |
| docs/sysrs.md | 129 | `docs/chanora_SDD.md` | Listed as "Potential downstream file name" — does not exist |
| docs/sysrs.md | 130 | `docs/chanora_Verification.md` | Listed as "Potential downstream file name" — does not exist |
**Note:** These are documented as "Potential downstream file names" in a table and are aspirational/historical. They are presented as plain text in a table, not as navigable links. Low severity.
## Missing Code References
| File | Line | Reference | Expected Location | Issue |
|------|------|-----------|-------------------|-------|
| docs/architecture/sdd.md | 19 | `snapshot_state_mapper.dart` | Listed under "Snapshot and channel UI" widgets section | File actually exists in `apps/chanora_flutter/lib/services/`, not `apps/chanora_flutter/lib/widgets/` — directory mismatch in docs |
| docs/architecture/sdd.md | 21 | `voice_settings*.dart` | Listed under Voice UI widgets | Files are `voice_settings.dart` and `voice_settings_controls.dart` — glob reference is ambiguous (two files match) |
**Note:** The `snapshot_state_mapper.dart` directory mismatch is a minor documentation inaccuracy — the file exists but is categorized differently than documented.
## Broken Anchor Links
No broken anchor links found. All `#section` references within documents resolve to existing headers.
## Orphaned Documents
(Not referenced by any other document in the main doc tree)
| File | Last Modified | Should Be Referenced From |
|------|---------------|--------------------------|
| docs/offline-knowledge/README.md | 2026-06-13 | Could be referenced from a top-level docs index |
| docs/offline-knowledge/function-inventory.md | 2026-06-13 | Could be referenced from docs/architecture/sdd.md |
| docs/offline-knowledge/coverage-analysis.md | 2026-06-13 | Could be referenced from docs/verification/ plans |
| docs/offline-knowledge/doc-quality-analysis.md | 2026-06-13 | Could be referenced from docs/governance/document-review-report.md |
| docs/offline-knowledge/link-coverage-report.md | 2026-06-13 | Could be referenced from docs/governance/ |
| docs/offline-knowledge/external/teaspeak-overview.md | 2026-06-13 | Could be referenced from docs/references/external-references.md |
| docs/offline-knowledge/external/respeak-overview.md | 2026-06-13 | Could be referenced from docs/references/external-references.md |
| docs/offline-knowledge/external/yatqa-en.md | 2026-06-13 | Could be referenced from docs/references/external-references.md |
| docs/offline-knowledge/external/yatqa-de.md | 2026-06-13 | Could be referenced from docs/references/external-references.md |
| docs/offline-knowledge/reviews/coverage-analysis-review.md | 2026-06-13 | Could be referenced from docs/offline-knowledge/README.md (already is) |
| docs/offline-knowledge/reviews/doc-quality-review.md | 2026-06-13 | Could be referenced from docs/offline-knowledge/README.md (already is) |
| docs/offline-knowledge/reviews/link-coverage-review.md | 2026-06-13 | Could be referenced from docs/offline-knowledge/README.md (already is) |
| docs/offline-knowledge/reviews/external-docs-review.md | 2026-06-13 | Could be referenced from docs/offline-knowledge/README.md (already is) |
| docs/superpowers/specs/*.md (6 files) | 2026-05-28 to 2026-06-09 | Internal planning docs; not expected in DV tree |
| docs/superpowers/plans/*.md (8 files) | 2026-05-28 to 2026-06-08 | Internal planning docs; not expected in DV tree |
**Note:** The offline-knowledge files are self-referencing within their own README but are not linked from the main documentation tree. The superpowers files are internal planning documents and are intentionally separate from the DV document set.
## Suspicious External URLs
| File | Line | URL | Issue |
|------|------|-----|-------|
| docs/architecture/file-transfer-research.md | 406 | `https://git.did.science/TeaSpeak/Server/Server` | Self-hosted GitLab instance; may become unavailable. Specific branch `new-groups` commit `b54c6d4e` referenced. |
| docs/security/license-inventory.md | 96 | `http://github.com/ejmahler/strength_reduce` | Uses HTTP instead of HTTPS for GitHub URL |
| docs/security/flutter-license-inventory.md | various | `http://www.apache.org/licenses/` and `http://mozilla.org/MPL/2.0/` | HTTP URLs in license text bodies (not navigational links) |
**Note:** The file-transfer research links point to specific GitHub commit SHAs which may become stale over time if force-pushes occur. The HTTP-vs-HTTPS issue on the strength_reduce URL is cosmetic but should be corrected.
## Cross-Reference Chain Issues
| Chain | Issue |
|-------|-------|
| None found | All doc-to-doc cross-references in prose text resolve correctly |
All cross-reference chains verified:
- `docs/architecture/sad.md``docs/srs.md`
- `docs/architecture/sad.md``docs/sysdes.md`
- `docs/architecture/sdd.md``docs/architecture/sad.md`
- `docs/architecture/sdd.md``docs/srs.md`
- `docs/architecture/sysdes.md``docs/sysdes.md`
- `docs/architecture/file-transfer-design.md``docs/architecture/sad.md`
- `docs/architecture/file-transfer-research.md``docs/architecture/file-transfer-design.md`
- `docs/architecture/file-transfer-implementation-plan.md` → both upstream docs ✓
- `docs/architecture/desktop-ptt-architecture.md` → sad, sdd, dv-waiver-register ✓
- `docs/requirements/sysrs.md``../sysrs.md`
- `docs/requirements/srs.md``../srs.md`
- `docs/ui-ux/material3-guideline.md``docs/material3-guideline.md`
## Missing Image/Asset References
No image references (`![alt](path)`) found in any documentation files. All docs are text-only.
## Include/Import References
No include directives or template references found in documentation files.
---
## Per-File Link Inventory
### README.md
| Line | Link | Status |
|------|------|--------|
| 130 | `docs/architecture/desktop-ptt-architecture.md` | ✓ Valid (markdown link) |
| 144 | `docs/governance/product-decision-register.md` | ✓ Valid (inline ref) |
| 261 | `docs/requirements/sysrs.md` | ✓ Valid (inline ref) |
| 262 | `docs/requirements/srs.md` | ✓ Valid (inline ref) |
| 263 | `docs/architecture/sysdes.md` | ✓ Valid (inline ref) |
| 264 | `docs/architecture/sad.md` | ✓ Valid (inline ref) |
| 265 | `docs/architecture/sdd.md` | ✓ Valid (inline ref) |
| 266 | `docs/verification/verification-master-plan.md` | ✓ Valid (inline ref) |
| 267 | `docs/release/release-readiness-go-nogo-record.md` | ✓ Valid (inline ref) |
| 268 | `docs/release/platform-release-policy.md` | ✓ Valid (inline ref) |
| 269 | `docs/governance/product-decision-register.md` | ✓ Valid (inline ref) |
| 270 | `docs/governance/traceability-matrix.md` | ✓ Valid (inline ref) |
| 271 | `docs/security/security-privacy-legal-guideline.md` | ✓ Valid (inline ref) |
| 312 | `docs/release/release-readiness-go-nogo-record.md` | ✓ Valid (inline ref) |
| 349-355 | 6 security/privacy/legal doc paths | ✓ Valid (inline refs) |
| 391 | `docs/governance/git-commit-message-convention.md` | ✓ Valid (inline ref) |
| 428 | `LICENSE-APACHE` | ✗ **BROKEN** — file does not exist |
| 431 | `LICENSE-MIT` | ✗ **BROKEN** — file does not exist |
| 436 | `docs/governance/product-decision-register.md` | ✓ Valid (markdown link) |
| 444 | `NOTICE` | ✓ Valid (markdown link) |
| 449-451 | 3 doc paths | ✓ Valid (inline refs) |
### CONTRIBUTING.md
| Line | Link | Status |
|------|------|--------|
| 25 | `docs/governance/git-commit-message-convention.md` | ✓ Valid (inline ref) |
### SECURITY.md
| Line | Link | Status |
|------|------|--------|
| 33-38 | 6 security/privacy/legal doc paths | ✓ Valid (inline refs) |
### docs/architecture/sad.md
| Line | Link | Status |
|------|------|--------|
| 6 | `docs/srs.md` | ✓ Valid (inline ref) |
| 7 | `docs/sysdes.md` | ✓ Valid (inline ref) |
| 41-52 | 12 component source paths | ✓ Valid (code refs) |
| 176 | `docs/governance/traceability-matrix.md` | ✓ Valid (inline ref) |
### docs/architecture/sdd.md
| Line | Link | Status |
|------|------|--------|
| 6 | `docs/architecture/sad.md` | ✓ Valid (inline ref) |
| 7 | `docs/srs.md` | ✓ Valid (inline ref) |
| 17-31 | Module source paths | ✓ Valid (code refs), except `snapshot_state_mapper.dart` listed under wrong section |
| 35 | `crates/chanora_bridge/src/api.rs` | ✓ Valid (code ref) |
### docs/architecture/sysdes.md
| Line | Link | Status |
|------|------|--------|
| 6 | `docs/sysdes.md` | ✓ Valid (canonical pointer) |
### docs/architecture/desktop-ptt-architecture.md
| Line | Link | Status |
|------|------|--------|
| 5 | `docs/architecture/sad.md`, `docs/architecture/sdd.md`, `docs/release/dv-waiver-register.md` | ✓ Valid (inline refs) |
### docs/architecture/file-transfer-design.md
| Line | Link | Status |
|------|------|--------|
| 6 | `docs/architecture/sad.md` | ✓ Valid (inline ref) |
| 118-137 | `tsclientlib/src/lib.rs` code references | ✓ Valid (external code refs — not locally verifiable) |
| 737 | `crates/chanora_protocol/src/adapter.rs` | ✓ Valid (code ref) |
### docs/architecture/file-transfer-research.md
| Line | Link | Status |
|------|------|--------|
| 5 | `docs/architecture/file-transfer-design.md` | ✓ Valid (inline ref) |
| 29-31 | GitHub commit URLs | ⚠ External — may become stale |
| 406 | `https://git.did.science/TeaSpeak/Server/Server` | ⚠ Self-hosted GitLab — may become unavailable |
### docs/architecture/file-transfer-implementation-plan.md
| Line | Link | Status |
|------|------|--------|
| 6 | `docs/architecture/file-transfer-design.md`, `docs/architecture/file-transfer-research.md` | ✓ Valid (inline refs) |
### docs/governance/document-index.md
| Line | Link | Status |
|------|------|--------|
| 14-32 | All 18 listed document paths | ✓ Valid (inline refs) |
### docs/governance/traceability-matrix.md
| Line | Link | Status |
|------|------|--------|
| 5 | 5 primary upstream doc paths | ✓ Valid (inline refs) |
| 25-31 | 7 source doc paths | ✓ Valid (inline refs) |
### docs/governance/path-migration-map.md
| Line | Link | Status |
|------|------|--------|
| 10-14 | 5 README path mappings | ✓ Valid (inline refs) |
### docs/verification/verification-master-plan.md
| Line | Link | Status |
|------|------|--------|
| 6 | 6 primary upstream doc paths | ✓ Valid (inline refs) |
| 18-21 | 4 verification plan paths | ✓ Valid (inline refs) |
| 47 | `tools/windows-smoke.md` | ✓ Valid (code ref) |
| 48 | `docs/release/ios-build.md` | ✓ Valid (inline ref) |
### docs/security/license-inventory.md
| Line | Link | Status |
|------|------|--------|
| 9 | `../../LICENSE-APACHE` | ✗ **BROKEN** — file does not exist |
| 10 | `../../LICENSE-MIT` | ✗ **BROKEN** — file does not exist |
| 11 | `docs/governance/product-decision-register.md` | ✓ Valid (inline ref) |
### docs/security/flutter-license-inventory.md
| Line | Link | Status |
|------|------|--------|
| 11 | `../../LICENSE-APACHE` | ✗ **BROKEN** — file does not exist |
| 12 | `../../LICENSE-MIT` | ✗ **BROKEN** — file does not exist |
### docs/security/dependency-and-supply-chain-report.md
| Line | Link | Status |
|------|------|--------|
| 29 | `https://github.com/EdisonJwa/oboe-rs` | ✓ Valid (external GitHub URL) |
### docs/privacy/privacy-policy.md
| Line | Link | Status |
|------|------|--------|
| (none) | No links or references | N/A |
### docs/legal/trademark-and-attribution-review.md
| Line | Link | Status |
|------|------|--------|
| (none) | No links or references | N/A |
### docs/release/release-readiness-go-nogo-record.md
| Line | Link | Status |
|------|------|--------|
| 37 | `docs/implementation-status-2026-05-28.md` | ✓ Valid (inline ref) |
| 26 | `apps/chanora_flutter/pubspec.yaml` | ✓ Valid (code ref) |
### docs/release/dv-waiver-register.md
| Line | Link | Status |
|------|------|--------|
| 15-23 | Various `docs/` paths in Source evidence column | ✓ Valid (inline refs) |
### docs/requirements/sysrs.md
| Line | Link | Status |
|------|------|--------|
| 4 | `../sysrs.md` | ✓ Valid (canonical pointer) |
### docs/requirements/srs.md
| Line | Link | Status |
|------|------|--------|
| 4 | `../srs.md` | ✓ Valid (canonical pointer) |
### docs/ui-ux/material3-guideline.md
| Line | Link | Status |
|------|------|--------|
| 6 | `docs/material3-guideline.md` | ✓ Valid (canonical pointer) |
### docs/material3-guideline.md
| Line | Link | Status |
|------|------|--------|
| 10 | `docs/ui-ux/material3-guideline.md` | ✓ Valid (self-referencing path record) |
### docs/sysrs.md
| Line | Link | Status |
|------|------|--------|
| 126 | `docs/chanora_SysDes.md` | ⚠ Hypothetical — does not exist (aspirational name) |
| 127 | `docs/chanora_SRS.md` | ⚠ Hypothetical — does not exist (aspirational name) |
| 128 | `docs/chanora_SAD.md` | ⚠ Hypothetical — does not exist (aspirational name) |
| 129 | `docs/chanora_SDD.md` | ⚠ Hypothetical — does not exist (aspirational name) |
| 130 | `docs/chanora_Verification.md` | ⚠ Hypothetical — does not exist (aspirational name) |
### apps/chanora_flutter/README.md
| Line | Link | Status |
|------|------|--------|
| 11 | `https://docs.flutter.dev/get-started/learn-flutter` | ✓ Valid (external) |
| 12 | `https://docs.flutter.dev/get-started/codelab` | ✓ Valid (external) |
| 13 | `https://docs.flutter.dev/reference/learning-resources` | ✓ Valid (external) |
| 16 | `https://docs.flutter.dev/` | ✓ Valid (external) |
---
## Action Items (Priority Order)
### P0 — Must Fix Before Any Release
1. **Create `LICENSE-APACHE` and `LICENSE-MIT` files** at repo root. These are required by DEC-020 (dual-license model) and referenced by README.md, docs/security/license-inventory.md, and docs/security/flutter-license-inventory.md.
### P1 — Should Fix for DV Quality
2. **Fix `snapshot_state_mapper.dart` categorization** in docs/architecture/sdd.md:19 — move from "Snapshot and channel UI" widgets section to service layer section, or add a note clarifying the actual location.
### P2 — Nice to Have
3. **Add offline-knowledge docs to document index** or references section so they are discoverable.
4. **Fix HTTP URL** in docs/security/license-inventory.md:96 (`http://github.com/ejmahler/strength_reduce``https://...`).
5. **Clean up hypothetical file names** in docs/sysrs.md:124-131 — either remove the table or clearly mark as historical/aspirational.
@@ -0,0 +1,187 @@
# Documentation Out-of-Date Analysis
**Generated:** 2026-06-13
**Workspace version:** 0.2.0-beta.1
**Latest commit:** dd6e80f (2026-06-13)
## Summary
- Total docs checked: 64
- Outdated docs: 12
- Stale version refs: 5
- Undocumented recent changes: 8
- Stale date refs: 15+
## Stale Version References
| File | Line | Version Referenced | Current Version | Drift |
|------|------|-------------------|-----------------|-------|
| `docs/sysdes.md` | 6 | 0.9.8 | N/A (doc version) | Last updated 2026-05-14, 30 days stale |
| `docs/srs.md` | 7 | 0.9.9 | N/A (doc version) | Last updated 2026-05-18, 26 days stale |
| `docs/sysrs.md` | 5 | 0.9.11 | N/A (doc version) | Last updated 2026-06-07, 6 days stale |
| `docs/material3-guideline.md` | ~4 | 0.9.2 | N/A (doc version) | Last updated 2026-05-14, 30 days stale |
| `tools/windows-smoke.md` | 6 | `product/scaffold-v0` branch | `main` | Default branch changed per CHANGELOG |
## Stale Date References
| File | Date | Age | Issue |
|------|------|-----|-------|
| `docs/implementation-status-2026-05-28.md` | 2026-05-28 | 16 days | Pre-dates DV baseline (2026-05-29) and 8 major feature PRs |
| `docs/architecture/sad.md` | 2026-05-29 | 15 days | Missing file transfer, poke notifications, desktop VAD features |
| `docs/architecture/sdd.md` | 2026-05-29 | 15 days | Missing file transfer, poke notifications, desktop VAD features |
| `docs/verification/verification-master-plan.md` | 2026-05-29 | 15 days | Missing file transfer and poke notification verification |
| `docs/verification/swe4-unit-verification-plan.md` | 2026-05-29 | 15 days | Missing new test coverage for file transfer |
| `docs/verification/swe5-software-integration-verification-plan.md` | 2026-05-29 | 15 days | Missing file transfer integration verification |
| `docs/verification/swe6-software-verification-plan.md` | 2026-05-29 | 15 days | Missing file transfer software verification |
| `docs/verification/sys4-system-integration-verification-plan.md` | 2026-05-29 | 15 days | Missing file transfer system verification |
| `docs/governance/product-decision-register.md` | 2026-05-29 | 15 days | Missing file-transfer-related decisions |
| `docs/governance/document-index.md` | 2026-05-29 | 15 days | Missing file-transfer-design.md, file-transfer-research.md, file-transfer-implementation-plan.md |
| `docs/security/security-privacy-legal-guideline.md` | 2026-05-29 | 15 days | Missing file transfer security considerations |
| `docs/security/threat-model.md` | 2026-05-29 | 15 days | Missing file transfer threat analysis |
| `docs/i18n/localization-architecture.md` | 2026-05-29 | 15 days | Missing poke notification l10n strings |
| `docs/legal/trademark-and-attribution-review.md` | 2026-05-29 | 15 days | Missing cacache license review |
| `docs/privacy/privacy-policy.md` | 2026-05-29 | 15 days | Missing file transfer data handling |
## Undocumented Recent Changes
| Change | Date | Expected Doc | Status |
|--------|------|-------------|--------|
| File transfer system (avatar/icon download with cacache) | 2026-06-10 | README.md, SAD, SDD, CHANGELOG | Not in README crate list, not in CHANGELOG |
| Poke notifications (local notifications, settings, bridge) | 2026-06-08 | SAD, SDD, CHANGELOG | Not in CHANGELOG |
| Desktop Silero ONNX VAD + Windows PTT modernization | 2026-06-09 | SAD, SDD, CHANGELOG | Not in CHANGELOG |
| iOS RemoteIO+WebRTC APM path removal | 2026-06-10 | SAD, SDD | Not documented |
| SonoraExperimental bridge API removal | 2026-06-10 | SAD, SDD, bridge docs | Not documented |
| iOS AVAudioSession activation fix | 2026-06-10 | Platform docs | Not documented |
| iOS Debug build unblocking + FRB regeneration | 2026-06-10 | Build docs | Not documented |
| poke-without-message design | 2026-06-09 | Design docs | Committed but not indexed |
## Feature Drift
### Documented but No Longer in Code
| Feature | Doc File | Last Seen In Code |
|---------|----------|-------------------|
| `SonoraExperimental` bridge API | `docs/architecture/sdd.md` (implied) | Removed 2026-06-10 (commit 2b28549) |
| iOS `ios_raw_unit.rs` | `docs/implementation-status-2026-05-28.md:33` | Removed 2026-06-10 (commit 3f9ea4f) |
| `SnapshotChanged` event variant | CHANGELOG v0.3.0 | Removed end-to-end |
| Timer-based snapshot polling | CHANGELOG v0.2.0-beta.1 | Replaced by event-driven UI |
### In Code but Not Documented
| Feature | Code Location | Expected Doc |
|---------|--------------|-------------|
| `chanora_cache` crate (cacache-backed blob store) | `crates/chanora_cache/` | README.md crate list, SAD, SDD |
| File transfer protocol support | `crates/chanora_protocol/` | SAD, SDD, CHANGELOG |
| Poke notification service | `apps/chanora_flutter/lib/services/` | SAD, SDD, CHANGELOG |
| Poke notification settings UI | `apps/chanora_flutter/lib/widgets/` | SAD, SDD |
| Desktop Silero ONNX VAD | `crates/chanora_audio/src/vad/silero_onnx.rs` | SAD, SDD |
| Windows PTT modernization | `crates/chanora_audio/src/ptt_backends/windows.rs` | SAD, SDD |
| `poke_limiter.rs` | `crates/chanora_protocol/src/poke_limiter.rs` | SAD, SDD |
| Local notification plugin integration | `apps/chanora_flutter/` | SAD, SDD |
## Per-File Out-of-Date Assessment
### README.md
- **Last meaningful update:** Unknown (no date in file)
- **Stale sections:**
- Crate list (line 242-249): Missing `chanora_cache` and `chanora_resolver` crates
- Repository layout (line 233-249): Missing `chanora_cache`, `chanora_resolver`, `chanora_prefetch`
- Status section (line 16-23): References "v0.9.x document set" — no specific date
- Development section (line 398-409): Missing `just` commands (justfile exists)
- **Missing recent changes:** File transfer system, poke notifications, desktop VAD
### docs/sysdes.md
- **Version:** 0.9.8
- **Last change record:** 2026-05-14
- **Stale sections:** All — 30 days without update
- **Missing:** File transfer system element (SE-20?), poke notification interface (IF-015?)
### docs/srs.md
- **Version:** 0.9.9
- **Last change record:** 2026-05-18
- **Stale sections:** All — 26 days without update
- **Missing:** File transfer SRS requirements, poke notification SRS requirements, desktop VAD SRS requirements
### docs/sysrs.md
- **Version:** 0.9.11
- **Last change record:** 2026-06-07
- **Stale sections:** Mostly current but missing file transfer and poke notification requirements
### docs/architecture/sad.md
- **Date:** 2026-05-29
- **Stale sections:**
- Component architecture table (line 39-50): Missing `chanora_cache` component
- Missing file transfer architecture
- Missing poke notification architecture
- Missing desktop VAD architecture
- **Missing recent changes:** All PRs from 2026-06-07 through 2026-06-13
### docs/architecture/sdd.md
- **Date:** 2026-05-29
- **Stale sections:**
- Module catalogue (line 15-31): Missing file transfer module, poke notification module
- Missing `chanora_cache` module (SDD-MOD-016?)
- Missing `poke_limiter` module
- **Missing recent changes:** All PRs from 2026-06-07 through 2026-06-13
### docs/implementation-status-2026-05-28.md
- **Date:** 2026-05-28 — 16 days old
- **Stale sections:**
- "Done" table: Missing file transfer, poke notifications, desktop VAD, iOS fixes
- "Partial / Scaffold Only": `chanora_cache` was scaffold, now implemented
- "Not Done (P0 blockers)": iOS `AVAudioSession.Mode.voiceChat` — now implemented (commit 89bbfa1)
- Android target compilation: Still blocked per DEC-034
- Agent spec docs reference (line 140): States docs are "deleted" — stale cleanup note
- **Recommendation:** Update to reflect current state or create new status doc
### docs/governance/product-decision-register.md
- **Date:** 2026-05-29
- **Note:** DEC-033 and DEC-034 are present (lines 20-21). Missing decisions:
- File transfer architecture decision
- Poke notification feature decision
### docs/governance/document-index.md
- **Date:** 2026-05-29
- **Missing documents:**
- `docs/architecture/file-transfer-design.md`
- `docs/architecture/file-transfer-research.md`
- `docs/architecture/file-transfer-implementation-plan.md`
- `docs/superpowers/specs/2026-06-09-poke-without-message-design.md`
### docs/security/license-inventory.md
- **Status:** Refreshed 2026-06-09 (commit b841d3f)
- **Issue:** May be missing `cacache` dependency license if not in Cargo.lock at refresh time
### docs/material3-guideline.md
- **Version:** 0.9.2
- **Last change record:** 2026-05-14
- **Status:** 30 days stale, but Material 3 design may not have changed
### tools/windows-smoke.md
- **Stale reference:** Line 6 references `product/scaffold-v0` branch
- **Current default:** `main` per CHANGELOG v0.3.0
### docs/verification/*.md (all 5 files)
- **Date:** All dated 2026-05-29
- **Missing:** File transfer verification, poke notification verification, desktop VAD verification
## Recommendations
### Critical (blocks DV/release)
1. **Update `docs/implementation-status-2026-05-28.md`** — 16 days stale, missing 8 major PRs, iOS voiceChat now implemented
2. **Update `docs/governance/product-decision-register.md`** — Missing file transfer and poke notification decisions
3. **Update `docs/governance/document-index.md`** — Missing 3 file-transfer docs
### High Priority (DV completeness)
4. **Update `docs/architecture/sad.md`** — Missing file transfer, poke notifications, desktop VAD, chanora_cache component
5. **Update `docs/architecture/sdd.md`** — Missing file transfer, poke notifications, desktop VAD modules
6. **Update `docs/sysdes.md`** — 30 days stale, missing file transfer system elements
7. **Update `docs/srs.md`** — 26 days stale, missing file transfer and poke notification requirements
8. **Update CHANGELOG.md** — Missing v0.3.0+ changes (file transfer, poke notifications, desktop VAD, iOS fixes)
### Medium Priority (accuracy)
9. **Update README.md** — Missing `chanora_cache` and `chanora_resolver` in crate list
10. **Update `tools/windows-smoke.md`** — Fix stale branch reference
11. **Update verification plans** — Add file transfer and poke notification verification
12. **Update security docs** — Add file transfer threat analysis
### Low Priority (cleanup)
13. **Align document versions** — SysDes (0.9.8), SysRS (0.9.11), Material3 (0.9.2) have different version numbers
14. **Clean up path record files**`docs/architecture/sysdes.md`, `docs/requirements/sysrs.md`, `docs/requirements/srs.md`, `docs/ui-ux/material3-guideline.md` are stubs pointing to canonical files
@@ -0,0 +1,611 @@
# Chanora Function Inventory
> Auto-generated comprehensive inventory of all public APIs across 10 Rust crates and 50+ Dart files.
## Summary Statistics
| Category | Count |
|----------|-------|
| **Rust Crates** | 10 |
| **Rust pub fn** | ~180 |
| **Rust pub struct** | ~90 |
| **Rust pub enum** | ~50 |
| **Rust pub trait** | 6 |
| **Rust pub const** | ~30 |
| **Dart files** | 56 |
| **Dart public classes** | ~80 |
| **TODO/FIXME comments** | 15 |
| **Empty/commented stubs** | 0 |
---
## Rust Crates
### 1. `chanora_cache` — Content-Addressed Blob Cache
Disposable blob cache for avatar/icon files. Wraps `cacache` for crash safety.
| Kind | Name | File:Line | Purpose |
|------|------|-----------|---------|
| struct | `BlobCache` | lib.rs:30 | Content-addressed blob cache backed by cacache |
| enum | `BlobCacheError` | lib.rs:20 | Errors raised by blob cache (Io, InvalidKey) |
| const | `PREFIX_AVATAR` | lib.rs:37 | Avatar blob prefix `"av_"` |
| const | `PREFIX_ICON` | lib.rs:39 | Icon blob prefix `"ic_"` |
| fn | `BlobCache::new` | lib.rs:46 | Create/open cache rooted at `cache_dir/chanora/` |
| fn | `BlobCache::put` | lib.rs:63 | Store a blob with prefix+key |
| fn | `BlobCache::get` | lib.rs:80 | Read a blob (returns None if missing) |
| fn | `BlobCache::remove` | lib.rs:101 | Delete a specific blob |
| fn | `BlobCache::clear` | lib.rs:111 | Delete all blobs |
| fn | `BlobCache::total_size` | lib.rs:129 | Return total bytes used |
| fn | `BlobCache::evict` | lib.rs:154 | Evict oldest entries until under max_bytes |
**Dead code:** None found. All public items consumed by `chanora_core`.
---
### 2. `chanora_protocol` — TeamSpeak Protocol Adapter
Isolates `tsclientlib` behind a typed boundary. No upstream types leak.
| Kind | Name | File:Line | Purpose |
|------|------|-----------|---------|
| struct | `ConnectConfig` | adapter.rs:146 | Typed connection parameters |
| struct | `ProtocolClient` | adapter.rs:236 | Async handle owning live protocol connection |
| struct | `InboundVoice` | adapter.rs:260 | One inbound voice packet from remote client |
| struct | `SnapshotProbe` | adapter.rs:270 | Clone-free probe handle for watchdog |
| struct | `ChannelInfo` | dto.rs:19 | One channel in server tree |
| struct | `ClientInfo` | dto.rs:72 | One connected client |
| struct | `ClientProfile` | dto.rs:96 | Rich profile + live connection details |
| struct | `ServerSnapshot` | dto.rs:153 | Full server state snapshot |
| struct | `ChatMessage` | dto.rs:50 | In-channel text message |
| struct | `ServerActivity` | dto.rs:65 | Server-activity notification |
| struct | `ChannelId` | dto.rs:11 | Opaque channel identifier (u64 newtype) |
| struct | `ClientId` | dto.rs:15 | Opaque client identifier (u64 newtype) |
| struct | `PokeLimiter` | poke_limiter.rs:19 | Per-connection poke rate limiter |
| enum | `ProtocolError` | lib.rs:62 | Typed error catalogue (10 variants) |
| enum | `ProtocolDelta` | dto.rs:183 | Incremental state changes (7 variants) |
| enum | `DisconnectReason` | adapter.rs:225 | Why protocol task ended |
| enum | `MessageTarget` | dto.rs:37 | Text message target scope |
| enum | `PokeStrength` | poke_limiter.rs:8 | Poke notification strength |
| trait | *(re-exports)* | lib.rs:52 | `AudioData`, `CodecType`, `Direction`, `InAudioBuf`, `OutAudio`, `OutPacket` |
| fn | `ProtocolClient::generate_identity` | adapter.rs:296 | Generate fresh TS3 identity string |
| fn | `ProtocolClient::connect` | adapter.rs:304 | Dial server, wait for initial snapshot |
| fn | `ProtocolClient::snapshot` | adapter.rs:354 | Read typed server state snapshot |
| fn | `ProtocolClient::client_profile` | adapter.rs:365 | Fetch rich client profile |
| fn | `ProtocolClient::download_avatar` | adapter.rs:389 | Download avatar bytes by UID |
| fn | `ProtocolClient::download_icon` | adapter.rs:394 | Download icon bytes by ID |
| fn | `ProtocolClient::disconnect` | adapter.rs:399 | Clean disconnect |
| fn | `ProtocolClient::move_to_channel` | adapter.rs:421 | Move self to channel |
| fn | `ProtocolClient::queue_move_to_channel` | adapter.rs:442 | Fire-and-forget move |
| fn | `ProtocolClient::set_muted` | adapter.rs:458 | Update own mute state |
| fn | `ProtocolClient::voice_out` | adapter.rs:477 | Get outbound voice sender |
| fn | `ProtocolClient::snapshot_probe` | adapter.rs:485 | Get watchdog probe handle |
| fn | `ProtocolClient::take_voice_in` | adapter.rs:493 | Take inbound voice receiver |
| fn | `ProtocolClient::put_voice_in` | adapter.rs:503 | Put voice receiver back |
| fn | `ProtocolClient::take_loss_notifier` | adapter.rs:519 | Take disconnect notifier |
| fn | `ProtocolClient::take_chat_rx` | adapter.rs:525 | Take chat receiver |
| fn | `ProtocolClient::put_chat_rx` | adapter.rs:530 | Put chat receiver back |
| fn | `ProtocolClient::take_activity_rx` | adapter.rs:540 | Take activity receiver |
| fn | `ProtocolClient::put_activity_rx` | adapter.rs:545 | Put activity receiver back |
| fn | `ProtocolClient::take_delta_rx` | adapter.rs:556 | Take delta receiver |
| fn | `ProtocolClient::send_text_message` | adapter.rs:561 | Send text message |
| fn | `SnapshotProbe::probe` | adapter.rs:278 | Issue single snapshot RPC |
| fn | `PokeLimiter::new` | poke_limiter.rs:36 | Create limiter with 5-min window |
| fn | `PokeLimiter::record` | poke_limiter.rs:44 | Record poke, return strength |
| fn | `ChannelId::ROOT` | dto.rs:176 | Root channel constant |
**Dead code:** None found. All items consumed by `chanora_core`.
---
### 3. `chanora_bridge` — Flutter/Rust Bridge
Typed DTOs and commands for `flutter_rust_bridge` 2.x.
| Kind | Name | File:Line | Purpose |
|------|------|-----------|---------|
| struct | `BridgeChannel` | api.rs:404 | Channel DTO for Dart |
| struct | `BridgeClient` | api.rs:422 | Client DTO for Dart |
| struct | `BridgeClientProfile` | api.rs:445 | Rich profile DTO for Dart |
| struct | `BridgeSnapshot` | api.rs:502 | Server snapshot DTO for Dart |
| struct | `BridgeAudioStats` | api.rs:1034 | Audio engine statistics |
| struct | `BridgeAudioProcessingConfig` | api.rs:1112 | Audio processing config DTO |
| struct | `BridgeAudioProcessingStats` | api.rs:1143 | Audio processing stats DTO |
| struct | `BridgePttDescriptor` | api.rs:854 | PTT capability descriptor |
| struct | `BridgePttBinding` | api.rs:875 | PTT binding display state |
| enum | `BridgeError` | lib.rs:55 | Bridge-layer errors (7 variants) |
| enum | `BridgeTransmitMode` | api.rs:731 | Transmit mode mirror |
| enum | `BridgePttInputClass` | api.rs:843 | PTT input class |
| enum | `BridgeAudioRoute` | api.rs:1047 | Audio route class |
| enum | `BridgeIosVoiceProcessingMode` | api.rs:1064 | iOS voice processing mode |
| enum | `BridgeAudioBackend` | api.rs:1071 | Processing backend |
| enum | `BridgeVadBackend` | api.rs:1084 | VAD backend |
| enum | `BridgeEffectOwner` | api.rs:1097 | AEC/NS/AGC owner |
| fn | `bridge_init` | api.rs:220 | One-time process init (FRB init) |
| fn | `log_file_path_str` | api.rs:294 | Platform log file path |
| fn | `connect` | api.rs:601 | Connect to TS3 server |
| fn | `prefetch_server` | api.rs:636 | Warm DNS resolution |
| fn | `snapshot` | api.rs:645 | Re-fetch server snapshot |
| fn | `client_profile` | api.rs:654 | Fetch client profile |
| fn | `disconnect` | api.rs:663 | Disconnect from server |
| fn | `is_connected` | api.rs:672 | Check connection status |
| fn | `handle_route_change` | api.rs:687 | iOS route change handler |
| fn | `handle_media_services_reset_with_route` | api.rs:696 | iOS media reset handler |
| fn | `handle_interruption_began` | api.rs:703 | iOS interruption begin |
| fn | `handle_interruption_ended` | api.rs:709 | iOS interruption end |
| fn | `set_ptt` | api.rs:718 | Set PTT active state |
| fn | `voice_join` | api.rs:770 | Join voice channel |
| fn | `voice_leave` | api.rs:785 | Leave voice channel |
| fn | `set_transmit_mode` | api.rs:794 | Set transmit mode |
| fn | `get_transmit_mode` | api.rs:803 | Get transmit mode |
| fn | `set_release_tail_ms` | api.rs:814 | Set release tail |
| fn | `get_release_tail_ms` | api.rs:823 | Get release tail |
| fn | `set_hard_mute` | api.rs:832 | Engage/release hard mute |
| fn | `set_ptt_binding` | api.rs:907 | Update PTT binding |
| fn | `ptt_descriptor` | api.rs:925 | Get PTT descriptor |
| fn | `get_ptt_binding` | api.rs:941 | Get persisted PTT binding |
| fn | `move_to_channel` | api.rs:955 | Move self to channel |
| fn | `set_input_muted` | api.rs:971 | Toggle input mute |
| fn | `set_output_muted` | api.rs:983 | Toggle output mute |
| fn | `set_output_gain` | api.rs:994 | Set master output gain |
| fn | `set_client_volume` | api.rs:1006 | Set per-client volume |
| fn | `send_chat_message` | api.rs:1015 | Send text message |
| fn | `export_diagnostics` | api.rs:1392 | User-initiated diagnostic export |
**Dead code:** `publish_permission_state` is `#[cfg_attr(not(target_os = "android"), allow(dead_code))]` — intentional, only used on Android via JNI.
---
### 4. `chanora_storage` — Identity & Bookmark Storage
SQLite bookmarks + ChaCha20-Poly1305 encrypted identity file.
| Kind | Name | File:Line | Purpose |
|------|------|-----------|---------|
| struct | `IdentityFileStore` | lib.rs:176 | Encrypted identity file store |
| struct | `BookmarkRepository` | lib.rs:792 | SQLite-backed bookmark store |
| struct | `Bookmark` | lib.rs:767 | A persisted bookmark |
| struct | `PttBindingMeta` | lib.rs:117 | PTT binding metadata |
| enum | `StorageError` | lib.rs:59 | Storage errors (6 variants) |
| trait | `Crypto` | lib.rs:673 | Envelope encryption abstraction |
| const | `KEYRING_SERVICE` | lib.rs:190 | Keyring service name `"chanora"` |
| fn | `IdentityFileStore::new` | lib.rs:194 | Construct store at directory |
| fn | `IdentityFileStore::path` | lib.rs:211 | Get identity file path |
| fn | `IdentityFileStore::crypto` | lib.rs:384 | Get DekCrypto helper |
| fn | `IdentityFileStore::load` | lib.rs:390 | Read persisted identity |
| fn | `IdentityFileStore::save` | lib.rs:449 | Persist identity (encrypted) |
| fn | `IdentityFileStore::set_transmit_mode` | lib.rs:520 | Persist transmit mode |
| fn | `IdentityFileStore::get_transmit_mode` | lib.rs:528 | Read transmit mode |
| fn | `IdentityFileStore::set_release_tail_ms` | lib.rs:534 | Persist release tail |
| fn | `IdentityFileStore::get_release_tail_ms` | lib.rs:542 | Read release tail |
| fn | `IdentityFileStore::set_ptt_binding` | lib.rs:554 | Persist PTT binding |
| fn | `IdentityFileStore::get_ptt_binding` | lib.rs:568 | Read PTT binding |
| fn | `IdentityFileStore::clear` | lib.rs:579 | Remove persisted identity |
| fn | `BookmarkRepository::new` | lib.rs:801 | Open DB without encryption |
| fn | `BookmarkRepository::with_crypto` | lib.rs:807 | Open DB with password encryption |
| fn | `BookmarkRepository::encrypts_passwords` | lib.rs:858 | Check if encryption wired |
| fn | `BookmarkRepository::add` | lib.rs:865 | Insert bookmark |
| fn | `BookmarkRepository::upsert_or_add` | lib.rs:891 | Insert or update by host |
| fn | `BookmarkRepository::update` | lib.rs:935 | Replace existing bookmark |
| fn | `BookmarkRepository::delete` | lib.rs:963 | Delete bookmark by id |
| fn | `BookmarkRepository::list` | lib.rs:976 | List all bookmarks |
**Dead code:** None found.
---
### 5. `chanora_state` — Server State Mirror
Authoritative client-side mirror of server state with deterministic reducers.
| Kind | Name | File:Line | Purpose |
|------|------|-----------|---------|
| struct | `ServerState` | lib.rs:62 | Authoritative server state mirror |
| struct | `Reduction` | lib.rs:266 | Result of applying one event |
| struct | `ChannelJoinState` | channel_join.rs:53 | Channel-join reducer state |
| struct | `AuthoritativeMembership` | channel_join.rs:27 | Server-confirmed membership |
| struct | `JoinPending` | channel_join.rs:36 | Active pending join intent |
| struct | `ChannelJoinProjection` | channel_join.rs:135 | Reducer projection for UI |
| struct | `JoinOutcomeKey` | channel_join.rs:124 | Correlation key for outcomes |
| struct | `ConnectionEpoch` | channel_join.rs:15 | Per-connection epoch |
| struct | `JoinGeneration` | channel_join.rs:19 | Monotonic join generation |
| struct | `JoinRequestId` | channel_join.rs:23 | Protocol request identifier |
| struct | `ChannelId` (join) | channel_join.rs:11 | Channel identifier at reducer seam |
| enum | `ConnectionState` | lib.rs:43 | Connection lifecycle (5 variants) |
| enum | `Delta` | lib.rs:209 | State changes for bridge (8 variants) |
| enum | `StateEvent` | lib.rs:237 | Events flowing into reducer (9 variants) |
| enum | `StateError` | lib.rs:32 | State errors (2 variants) |
| enum | `ChannelJoinEvent` | channel_join.rs:165 | Channel-join events (10 variants) |
| enum | `ChannelJoinAction` | channel_join.rs:240 | Side-effect actions (7 variants) |
| enum | `ChannelJoinSyncState` | channel_join.rs:101 | Sync readiness (2 variants) |
| enum | `SyncReason` | channel_join.rs:115 | Sync reason (2 variants) |
| enum | `JoinReduceStatus` | channel_join.rs:309 | Transition status (9 variants) |
| enum | `JoinIntentRejected` | channel_join.rs:332 | Rejection reasons (2 variants) |
| enum | `JoinFailureKind` | channel_join.rs:341 | Failure kinds (5 variants) |
| enum | `JoinErrorCode` | channel_join.rs:356 | Stable error codes (11 variants) |
| enum | `JoinDiagnosticKey` | channel_join.rs:284 | Diagnostic event keys (10 variants) |
| enum | `AuthoritativeSource` | channel_join.rs:156 | Membership input source |
| fn | `ServerState::from_snapshot` | lib.rs:83 | Build from initial snapshot |
| fn | `ServerState::replace_from_snapshot` | lib.rs:114 | Replace with fresh snapshot |
| fn | `ServerState::channel` | lib.rs:119 | Look up channel by id |
| fn | `ServerState::client` | lib.rs:124 | Look up client by id |
| fn | `ServerState::channels` | lib.rs:130 | All channels iterator |
| fn | `ServerState::clients` | lib.rs:141 | All clients iterator |
| fn | `ServerState::channel_count` | lib.rs:148 | Number of channels |
| fn | `ServerState::client_count` | lib.rs:153 | Number of clients |
| fn | `ServerState::own_channel` | lib.rs:158 | Own client's channel |
| fn | `ServerState::clients_in_channel` | lib.rs:164 | Clients in specific channel |
| fn | `reduce` | lib.rs:281 | Apply StateEvent to state |
| fn | `reduce_reconnect_snapshot` | lib.rs:409 | Replace state after reconnect |
| fn | `channel_join::reduce` | channel_join.rs:393 | Channel-join event reducer |
| fn | `channel_join::project` | channel_join.rs:670 | Build channel-join projection |
| fn | `ChannelJoinState::new` | channel_join.rs:68 | Create join state for epoch |
**Dead code:** None found.
---
### 6. `chanora_audio` — Audio Subsystem
Platform capture/playback, Opus encoding, VAD, PTT, DSP.
| Kind | Name | File:Line | Purpose |
|------|------|-----------|---------|
| **Core Engine** | | | |
| struct | `AudioEngine` | engine.rs:289 | Main audio engine |
| struct | `AudioEngineConfig` | engine.rs:223 | Engine configuration |
| struct | `SessionAudioId` | engine.rs:69 | Stable audio session ID |
| struct | `AudioDeviceList` | engine.rs:87 | Available audio devices |
| struct | `AudioDeviceInfo` | engine.rs:96 | Single audio device info |
| enum | `AudioError` | lib.rs:100 | Audio subsystem errors (8 variants) |
| struct | `AudioEffects` | lib.rs:136 | AEC/AGC/NS/HPF toggles |
| fn | `list_audio_devices` | engine.rs:176 | Enumerate input/output devices |
| fn | `AudioEngine::start` | engine.rs:634 | Start audio engine |
| fn | `AudioEngine::start_with_gate` | engine.rs:647 | Start with transmit gate |
| fn | `AudioEngine::stop` | engine.rs:1358 | Stop audio engine |
| fn | `AudioEngine::set_transmit_active` | engine.rs:1601 | Set transmit state |
| fn | `AudioEngine::transmit_active` | engine.rs:1606 | Get transmit state |
| fn | `AudioEngine::set_output_muted` | engine.rs:1702 | Set output mute |
| fn | `AudioEngine::set_output_gain` | engine.rs:1714 | Set output gain |
| fn | `AudioEngine::set_client_volume` | engine.rs:1727 | Set per-client volume |
| fn | `AudioEngine::set_audio_processing_config` | engine.rs:1646 | Update processing config |
| fn | `AudioEngine::audio_processing_stats` | engine.rs:1683 | Get processing stats |
| **Frame Helpers** | | | |
| const | `SAMPLE_RATE_HZ` | frame.rs:9 | 48000 Hz |
| const | `FRAME_10MS_SAMPLES` | frame.rs:15 | 480 samples |
| const | `FRAME_20MS_SAMPLES` | frame.rs:17 | 960 samples |
| struct | `AudioFrame10ms` | frame.rs:21 | 10ms processing frame |
| struct | `AudioFrame20ms` | frame.rs:28 | 20ms network frame |
| fn | `i16_to_f32` | frame.rs:64 | PCM conversion |
| fn | `f32_to_i16` | frame.rs:69 | PCM conversion |
| fn | `dbfs` | frame.rs:74 | RMS dBFS calculation |
| **Transmit** | | | |
| enum | `TransmitMode` | transmit_mode.rs:13 | Ptt/Continuous/VoiceActivity |
| enum | `PermissionGate` | transmit_selector.rs:38 | Mic permission state |
| struct | `TransmitModeSelector` | transmit_selector.rs:88 | Multi-signal transmit selector |
| struct | `AudioTransmitGate` | ptt.rs:139 | Atomic transmit flag |
| struct | `ReleaseTailTimer` | release_tail.rs:43 | PTT release-tail timer |
| const | `DEFAULT_TAIL_MS` | release_tail.rs:24 | 200ms default |
| const | `MAX_TAIL_MS` | release_tail.rs:21 | 500ms max |
| **PTT** | | | |
| enum | `PttCapabilityLevel` | ptt.rs:34 | L0-L4 capability levels |
| struct | `PttBackendDescriptor` | ptt.rs:97 | Privacy-safe PTT descriptor |
| struct | `MissedKeyUpWatchdog` | ptt.rs:202 | PTT safety watchdog |
| trait | `DesktopPttBackend` | ptt_backends/mod.rs:162 | Platform PTT backend trait |
| struct | `PttBinding` | ptt_backends/mod.rs:48 | PTT binding metadata |
| enum | `PttInputClass` | ptt_backends/mod.rs:84 | None/Keyboard/MouseSideButton |
| enum | `PttBackendError` | ptt_backends/mod.rs:113 | PTT backend errors |
| fn | `select_ptt_backend` | ptt_backends/mod.rs:213 | Auto-select best backend |
| **Audio Processing** | | | |
| enum | `AudioRoute` | audio_processing.rs:14 | Route class (6 variants) |
| enum | `AudioBackend` | audio_processing.rs:65 | Processing backend (4 variants) |
| enum | `VadBackend` | audio_processing.rs:90 | VAD backend (4 variants) |
| enum | `EffectOwner` | audio_processing.rs:122 | Effect owner (5 variants) |
| enum | `IosVoiceProcessingMode` | audio_processing.rs:58 | iOS VPIO mode |
| struct | `AudioProcessingConfig` | audio_processing.rs:137 | Full processing config |
| struct | `AudioProcessingStats` | audio_processing.rs:270 | Processing statistics |
| struct | `SharedAudioProcessingStats` | audio_processing.rs:322 | Thread-safe stats |
| **DSP** | | | |
| struct | `Aec3` | processor/dsp/aec3.rs:48 | Acoustic echo canceller |
| struct | `Agc2` | processor/dsp/agc2.rs:206 | Automatic gain control |
| struct | `HighPassFilter` | processor/dsp/hpf.rs:42 | High-pass filter |
| struct | `NoiseSuppressor` | processor/dsp/ns.rs:40 | Noise suppressor |
| trait | `AudioProcessor` | processor/mod.rs:21 | Realtime processor trait |
| struct | `NoopProcessor` | processor/noop.rs:6 | No-op processor |
| struct | `PlatformVoiceProcessor` | processor/platform.rs:10 | Platform VPIO processor |
| struct | `SonoraProcessor` | processor/sonora.rs:88 | Sonora DSP processor |
| struct | `SonoraConfig` | processor/sonora.rs:37 | Sonora configuration |
| struct | `WebRtcApmProcessor` | processor/webrtc_apm.rs:91 | WebRTC APM processor |
| struct | `WebRtcApmConfig` | processor/webrtc_apm.rs:17 | WebRTC APM config |
| **VAD** | | | |
| trait | `VoiceActivityDetector` | vad/mod.rs:34 | VAD trait |
| struct | `VadOutput` | vad/mod.rs:26 | VAD output (probability + speech) |
| struct | `WebRtcFallbackVad` | vad/mod.rs:40 | WebRTC fallback VAD |
| struct | `Resampled16kHzVad` | vad/mod.rs:77 | 48→16kHz resampling wrapper |
| struct | `SileroOnnxVad` | vad/silero_onnx.rs:62 | Silero ONNX VAD |
| struct | `Downsampler48to16` | vad/resampler.rs:44 | 48→16kHz downsampler |
| fn | `set_silero_model_path` | vad/mod.rs:126 | Set VAD model path |
| fn | `silero_model_epoch` | vad/mod.rs:147 | Get model epoch |
| **Voice Activity** | | | |
| struct | `VoiceActivityStateMachine` | voice_activity.rs:29 | VAD gate state machine |
| **Mobile Backend** | | | |
| trait | `MobileVoiceAudioBackend` | mobile_voice_backend.rs:262 | Mobile audio backend trait |
| struct | `AndroidVoiceStreamConfig` | mobile_voice_backend.rs:194 | Android stream config |
| struct | `AndroidAudioDiagnostics` | mobile_voice_backend.rs:499 | Android diagnostics |
| enum | `BackendEvent` | mobile_voice_backend.rs:35 | Backend events |
| enum | `BackendError` | mobile_voice_backend.rs:154 | Backend errors |
| enum | `AchievedPerformanceMode` | mobile_voice_backend.rs:112 | Performance mode |
| enum | `LatencyTier` | mobile_voice_backend.rs:377 | Latency tier |
| struct | `AndroidVoiceUnit` | android_voice_unit.rs:565 | Android voice unit |
| struct | `IosVoiceUnit` | ios_voice_unit.rs:522 | iOS voice unit |
| **Route Policy** | | | |
| fn | `ios_route_policy` | route_policy.rs:27 | Route→config policy for iOS |
| fn | `apply_route_change` | route_policy.rs:105 | Apply route change |
| **Mode Stack** | | | |
| struct | `ModeStack` | mode_stack.rs:88 | Android audio mode refcount |
| enum | `ModeAcquire` | mode_stack.rs:41 | Acquire result |
| enum | `ModeRelease` | mode_stack.rs:62 | Release result |
| **Debug** | | | |
| struct | `WavDebugRecorder` | debug_wav.rs:68 | Debug WAV recorder |
**Dead code:** `AndroidVoiceUnit`, `IosVoiceUnit`, and platform-specific backends are `#[cfg]`-gated — intentional.
---
### 7. `chanora_resolver` — DNS/SRV/TSDNS Resolver
TeamSpeak address resolution: SRV, TSDNS, nick lookup.
| Kind | Name | File:Line | Purpose |
|------|------|-----------|---------|
| struct | `ChanoraResolver` | lib.rs:108 | Main resolver |
| struct | `Args` | lib.rs:26 | Resolution arguments |
| struct | `BuildInfo` | lib.rs:33 | Build metadata |
| struct | `SrvRecord` | lib.rs:40 | SRV record |
| struct | `ClientResolution` | lib.rs:58 | Client resolution result |
| enum | `Resolution` | lib.rs:85 | Resolution result (Dns/Srv/Nick) |
| enum | `ClientResolutionMethod` | lib.rs:48 | Resolution method (6 variants) |
| const | `DEFAULT_TEAMSPEAK_PORT` | lib.rs:23 | Port 9987 |
| fn | `build_info` | lib.rs:123 | Get build info |
| fn | `setup_log` | lib.rs:137 | Setup logging |
| fn | `ChanoraResolver::new` | lib.rs:157 | Create resolver |
| fn | `ChanoraResolver::resolve` | lib.rs:178 | Resolve with Args |
| fn | `ChanoraResolver::resolve_connection_address` | lib.rs:193 | Resolve to connection address |
| fn | `ChanoraResolver::resolve_client_address` | lib.rs:197 | Resolve client input to address |
| fn | `ChanoraResolver::resolve_client_request` | lib.rs:201 | Resolve with full metadata |
| fn | `ChanoraResolver::resolve_dns` | lib.rs:594 | DNS lookup |
| fn | `ChanoraResolver::resolve_ts3` | lib.rs:614 | TS3 SRV lookup |
| fn | `ChanoraResolver::resolve_tsdns` | lib.rs:619 | TSDNS SRV lookup |
| fn | `ChanoraResolver::resolve_nick` | lib.rs:624 | Nick lookup |
| fn | `normalize_args` | lib.rs:778 | Normalize Args |
| fn | `validate_args` | lib.rs:785 | Validate Args |
| fn | `run` | lib.rs:804 | CLI entry point |
**Dead code:** `run()` is a CLI entry point, not called from library code — intentional.
---
### 8. `chanora_prefetch` — Server Address Prefetch
Speculative DNS warming for faster connects.
| Kind | Name | File:Line | Purpose |
|------|------|-----------|---------|
| struct | `ServerPrefetcher` | lib.rs:83 | Prefetch cache + async resolver |
| enum | `ServerPrefetchError` | lib.rs:18 | Prefetch errors |
| fn | `ServerPrefetcher::new` | lib.rs:90 | Create prefetcher |
| fn | `ServerPrefetcher::prefetch` | lib.rs:99 | Schedule fire-and-forget prefetch |
| fn | `ServerPrefetcher::fresh_match` | lib.rs:145 | Check for cached result |
**Dead code:** None found.
---
### 9. `chanora_diagnostics` — Redaction & Diagnostic Export
Redaction policy, in-memory log sink, PTT sanitizer.
| Kind | Name | File:Line | Purpose |
|------|------|-----------|---------|
| struct | `Redactor` | lib.rs:121 | Production redaction policy |
| struct | `KnownSecretRegistry` | lib.rs:79 | Secret substring registry |
| struct | `InMemoryLogSink` | lib.rs:356 | Bounded redacted log sink |
| struct | `RedactingLogLayer` | lib.rs:440 | tracing Layer for redaction |
| struct | `PttSanitizer` | lib.rs:505 | PTT field ban Layer |
| struct | `DiagnosticExport` | lib.rs:614 | Export bundle |
| struct | `ProtocolEventRecorder` | lib.rs:714 | Protocol event ring buffer |
| enum | `DiagnosticsError` | lib.rs:51 | Diagnostics errors |
| const | `REDACTION_MARKER` | lib.rs:62 | `"[REDACTED]"` |
| const | `DEFAULT_LOG_CAPACITY` | lib.rs:67/70 | 256 (release) / 4096 (debug) |
| fn | `Redactor::with_default_policy` | lib.rs:128 | Create redactor |
| fn | `Redactor::with_secrets` | lib.rs:134 | Create with secret registry |
| fn | `Redactor::secrets` | lib.rs:140 | Access secret registry |
| fn | `Redactor::redact` | lib.rs:150 | Apply redaction policy |
| fn | `KnownSecretRegistry::register` | lib.rs:85 | Register secret |
| fn | `KnownSecretRegistry::len` | lib.rs:99 | Count secrets |
| fn | `KnownSecretRegistry::is_empty` | lib.rs:104 | Check empty |
| fn | `KnownSecretRegistry::contains_substr` | lib.rs:110 | Substring check |
| fn | `InMemoryLogSink::new` | lib.rs:365 | Create sink |
| fn | `InMemoryLogSink::snapshot` | lib.rs:377 | Snapshot lines |
| fn | `InMemoryLogSink::push` | lib.rs:386 | Push redacted line |
| fn | `InMemoryLogSink::redactor` | lib.rs:397 | Access redactor |
| fn | `RedactingLogLayer::new` | lib.rs:446 | Create layer |
| fn | `RedactingLogLayer::with_sanitizer` | lib.rs:452 | Wrap with PTT sanitizer |
| fn | `PttSanitizer::wrap` | lib.rs:513 | Wrap inner layer |
| fn | `DiagnosticExport::from_sink` | lib.rs:636 | Build export |
| fn | `DiagnosticExport::with_android_audio` | lib.rs:655 | Attach Android audio YAML |
| fn | `DiagnosticExport::with_network_info` | lib.rs:661 | Attach network info |
| fn | `DiagnosticExport::with_protocol_events` | lib.rs:667 | Attach protocol events |
| fn | `DiagnosticExport::to_text` | lib.rs:674 | Render as plaintext |
| fn | `ProtocolEventRecorder::new` | lib.rs:721 | Create recorder |
| fn | `ProtocolEventRecorder::record_connected` | lib.rs:740 | Record connection |
| fn | `ProtocolEventRecorder::record_disconnected` | lib.rs:745 | Record disconnect |
| fn | `ProtocolEventRecorder::record_reconnecting` | lib.rs:750 | Record reconnect |
| fn | `ProtocolEventRecorder::record_snapshot_changed` | lib.rs:759 | Record snapshot change |
| fn | `ProtocolEventRecorder::record_channel_join` | lib.rs:768 | Record channel join |
| fn | `ProtocolEventRecorder::record_lifecycle` | lib.rs:777 | Record lifecycle |
| fn | `ProtocolEventRecorder::drain` | lib.rs:782 | Drain all events |
| fn | `ProtocolEventRecorder::snapshot` | lib.rs:787 | Snapshot events |
**Dead code:** None found.
---
### 10. `chanora_core` — Top-Level Orchestration
Integration point composing all subsystems behind a stable API.
| Kind | Name | File:Line | Purpose |
|------|------|-----------|---------|
| struct | `ChanoraSession` | lib.rs:191 | Process-wide session handle |
| enum | `CoreError` | lib.rs:84 | Top-level errors (12 variants) |
| struct | `PttDescriptorSnapshot` | events.rs:6 | PTT descriptor snapshot |
| struct | `PersistedPttBinding` | events.rs:27 | Persisted PTT binding |
| struct | `PttController` | ptt.rs:68 | PTT controller |
| struct | `FileTransferService` | file_transfer.rs:39 | File transfer service |
| enum | `SessionEvent` | events.rs:49 | Session lifecycle events |
| enum | `VoiceJoinSyncState` | events.rs:239 | Voice join sync state |
| enum | `VoiceJoinErrorCode` | events.rs:250 | Voice join error codes |
| enum | `NetworkState` | events.rs:280 | Network connectivity state |
| enum | `FileTransferError` | file_transfer.rs:17 | File transfer errors |
| enum | `PttControllerError` | ptt.rs:35 | PTT controller errors |
| fn | `ChanoraSession::new` | lib.rs:245 | Create session |
| fn | `ChanoraSession::subscribe_events` | lib.rs:478 | Subscribe to session events |
| fn | `ChanoraSession::set_network_state` | lib.rs:460 | Set network state |
| fn | `ChanoraSession::network_state` | lib.rs:469 | Get network state |
| fn | `ChanoraSession::transmit_mode` | lib.rs:1568 | Get transmit mode |
| fn | `ChanoraSession::hard_mute` | lib.rs:1591 | Get hard mute state |
| fn | `ChanoraSession::release_tail_ms` | lib.rs:1645 | Get release tail |
| fn | `ChanoraSession::transmit_selector` | lib.rs:1652 | Get transmit selector |
| fn | `ChanoraSession::release_tail_timer` | lib.rs:1659 | Get release tail timer |
| fn | `ChanoraSession::audio_processing_stats_if_ready` | lib.rs:1205 | Get audio stats |
| fn | `PttController::new` | ptt.rs:103 | Create PTT controller |
| fn | `PttController::current_capability` | ptt.rs:226 | Get PTT capability |
| fn | `PttController::subscribe_capability` | ptt.rs:233 | Subscribe to capability |
| fn | `PttController::descriptor_watch` | ptt.rs:250 | Watch PTT descriptor |
| fn | `PttController::press_gate` | ptt.rs:257 | Get press gate |
| fn | `PttController::release_tail` | ptt.rs:263 | Get release tail |
**Dead code:** None found. All items consumed by `chanora_bridge`.
---
## Dart Files (apps/chanora_flutter/lib/)
### Widgets (31 files)
| Class | File:Line | Purpose |
|-------|-----------|---------|
| `AppSnackBar` | widgets/app_snack_bar.dart:9 | Snackbar notifications |
| `AppSnackBarVariant` | widgets/app_snack_bar.dart:6 | Neutral/success/warning/error |
| `AudioDebugStatsPanel` | widgets/audio_debug_stats_panel.dart:22 | Audio stats debug panel |
| `AudioDeviceListTile` | widgets/audio_device_list_tile.dart:21 | Audio device list item |
| `AudioDeviceKind` | widgets/audio_device_list_tile.dart:12 | Input/Output device kind |
| `AudioOutputTile` | widgets/audio_output_tile.dart:15 | Audio output route picker |
| `AudioProcessingConfigState` | widgets/audio_processing_config_state.dart:34 | Processing config state |
| `BbCodeText` | widgets/bbcode_text.dart:47 | BBCode renderer |
| `ChatPanel` | widgets/chat_panel.dart:13 | Chat panel container |
| `ChatEntry` | widgets/chat_views.dart:25 | Chat message entry |
| `ChatClientGroups` | widgets/chat_views.dart:374 | Client grouping for chat |
| `ChatPage` | widgets/chat_views.dart:548 | Full chat page |
| `ChatDetailView` | widgets/chat_views.dart:1070 | Chat detail view |
| `ClientInfoSheet` | widgets/client_info_sheet.dart:7 | Client info bottom sheet |
| `ConnectForm` | widgets/connect_widgets.dart:9 | Server connect form |
| `BookmarkList` | widgets/connect_widgets.dart:154 | Bookmark list |
| `CapturedBinding` | widgets/input_dialogs.dart:50 | PTT binding capture result |
| `BookmarkNameDialog` | widgets/input_dialogs.dart:61 | Bookmark name input |
| `ChannelPasswordDialog` | widgets/input_dialogs.dart:109 | Channel password input |
| `PttBindingCaptureDialog` | widgets/input_dialogs.dart:154 | PTT key binding dialog |
| `PermissionStateBanner` | widgets/permission_state_banner.dart:28 | Permission state banner |
| `PokeNotificationSettingsDialog` | widgets/poke_notification_settings.dart:7 | Poke notification settings |
| `PttCapabilityBadge` | widgets/ptt_capability_badge.dart:14 | PTT capability badge |
| `SnapshotView` | widgets/snapshot_view.dart:14 | Server tree view |
| `TalkPowerWarning` | widgets/talk_power_warning.dart:14 | Talk power warning |
| `VoiceBar` | widgets/voice_bar.dart:18 | Voice status bar |
| `VoiceStatusChip` | widgets/voice_compact.dart:46 | Voice status chip |
| `VoicePttButton` | widgets/voice_compact.dart:255 | PTT button widget |
| `VoiceLevelMeter` | widgets/voice_level_meter.dart:11 | Voice level meter |
| `VoiceSettingsDialog` | widgets/voice_settings.dart:61 | Voice settings dialog |
| `VoiceSettingsResult` | widgets/voice_settings.dart:46 | Settings result |
| `VoiceStatusSummary` | widgets/voice_status_summary.dart:5 | Voice status summary |
| `VoiceSubHeader` | widgets/voice_settings_controls.dart:117 | Voice sub-header |
| `VoiceSectionHeader` | widgets/voice_settings_controls.dart:141 | Voice section header |
| `AudioProcessingToggleRow` | widgets/voice_settings_controls.dart:159 | Processing toggle row |
### Services (20 files)
| Class/Function | File:Line | Purpose |
|----------------|-----------|---------|
| `AndroidAudioOutputDevice` | services/android_audio_output_devices.dart:1 | Android audio device |
| `AndroidPermissionsService` | services/android_permissions_service.dart:34 | Android permission handler |
| `AppBootstrap` | services/app_bootstrap.dart:95 | App bootstrap helpers |
| `AudioLifecycleService` | services/audio_lifecycle_service.dart:46 | Audio lifecycle wiring |
| `BackIntentPolicy` | services/back_intent_policy.dart | Back intent policy |
| `BackIntentService` | services/back_intent_service.dart:97 | Back intent handler |
| `ChannelJoinErrorMapper` | services/channel_join_error_mapper.dart:5 | Error message mapper |
| `ChannelSpacer` | services/channel_spacer.dart:110 | Spacer channel detection |
| `ConnectionPhaseState` | services/connection_phase_state.dart:38 | Connection phase state |
| `HardMuteOwners` | services/hard_mute_owners.dart | Hard mute owners |
| `IosAudioSessionController` | services/ios_audio_session_controller.dart | iOS audio session |
| `IosPermissionsService` | services/ios_permissions_service.dart:66 | iOS permission handler |
| `LinkTrustService` | services/link_trust_service.dart:29 | Link trust checker |
| `MacosPermissionsService` | services/macos_permissions_service.dart:273 | macOS permission handler |
| `PokePreferencesService` | services/poke_preferences_service.dart:44 | Poke mute preferences |
| `PokeNotificationService` | services/poke_notification_service.dart | Poke notification handler |
| `PrefetchDebouncer` | services/prefetch_debouncer.dart:15 | DNS prefetch debouncer |
| `OwnClientSnapshotState` | services/snapshot_state_mapper.dart:3 | Snapshot→state mapper |
| `ownClientSnapshotState()` | services/snapshot_state_mapper.dart:23 | Build snapshot state |
| `snapshotChannelName()` | services/snapshot_state_mapper.dart:43 | Get channel name from snapshot |
| `snapshotNeededTalkPower()` | services/snapshot_state_mapper.dart:48 | Get required talk power |
| `Ts3ServerLink` | services/ts3_server_link.dart:83 | TS3 server link parser |
| `UiPreferencesService` | services/ui_preferences_service.dart | UI preferences |
| `VoiceJoinOrdering` | services/voice_join_ordering.dart | Voice join ordering |
### Design (4 files)
| Class | File:Line | Purpose |
|-------|-----------|---------|
| `ChanoraTokens` | design/chanora_tokens.dart | Design tokens |
| `Breakpoints` | design/breakpoints.dart | Responsive breakpoints |
| `ViewportInfo` | design/viewport_info.dart | Viewport info |
| `PlatformCapabilities` | design/platform_capabilities.dart | Platform capabilities |
---
## Dead Code Analysis
### Confirmed Dead Code
None found. All public items are consumed by downstream crates or are intentionally platform-gated.
### Platform-Gated (Intentional)
- `AndroidVoiceUnit`, `IosVoiceUnit` — only compiled on target platforms
- `chanora_android_*` JNI functions — Android only
- `ios_voice_unit.rs`, `android_voice_unit.rs` — platform-specific
- `sdl_output.rs` — Linux only
### TODO/FIXME Items (15 total)
| File | Line | Note |
|------|------|------|
| `chanora_audio/src/audio_event_queue.rs` | 27 | Wire to client disconnect path |
| `chanora_audio/src/engine.rs` | 2348 | Realtime audio callback concern |
| `chanora_audio/src/mobile_voice_backend.rs` | 16 | Back-fill IosVoiceUnit to trait |
| `audio_lifecycle_service.dart` | 151 | Wire macOS default device change |
| `audio_lifecycle_service.dart` | 156 | macOS device change no action yet |
| `poke_notification_service.dart` | 33,35,41,43,48,130,132,142,155,168 | Future EventSoundService (10 items) |
### Useless Code
- No empty impls found
- No commented-out function bodies found
- No dead trait implementations found
---
## Architecture Notes
- **Boundary discipline**: `tsclientlib` types never cross `chanora_protocol` boundary (SAD-067)
- **Single connection**: DEC-006 enforces one connection at runtime
- **Secret isolation**: `chanora_storage` never stores secrets in plaintext DB
- **Deterministic reducers**: `chanora_state` reducers are pure functions (SRS-056)
- **PTT privacy**: Raw key codes never appear in logs or diagnostics (DEC-027)
- **Audio pipeline**: 48kHz mono, 20ms Opus frames, 10ms processing frames
@@ -0,0 +1,252 @@
# Link Coverage Report
**Generated:** 2026-06-13
**Scope:** All `.md` files in repository root and `docs/` tree
## Summary
- Total links checked: 148
- Valid internal links: 12 (4 markdown links + 8 inline doc-path references)
- Broken internal links: 2
- Valid inline doc-path references: 94
- Broken inline doc-path references: 5
- Valid code references: 62
- Broken code references: 2
- External links (manual review): 48
- Cross-references (doc→doc in prose): 0 broken
---
## Broken Internal Links
Markdown `[text](path)` style links that resolve to missing files.
| File | Line | Link Text | Target | Issue |
|------|------|-----------|--------|-------|
| README.md | 428 | `LICENSE-APACHE` | `LICENSE-APACHE` | File does not exist at repo root |
| README.md | 431 | `LICENSE-MIT` | `LICENSE-MIT` | File does not exist at repo root |
**Impact:** Users clicking the license links in the README will get a 404 on GitHub. These are referenced in the License section as the dual-license model files.
**Also affected by missing LICENSE files:**
| File | Line | Reference | Issue |
|------|------|-----------|-------|
| docs/security/license-inventory.md | 9 | `../../LICENSE-APACHE` | Resolves to missing `LICENSE-APACHE` at repo root |
| docs/security/license-inventory.md | 10 | `../../LICENSE-MIT` | Resolves to missing `LICENSE-MIT` at repo root |
| docs/security/flutter-license-inventory.md | 11 | `../../LICENSE-APACHE` | Resolves to missing `LICENSE-APACHE` at repo root |
| docs/security/flutter-license-inventory.md | 11 | `../../LICENSE-MIT` | Resolves to missing `LICENSE-MIT` at repo root |
---
## Valid Internal Links
| File | Line | Target |
|------|------|--------|
| README.md | 130 | `docs/architecture/desktop-ptt-architecture.md` |
| README.md | 436 | `docs/governance/product-decision-register.md` |
| README.md | 444 | `NOTICE` |
| docs/superpowers/specs/2026-06-05-adaptive-3-panel-layout-design.md | 266 | `../ui-ux/adaptive-layout-platform-guide.md` |
---
## Inline Doc-Path References
References to documentation files using backtick-quoted paths (not markdown links).
### Valid
| File | Line | Reference |
|------|------|-----------|
| CONTRIBUTING.md | 25 | `docs/governance/git-commit-message-convention.md` |
| README.md | 144 | `docs/governance/product-decision-register.md` |
| README.md | 261271 | `docs/requirements/sysrs.md`, `docs/requirements/srs.md`, `docs/architecture/sysdes.md`, `docs/architecture/sad.md`, `docs/architecture/sdd.md`, `docs/verification/verification-master-plan.md`, `docs/release/release-readiness-go-nogo-record.md`, `docs/release/platform-release-policy.md`, `docs/governance/product-decision-register.md`, `docs/governance/traceability-matrix.md`, `docs/security/security-privacy-legal-guideline.md` |
| README.md | 312 | `docs/release/release-readiness-go-nogo-record.md` |
| README.md | 349355 | `docs/security/threat-model.md`, `docs/security/secure-storage-audit-report.md`, `docs/security/diagnostic-redaction-audit-report.md`, `docs/security/dependency-and-supply-chain-report.md`, `docs/privacy/privacy-policy.md`, `docs/legal/trademark-and-attribution-review.md` |
| README.md | 391 | `docs/governance/git-commit-message-convention.md` |
| README.md | 449451 | `docs/governance/product-decision-register.md`, `docs/security/dependency-and-supply-chain-report.md`, `docs/legal/trademark-and-attribution-review.md` |
| docs/architecture/sad.md | 6 | `docs/srs.md` |
| docs/architecture/sad.md | 7 | `docs/sysdes.md` |
| docs/architecture/sad.md | 176 | `docs/governance/traceability-matrix.md` |
| docs/architecture/sdd.md | 6 | `docs/architecture/sad.md` |
| docs/architecture/sdd.md | 7 | `docs/srs.md` |
| docs/architecture/sysdes.md | 6 | `docs/sysdes.md` (canonical pointer) |
| docs/architecture/desktop-ptt-architecture.md | 5 | `docs/architecture/sad.md`, `docs/architecture/sdd.md`, `docs/release/dv-waiver-register.md` |
| docs/architecture/file-transfer-design.md | 6 | `docs/architecture/sad.md` |
| docs/architecture/file-transfer-research.md | 5 | `docs/architecture/file-transfer-design.md` |
| docs/architecture/file-transfer-implementation-plan.md | 6 | `docs/architecture/file-transfer-design.md`, `docs/architecture/file-transfer-research.md` |
| docs/requirements/sysrs.md | 4 | `../sysrs.md` (canonical pointer) |
| docs/requirements/srs.md | 4 | `../srs.md` (canonical pointer) |
| docs/governance/document-index.md | 1432 | All listed document paths |
| docs/material3-guideline.md | 10 | `docs/ui-ux/material3-guideline.md` (self-referencing path record) |
| docs/ui-ux/material3-guideline.md | 6 | `docs/material3-guideline.md` (canonical pointer) |
| docs/superpowers/specs/2026-06-08-maintainability-continuation-design.md | 125141 | Multiple `docs/` paths |
| docs/superpowers/specs/2026-05-29-state-sync-ui-settings-validation-design.md | 5155 | Multiple `docs/` paths |
| docs/superpowers/plans/2026-05-29-finish-dv-document-tree.md | 1654 | Multiple `docs/` paths |
| docs/superpowers/plans/2026-05-29-swe2-swe3-baselines.md | 1635 | Multiple `docs/` paths |
| docs/superpowers/plans/2026-05-29-state-sync-ui-settings-validation.md | 8488 | Multiple `docs/` paths |
| docs/superpowers/plans/2026-05-29-dv-evidence-pack.md | 1654 | Multiple `docs/` paths |
| docs/superpowers/plans/2026-05-28-server-resolution-prefetch.md | 31836 | Multiple source file paths |
| docs/superpowers/plans/2026-05-28-chanora-server-prefetch-crate.md | 15541 | Multiple source file paths |
| docs/superpowers/plans/2026-06-06-chat-panel-switching.md | 58507 | Multiple source file paths |
| docs/superpowers/plans/2026-06-08-core-internal-split.md | 1691 | Multiple source file paths |
### Broken
| File | Line | Reference | Issue |
|------|------|-----------|-------|
| docs/sysrs.md | 126 | `docs/chanora_SysDes.md` | Does not exist (listed as "potential downstream file name") |
| docs/sysrs.md | 127 | `docs/chanora_SRS.md` | Does not exist (listed as "potential downstream file name") |
| docs/sysrs.md | 128 | `docs/chanora_SAD.md` | Does not exist (listed as "potential downstream file name") |
| docs/sysrs.md | 129 | `docs/chanora_SDD.md` | Does not exist (listed as "potential downstream file name") |
| docs/sysrs.md | 130 | `docs/chanora_Verification.md` | Does not exist (listed as "potential downstream file name") |
**Note:** These five are documented as "Potential downstream file names" in a table and are aspirational/historical. They are presented as code blocks in the original, so they function as suggestions rather than navigable links. Low severity.
---
## Code References
### Valid
| File | Line | Reference | Found At |
|------|------|-----------|----------|
| docs/architecture/sad.md | 41 | `apps/chanora_flutter/lib/main.dart` | EXISTS |
| docs/architecture/sad.md | 42 | `apps/chanora_flutter/lib/services/` | EXISTS |
| docs/architecture/sad.md | 43 | `apps/chanora_flutter/lib/widgets/` | EXISTS |
| docs/architecture/sad.md | 44 | `crates/chanora_bridge`, `apps/chanora_flutter/lib/src/rust/` | EXISTS |
| docs/architecture/sad.md | 45 | `core/chanora_core` | EXISTS |
| docs/architecture/sad.md | 46 | `crates/chanora_protocol` | EXISTS |
| docs/architecture/sad.md | 47 | `crates/chanora_state` | EXISTS |
| docs/architecture/sad.md | 48 | `crates/chanora_audio` | EXISTS |
| docs/architecture/sad.md | 49 | `crates/chanora_storage` | EXISTS |
| docs/architecture/sad.md | 50 | `crates/chanora_diagnostics` | EXISTS |
| docs/architecture/sad.md | 51 | `crates/chanora_resolver` | EXISTS |
| docs/architecture/sad.md | 52 | `crates/chanora_prefetch`, Flutter `prefetch_debouncer.dart` | EXISTS |
| docs/architecture/sdd.md | 17 | `apps/chanora_flutter/lib/services/app_bootstrap.dart`, `main.dart` | EXISTS |
| docs/architecture/sdd.md | 18 | `apps/chanora_flutter/lib/widgets/connect_widgets.dart` | EXISTS |
| docs/architecture/sdd.md | 19 | `snapshot_view.dart`, `snapshot_state_mapper.dart`, `channel_spacer.dart` | EXISTS (in services/) |
| docs/architecture/sdd.md | 20 | `chat_views.dart`, `bbcode_text.dart` | EXISTS |
| docs/architecture/sdd.md | 21 | `voice_bar.dart`, `voice_compact.dart`, `voice_settings*.dart`, `voice_level_meter.dart`, `ptt_capability_badge.dart` | EXISTS |
| docs/architecture/sdd.md | 22 | `android_permissions_service.dart`, `ios_permissions_service.dart`, `audio_lifecycle_service.dart`, `back_intent_*`, `link_trust_service.dart` | EXISTS |
| docs/architecture/sdd.md | 23 | `crates/chanora_bridge/src/api.rs` | EXISTS |
| docs/architecture/sdd.md | 24 | `core/chanora_core/src/lib.rs`, `events.rs`, `network_diagnostics.rs`, `ptt.rs` | EXISTS |
| docs/architecture/sdd.md | 25 | `crates/chanora_protocol/src/` | EXISTS |
| docs/architecture/sdd.md | 26 | `crates/chanora_state/src/lib.rs`, `channel_join.rs` | EXISTS |
| docs/architecture/sdd.md | 27 | `crates/chanora_audio/src/` | EXISTS |
| docs/architecture/sdd.md | 28 | `crates/chanora_storage/src/lib.rs` | EXISTS |
| docs/architecture/sdd.md | 29 | `crates/chanora_diagnostics/src/lib.rs` | EXISTS |
| docs/architecture/sdd.md | 30 | `crates/chanora_resolver/src/lib.rs`, `crates/chanora_prefetch/src/lib.rs`, `prefetch_debouncer.dart` | EXISTS |
| docs/architecture/sdd.md | 31 | `.github/workflows/`, `tools/` | EXISTS |
| docs/architecture/sdd.md | 35 | `crates/chanora_bridge/src/api.rs`, `apps/chanora_flutter/lib/src/rust/` | EXISTS |
| docs/release/release-readiness-go-nogo-record.md | 26 | `apps/chanora_flutter/pubspec.yaml` | EXISTS |
| docs/implementation-status-2026-05-28.md | 69 | `apps/chanora_flutter/ios/Runner/AppDelegate.swift` | EXISTS |
| docs/sysrs.md | 503 | `apps/chanora_flutter/ios/Runner/AppDelegate.swift` | EXISTS |
| docs/sysrs.md | 1962 | `apps/chanora_flutter/macos/chanora_bridge.podspec` | EXISTS |
| README.md | 236249 | `apps/chanora_flutter/`, `core/chanora_core/`, `crates/chanora_protocol/`, `crates/chanora_audio/`, `crates/chanora_state/`, `crates/chanora_storage/`, `crates/chanora_diagnostics/`, `crates/chanora_bridge/` | EXISTS |
### Broken
| File | Line | Reference | Issue |
|------|------|-----------|-------|
| docs/architecture/sdd.md | 19 | `snapshot_state_mapper.dart` (listed under "Snapshot and channel UI" widgets) | File is in `apps/chanora_flutter/lib/services/`, not `apps/chanora_flutter/lib/widgets/` — directory mismatch |
| docs/architecture/sdd.md | 21 | `voice_settings*.dart` (listed under Voice UI widgets) | Files are `voice_settings.dart` and `voice_settings_controls.dart` in `widgets/` — EXISTS but glob reference is ambiguous (two files match) |
**Note:** The `snapshot_state_mapper.dart` directory mismatch is a minor documentation inaccuracy — the file exists but is listed under the wrong component section (widget layer vs service layer).
---
## External Links (Manual Review)
These URLs should be checked manually for validity.
| File | Line | URL |
|------|------|-----|
| README.md | 429 | `https://www.apache.org/licenses/LICENSE-2.0` |
| README.md | 432 | `https://opensource.org/licenses/MIT` |
| apps/chanora_flutter/README.md | 11 | `https://docs.flutter.dev/get-started/learn-flutter` |
| apps/chanora_flutter/README.md | 12 | `https://docs.flutter.dev/get-started/codelab` |
| apps/chanora_flutter/README.md | 13 | `https://docs.flutter.dev/reference/learning-resources` |
| apps/chanora_flutter/README.md | 16 | `https://docs.flutter.dev/` |
| silero-coreml/README.md | 343 | `https://apple.github.io/coremltools/docs-guides/source/introductory-quickstart.html` |
| silero-coreml/README.md | 350 | `https://apple.github.io/coremltools/docs-guides/source/convert-pytorch.html` |
| silero-coreml/Docs/CoreMLConversion.md | 244 | `https://apple.github.io/coremltools/docs-guides/source/convert-pytorch.html` |
| silero-coreml/Docs/CoreMLConversion.md | 252 | `https://apple.github.io/coremltools/docs-guides/source/introductory-quickstart.html` |
| docs/security/dependency-and-supply-chain-report.md | 29 | `https://github.com/EdisonJwa/oboe-rs` |
| docs/superpowers/specs/2026-06-05-adaptive-3-panel-layout-design.md | 261 | `https://github.com/asportnoy/compact-discord` |
| docs/superpowers/specs/2026-06-05-adaptive-3-panel-layout-design.md | 262 | `https://github.com/mattermost/mattermost/blob/...` |
| docs/superpowers/specs/2026-06-05-adaptive-3-panel-layout-design.md | 263 | `https://github.com/RocketChat/fuselage/blob/...` |
| docs/superpowers/specs/2026-06-05-adaptive-3-panel-layout-design.md | 264 | `https://github.com/flutter/flutter/issues/162965` |
| docs/superpowers/specs/2026-06-05-adaptive-3-panel-layout-design.md | 265 | `https://m3.material.io/foundations/layout/breakpoints/overview` |
| docs/architecture/file-transfer-research.md | 29 | `https://github.com/Splamy/TS3AudioBot/blob/...` |
| docs/architecture/file-transfer-research.md | 30 | `https://github.com/Multivit4min/TS3-NodeJS-Library/blob/...` |
| docs/architecture/file-transfer-research.md | 31 | `https://github.com/planetteamspeak/ts3phpframework/blob/...` |
| docs/architecture/file-transfer-research.md | 49 | `https://github.com/Multivit4min/TS3-NodeJS-Library/blob/...` |
| docs/architecture/file-transfer-research.md | 50 | `https://github.com/planetteamspeak/ts3phpframework/blob/...` |
| docs/architecture/file-transfer-research.md | 123 | `https://github.com/ReSpeak/Qint/blob/...` |
| docs/architecture/file-transfer-research.md | 124 | `https://github.com/ReSpeak/Qint/blob/...` |
| docs/architecture/file-transfer-research.md | 125 | `https://github.com/ReSpeak/Qint/blob/...` |
| docs/architecture/file-transfer-research.md | 140 | `https://github.com/teamspeak/ts3client-pluginsdk/blob/...` |
| docs/architecture/file-transfer-research.md | 141 | `https://github.com/teamspeak/ts3client-pluginsdk/blob/...` |
| docs/architecture/file-transfer-research.md | 142 | `https://community.teamspeak.com/t/clear-cache/41511` |
| docs/architecture/file-transfer-research.md | 142 | `https://community.teamspeak.com/t/server-icons-are-displaying-a-broken-image-issues-with-local-cache/58680` |
| docs/architecture/file-transfer-research.md | 208 | `https://github.com/Splamy/TS3AudioBot/blob/...` |
| docs/architecture/file-transfer-research.md | 209 | `https://github.com/Splamy/TS3AudioBot/blob/...` |
| docs/architecture/file-transfer-research.md | 220 | `https://github.com/Multivit4min/TS3-NodeJS-Library/blob/...` |
| docs/architecture/file-transfer-research.md | 221 | `https://github.com/Multivit4min/TS3-NodeJS-Library/blob/...` |
| docs/architecture/file-transfer-research.md | 320 | `https://github.com/rust-lang/rust/blob/...` |
| docs/architecture/file-transfer-research.md | 321 | `https://source.android.com/docs/core/storage/scoped` |
| docs/architecture/file-transfer-research.md | 322 | `https://developer.apple.com/library/archive/documentation/FileManagement/...` |
| docs/architecture/file-transfer-research.md | 334 | `https://github.com/zkat/cacache-rs/blob/...` |
| docs/architecture/file-transfer-research.md | 335 | `https://github.com/zkat/cacache-rs/blob/...` |
| docs/architecture/file-transfer-research.md | 336 | `https://github.com/zkat/cacache-rs/blob/...` |
| docs/architecture/file-transfer-research.md | 345 | `https://pub.dev/packages/flutter_cache_manager` |
| docs/architecture/file-transfer-research.md | 346 | `https://pub.dev/packages/super_cache_disk/versions/1.0.0` |
| docs/architecture/file-transfer-research.md | 406 | `https://git.did.science/TeaSpeak/Server/Server` |
| docs/security/license-inventory.md | 3180 | ~50 URLs to GitHub repos for dependency licenses |
| docs/security/flutter-license-inventory.md | 6244209 | Multiple `http://www.apache.org/licenses/` and `http://mozilla.org/MPL/2.0/` (in license text bodies) |
---
## Cross-References
### Valid
All doc-to-doc cross-references found in prose text resolve to existing files. Key verified chains:
| Source | Reference | Target Exists |
|--------|-----------|---------------|
| docs/architecture/sad.md:6 | `docs/srs.md` | YES |
| docs/architecture/sad.md:7 | `docs/sysdes.md` | YES |
| docs/architecture/sdd.md:6 | `docs/architecture/sad.md` | YES |
| docs/architecture/sdd.md:7 | `docs/srs.md` | YES |
| docs/architecture/sysdes.md:6 | `docs/sysdes.md` | YES |
| docs/architecture/file-transfer-design.md:6 | `docs/architecture/sad.md` | YES |
| docs/architecture/file-transfer-research.md:5 | `docs/architecture/file-transfer-design.md` | YES |
| docs/architecture/file-transfer-implementation-plan.md:6 | `docs/architecture/file-transfer-design.md` | YES |
| docs/architecture/file-transfer-implementation-plan.md:6 | `docs/architecture/file-transfer-research.md` | YES |
| docs/architecture/desktop-ptt-architecture.md:5 | `docs/architecture/sad.md` | YES |
| docs/architecture/desktop-ptt-architecture.md:5 | `docs/architecture/sdd.md` | YES |
| docs/architecture/desktop-ptt-architecture.md:5 | `docs/release/dv-waiver-register.md` | YES |
| docs/requirements/sysrs.md:4 | `../sysrs.md` | YES |
| docs/requirements/srs.md:4 | `../srs.md` | YES |
| docs/governance/document-index.md | All 18 listed paths | YES |
| docs/ui-ux/material3-guideline.md:6 | `docs/material3-guideline.md` | YES |
### Broken
None found — all doc-to-doc cross-references in prose text resolve correctly.
---
## Notes
1. **Path-record files**: Several docs exist as stubs pointing to canonical locations (`docs/requirements/sysrs.md``docs/sysrs.md`, `docs/requirements/srs.md``docs/srs.md`, `docs/architecture/sysdes.md``docs/sysdes.md`, `docs/ui-ux/material3-guideline.md``docs/material3-guideline.md`). These are intentional DV navigation aids, not broken links.
2. **Hypothetical file names in sysrs.md**: The table at lines 124131 lists `docs/chanora_SysDes.md` etc. as "Potential downstream file names." These are aspirational names from an earlier draft, not current files. They are presented as plain text in a table, not as navigable links.
3. **LICENSE-APACHE and LICENSE-MIT**: These are the most impactful broken references. The README's License section links to them, and the security license inventory files reference them. The dual-license model (DEC-020) requires these files to exist for proper attribution.
4. **snapshot_state_mapper.dart location**: The SDD lists this under "Snapshot and channel UI" (widget layer), but the file is actually in `services/`. This is a minor organizational mismatch — the file exists but is categorized differently than documented.
5. **External links**: Concentrated in `docs/architecture/file-transfer-research.md` (protocol research sources) and `docs/security/license-inventory.md` (dependency homepages). The file-transfer research links point to specific GitHub commit SHAs which may become stale over time.
@@ -0,0 +1,135 @@
# Review: Test & Document Coverage Analysis
**Reviewer:** opencode (automated)
**Reviewed file:** `docs/offline-knowledge/coverage-analysis.md`
**Date:** 2026-06-13
**Method:** Spot-checked 5 random test files, verified aggregate counts via grep/find, cross-referenced directory listings
---
## Verdict: Significant inaccuracies found
The document has **3 critical counting errors**, **2 factual errors about file existence**, and **several minor issues**. The per-file Rust test counts are mostly accurate, but the aggregate totals are wrong.
---
## Critical Errors
### 1. Total Rust test count is wrong by 40%
| Metric | Document | Actual | Delta |
|--------|----------|--------|-------|
| Inline `#[test]` | 220 | 309 | +89 |
| Integration tests | 2 | 2 | 0 |
| **Total** | **222** | **312** | **+90** |
The per-crate sums also don't reconcile: the document's own per-file tables sum to ~202 for chanora_audio (plus 2 integration = 204), but `grep -c '#\[test\]'` across `crates/chanora_audio/src/` yields **219** inline tests (+ 2 integration = 221). The document undercounts chanora_audio by 17 tests.
### 2. chanora_resolver test count off by 1
| Crate | Document | Actual |
|-------|----------|--------|
| chanora_resolver | 12 | 13 |
The extra test is in `examples/cli.rs` (documented separately as 1 example test, but the crate header total should be 13, not 12).
### 3. Doc file count is ambiguous and inaccurate
| Scope | Document says | Actual |
|-------|---------------|--------|
| All docs/ .md files | 55 | 75 |
| Excluding superpowers/ | — | 66 |
| Excluding superpowers/ + offline-knowledge/ | — | 51 |
The "55" figure doesn't match any reasonable scope calculation. The document also doesn't clarify whether superpowers/ plans/specs are included.
---
## Factual Errors
### 4. `poke_active_chat.dart` does not exist as a source file
The document lists `poke_active_chat.dart` as a tested service (line 163), and `poke_active_chat_test.dart` does exist under `test/services/`. However, **no corresponding source file** exists in `lib/services/`. This is either:
- An orphaned test for a deleted/moved source file, or
- The source file is located elsewhere (not in `lib/services/`)
The document should flag this as an anomaly, not list it as "Tested".
### 5. `audio_device_list_tile_test.dart` exists but is not counted
The document marks `audio_device_list_tile.dart` as "UNTESTED" (line 189), but `apps/chanora_flutter/test/widgets/audio_device_list_tile_test.dart` **does exist**. This means:
- Widget test file count should be **14**, not 13
- Widget coverage should be **14/24 (58%)**, not 13/24 (54%)
---
## Section Header vs. Content Mismatches
### 6. Architecture section: header says "4 files", lists 7
The header on line 221 reads "Architecture (4 files)" but the table contains 7 entries. The actual `docs/architecture/` directory has 7 files.
### 7. Governance section: header says "11 files", lists 12
The header on line 264 reads "Governance (11 files)" but the table contains 12 entries. The actual `docs/governance/` directory has 12 files.
---
## Spot-Check Results (5 Random Test Files)
| File | Document Count | Actual | Match? |
|------|---------------|--------|--------|
| `chanora_audio/src/ptt_backends/windows.rs` | 44 | 44 | ✅ |
| `chanora_audio/src/engine.rs` | 7 | 7 | ✅ |
| `chanora_state/src/lib.rs` | 18 | 18 | ✅ |
| `chanora_storage/src/lib.rs` | 15 | 15 | ✅ |
| `chanora_audio/src/route_policy.rs` | 8 | 8 | ✅ |
Per-file Rust test counts are **accurate**. The error is in the aggregation.
---
## Dart/Flutter Section: Mostly Accurate
| Metric | Document | Actual | Match? |
|--------|----------|--------|--------|
| `test()` calls | 155 | 155 | ✅ |
| `testWidgets()` calls | 66 | 66 | ✅ |
| Total Dart tests | 221 | 221 | ✅ |
| Service source files | 21 | 21 | ✅ |
| Widget source files | 24 | 24 | ✅ |
| Service test files | — | 20 | ⚠️ Not stated |
| Widget test files | 13 | 14 | ❌ |
---
## Missing Crates / Scope Issues
The document covers all 9 crates under `crates/` plus `chanora_core` under `core/`. No crates are missing. However:
- The document doesn't clearly explain that `chanora_core` lives under `core/`, not `crates/`
- The "Crates with tests: 7/9" metric (line 15) excludes `chanora_core`, which has 11 tests. If counted, it should be **8/10**
---
## Documentation Gap Analysis: Mostly Complete
The gap analysis (lines 333-354) correctly identifies undocumented modules. One omission:
- **Flutter test infrastructure** — no doc for the test helper setup, mock patterns, or test utilities used across 37 test files
---
## Summary of Required Corrections
| # | Issue | Severity | Fix |
|---|-------|----------|-----|
| 1 | Total Rust tests: 220 → 312 | Critical | Re-count and update |
| 2 | chanora_audio tests: 204 → 221 | Critical | Re-count and update |
| 3 | chanora_resolver tests: 12 → 13 | Minor | Update count |
| 4 | Doc file count: 55 → clarify scope | Minor | State scope explicitly |
| 5 | `poke_active_chat.dart` doesn't exist | Critical | Remove or flag as anomaly |
| 6 | `audio_device_list_tile_test.dart` exists | Major | Update widget test count to 14 |
| 7 | Architecture header: 4 → 7 | Minor | Fix header |
| 8 | Governance header: 11 → 12 | Minor | Fix header |
| 9 | Widget coverage: 54% → 58% | Major | Recalculate |
@@ -0,0 +1,148 @@
# Review: coverage-analysis.md & doc-quality-analysis.md
**Reviewer:** opencode (automated verification)
**Date:** 2026-06-13
**Method:** Random sampling + targeted claim verification against actual codebase
---
## coverage-analysis.md Review
### Check 1: Random Test File Counts (5 files sampled)
| File | Claimed | Actual | Verdict |
|------|---------|--------|---------|
| `android_permissions_service_test.dart` | 14 | 14 | ✅ PASS |
| `macos_permissions_service_test.dart` | 20 | 20 | ✅ PASS |
| `chat_views_test.dart` | 18 | 29 | ❌ FAIL (off by 11) |
| `back_intent_policy_test.dart` | 9 | 9 | ✅ PASS |
| `channel_spacer_test.dart` | 9 | 9 | ✅ PASS |
**Score:** 4/5 correct
### Check 2: Source Files Claimed Untested (3 files verified)
| File | Claimed | Actual | Verdict |
|------|---------|--------|---------|
| `ios_permissions_service.dart` | UNTESTED | No test file exists | ✅ PASS |
| `link_trust_service.dart` | UNTESTED | No test file exists | ✅ PASS |
| `audio_device_list_tile.dart` (widget) | UNTESTED | **Test file EXISTS** (`audio_device_list_tile_test.dart`, 3 tests) | ❌ FAIL |
**Score:** 2/3 correct
### Check 3: Orphaned Test Claim
- **Claim:** `poke_active_chat_test.dart` is orphaned (no matching source)
- **Actual:** `poke_active_chat_test.dart` EXISTS in test/services/, but `poke_active_chat.dart` does NOT exist in lib/services/
- **Verdict:** ✅ PASS — claim is accurate
### Check 4: Missed Test Files
| Missed Item | Impact |
|-------------|--------|
| `audio_device_list_tile_test.dart` | Widget test coverage is 14/24 (58%), not 13/24 (54%) |
| chanora_core integration tests (3 files: alpha_smoke.rs, avatar_cache.rs, mvp_storage.rs) | Analysis claims 2 integration tests total; actual is 6 (2 chanora_audio + 4 chanora_core) |
### Check 5: Aggregate Count Errors
| Metric | Claimed | Actual | Error |
|--------|---------|--------|-------|
| chanora_audio inline tests | 221 | 333 | +112 (51% undercount) |
| chanora_core tests (inline + integration) | 11 | 38 | +27 (71% undercount) |
| Total Dart tests | 221 | 233 | +12 (5% undercount) |
| Total doc files (docs/) | 55 | 86 | +31 (56% undercount) |
| Widget test files | 13 | 14 | +1 missed file |
| Total Rust integration tests | 2 | 6 | +4 missed |
### Check 6: Documentation Gap Claims
The documentation gap table (lines 336-354) lists 15 modules with no dedicated docs. Spot-checking confirms these modules确实 lack dedicated documentation files. **Verdict:** ✅ PASS — gaps are accurately identified.
---
## doc-quality-analysis.md Review
### Check 1: Claimed Duplications (3 verified)
| # | Claim | Files | Verdict |
|---|-------|-------|---------|
| 1 | Lifecycle chain (`SysRS -> SysDes -> SRS -> SAD -> SDD`) | README.md:280, CONTRIBUTING.md:10 | ✅ PASS — identical text confirmed |
| 2 | Commit examples | README.md:379-386, CONTRIBUTING.md:37-42, git-commit-message-convention.md:14-18 | ⚠️ PARTIAL — README has 6 examples, CONTRIBUTING has 4, convention file has 4. Not "identical" but overlapping. |
| 3 | Security doc list | README.md:349-355, SECURITY.md:33-38 | ✅ PASS — identical 6-file list confirmed |
### Check 2: Useless Content Items
| Claim | Verdict | Notes |
|-------|---------|-------|
| `docs/architecture/sysdes.md` is "path record — no unique content" | ⚠️ MISLEADING | It's a DV entry-point record with review summary table. Intentional for ASPICE compliance, not "useless." |
| `docs/requirements/sysrs.md` is "path record — no unique content" | ⚠️ MISLEADING | Same as above — intentional DV navigation aid. |
| `docs/requirements/srs.md` is "path record — no unique content" | ⚠️ MISLEADING | Same pattern. |
| `docs/ui-ux/material3-guideline.md` is "path record — no unique content" | ⚠️ MISLEADING | Same pattern. |
| `docs/sysdes.md:13` malformed markdown | ✅ PASS | Line 13: `**Repo path:** ... ---` missing blank line before `---`. Confirmed. |
### Check 3: Broken References
| Claim | Verdict |
|-------|---------|
| `docs/sysrs.md:126-130` references non-existent `docs/chanora_SysDes.md` etc. | ✅ PASS — confirmed. Actual files are `docs/sysdes.md`, `docs/srs.md`, etc. |
| `docs/implementation-status-2026-05-28.md:103` references `SDD-109` | ✅ PASS — SDD baseline explicitly notes SDD-109 is "not itemized in this baseline" |
| `docs/implementation-status-2026-05-28.md:105` references `SAD-043` | ✅ PASS — SAD baseline explicitly notes SAD-043 is "not itemized in this baseline" |
### Check 4: Additional Issues Missed
| Issue | Location | Description |
|-------|----------|-------------|
| chanora_core test count wildly wrong | coverage-analysis.md:107-113 | Claims 11 tests; actual is 34 inline + 4 integration = 38 |
| chanora_audio test count wrong | coverage-analysis.md:24 | Claims 221 inline tests; actual is 333 |
| Total doc count wrong | coverage-analysis.md:215 | Claims 55; actual is 86 under docs/ |
| Widget test file missed | coverage-analysis.md:188 | `audio_device_list_tile_test.dart` exists but listed as UNTESTED |
| `release(android)` commit type | doc-quality-analysis.md:90 | Analysis correctly flags this as non-standard Conventional Commits type, but doesn't note it appears in the canonical `git-commit-message-convention.md` itself |
---
## Summary of Errors
### coverage-analysis.md — Errors Found
1. **chanora_audio test count:** 221 claimed → 333 actual (112 test undercount)
2. **chanora_core test count:** 11 claimed → 38 actual (27 test undercount)
3. **Total Dart test count:** 221 claimed → 233 actual (12 test undercount)
4. **chat_views_test.dart count:** 18 claimed → 29 actual
5. **Widget test file count:** 13 claimed → 14 actual (missed audio_device_list_tile_test.dart)
6. **Total integration tests:** 2 claimed → 6 actual (missed chanora_core's 3 files / 4 tests)
7. **Total doc file count:** 55 claimed → 86 actual
### doc-quality-analysis.md — Errors Found
1. **"Useless content" characterization:** Path record files are intentional DV navigation aids, not useless. The label is misleading.
2. **Commit examples "identical" claim:** They overlap but are not identical (different files have different subsets).
---
## Quality Scores
| File | Score | Rationale |
|------|-------|-----------|
| **coverage-analysis.md** | **4/10** | Structure and methodology are sound, but 7 factual errors in counts undermine reliability. The chanora_audio undercount (112 tests) and chanora_core undercount (27 tests) are severe. Missed widget test file is a moderate error. |
| **doc-quality-analysis.md** | **7/10** | Duplications and broken references are accurately identified. The "useless content" label is misleading but not factually wrong. Minor inaccuracy on "identical" claim for commit examples. |
---
## Corrections Needed
### coverage-analysis.md
1. Update chanora_audio inline test count: 221 → 333
2. Update chanora_core test count: 11 → 38 (34 inline + 4 integration)
3. Update total Dart test count: 221 → 233
4. Update chat_views_test.dart count: 18 → 29
5. Add `audio_device_list_tile_test.dart` to widget test list (3 tests)
6. Update widget test file count: 13 → 14; untested widgets: 11 → 10
7. Update total integration tests: 2 → 6
8. Update total doc file count: 55 → 86
9. Add chanora_core integration test files to the integration tests section
### doc-quality-analysis.md
1. Relabel "Useless Content" → "Path Record Files" or "DV Navigation Aids" with explanation that these are intentional
2. Soften "identical" to "overlapping" for commit examples (Instance 2)
@@ -0,0 +1,176 @@
# Documentation Quality Analysis Review
**Reviewer:** Document Review Agent
**Date:** 2026-06-13
**Source:** `docs/offline-knowledge/doc-quality-analysis.md`
## Overall Assessment
The analysis is **largely accurate** but mischaracterizes several items. Most notably, it labels intentional ASPICE-compliance structures as "useless" and "duplicated" when they serve a documented purpose. The broken references finding is partially valid.
## Duplications: Spot-Check Results
### Instance 1: Lifecycle Chain — Justified Cross-Reference
**Verdict: NOT a problem.**
The lifecycle chain `SysRS -> SysDes -> SRS -> SAD -> SDD` appears in 6 files, but each serves a different purpose:
| File | Purpose |
|------|---------|
| `README.md:280` | Project overview for new contributors |
| `CONTRIBUTING.md:10` | Contributor guidance — must be self-contained |
| `docs/sysdes.md:90` | SysDes document context section |
| `docs/sysrs.md:108` | SysRS downstream relationship |
| `docs/governance/traceability-matrix.md:16` | Traceability rule definition |
| `docs/references/aspice-swe2-swe3-integration-note.md:12` | ASPICE integration reference |
ASPICE expects each document to be reviewable independently. Removing the chain from CONTRIBUTING.md or traceability-matrix.md would break document self-containment. **Recommendation: Keep as-is.**
### Instance 2: Git Commit Examples — Genuine Duplication
**Verdict: VALID.**
The commit examples are genuinely duplicated:
- `README.md:379-386` has 6 examples (including `docs(sad)` and `i18n(ui)`)
- `CONTRIBUTING.md:37-42` has 4 examples
- `docs/governance/git-commit-message-convention.md:14-19` has 4 examples
The README already references the convention file (line 391). The examples in README and CONTRIBUTING add no unique value. **Recommendation: Valid — consolidate to convention file.**
### Instance 3: Security Doc List — Genuine Duplication
**Verdict: VALID.**
The security document list is identical in both files:
- `README.md:349-355` — 6 file paths in a code block
- `SECURITY.md:33-38` — same 6 file paths in a code block
SECURITY.md is the authoritative source. The README could reference it instead. **Recommendation: Valid — keep in SECURITY.md, reference from README.**
## Useless Content: Verification Results
### Path Record Files — NOT Useless
**Verdict: INVALID. The analysis is wrong.**
The analysis labels these files as "useless" with "no unique content":
| File | Lines | Analysis Claim |
|------|-------|----------------|
| `docs/architecture/sysdes.md` | 21 | "Path record file — 21 lines pointing to `docs/sysdes.md`" |
| `docs/requirements/sysrs.md` | 22 | "Path record file — 22 lines pointing to `docs/sysrs.md`" |
| `docs/requirements/srs.md` | 22 | "Path record file — 22 lines pointing to `docs/srs.md`" |
| `docs/ui-ux/material3-guideline.md` | 8 | "Path record file — 8 lines pointing to `docs/material3-guideline.md`" |
These are **DV entry-point records** — intentional ASPICE compliance artifacts. Each file:
1. Preserves a README-advertised path for DV navigation
2. Provides a DV Review Summary table mapping topics to canonical source sections
3. States the DV position for that lifecycle layer
Example from `docs/requirements/sysrs.md`:
```
## DV Review Summary
| Topic | Canonical source |
|---|---|
| System scope and context | `docs/sysrs.md` sections 2 through 5 |
| Verification and validation requirements | `docs/sysrs.md` section 24 |
| MVP acceptance requirements | `docs/sysrs.md` section 25, SysRS-241 through SysRS-257 |
```
**These are not stubs.** They provide reviewer navigation aids. Deleting them would break DV traceability. **Recommendation: Keep all path record files.**
### Malformed Markdown — Valid
**Verdict: VALID.**
`docs/sysdes.md:13` has:
```
**Repo path:** `docs/architecture/sysdes.md` ---
```
Missing blank line before `---`. This renders as inline text instead of a horizontal rule. **Recommendation: Fix by adding a blank line.**
## Broken References: Verification Results
### `chanora_*` Filenames — Confirmed Broken
**Verdict: VALID.**
`docs/sysrs.md:126-130` suggests these filenames:
```
docs/chanora_SysDes.md
docs/chanora_SRS.md
docs/chanora_SAD.md
docs/chanora_SDD.md
docs/chanora_Verification.md
```
None of these files exist. The actual files use different names (`docs/sysdes.md`, `docs/srs.md`, etc.). This is a genuine broken reference. **Recommendation: Update the suggested filenames to match actual paths.**
### SDD-109 and SAD-043 — NOT Broken
**Verdict: INVALID. The analysis is wrong.**
The analysis claims these are broken references. However, the traceability matrix (`docs/governance/traceability-matrix.md:67`) explicitly documents this:
> "SAD and SDD are baseline candidates rather than fully item-numbered historical documents. Some prior references such as `SAD-043` and `SDD-109` are not reconstructed as itemized records. Treat the new SAD/SDD as DV baselines; add strict item IDs later if the process owner requires ID-level audit."
The SAD (`docs/architecture/sad.md:182`) and SDD (`docs/architecture/sdd.md:153`) also acknowledge this. These are **documented historical references**, not broken links. The implementation status file correctly notes them as "Referenced but not confirmed." **Recommendation: No action needed — this is intentional.**
## Additional Issues Found
### 1. Version Inconsistency Not Flagged
The analysis mentions version inconsistency in "Outdated Content" but doesn't flag it as a cross-document consistency issue:
- `docs/sysdes.md:6` — Version 0.9.8
- `docs/sysrs.md:5` — Version 0.9.11
- `docs/material3-guideline.md:4-5` — Version 0.9.2
These version numbers suggest independent evolution, but ASPICE expects version alignment across the lifecycle chain. **Recommendation: Add to high-priority recommendations.**
### 2. `release` Commit Type
`docs/governance/git-commit-message-convention.md:18` uses `release(android)` as an example, but `release` is not a standard Conventional Commits type. The analysis correctly flags this in "Stale Content" but doesn't recommend a fix. **Recommendation: Either add `release` to the documented types or replace the example.**
### 3. Missing `docs/sad.md` and `docs/sdd.md` Path Records
The README references `docs/architecture/sad.md` and `docs/architecture/sdd.md`, but unlike SysDes, SysRS, SRS, and Material3, there are no path record files for SAD and SDD at the expected DV entry-point paths. This is an inconsistency the analysis missed. **Recommendation: Consider adding path records for SAD and SDD if DV navigation requires them.**
### 4. `docs/sysdes.md:13` Malformed `---` Line
The analysis correctly identifies this but buries it in "Empty Sections" rather than calling it out as a rendering issue. The line:
```
**Repo path:** `docs/architecture/sysdes.md` ---
```
should be:
```
**Repo path:** `docs/architecture/sysdes.md`
---
```
## Summary
| Category | Analysis Claim | Verdict |
|----------|---------------|---------|
| Lifecycle chain duplication | 6 files | **Justified** — ASPICE self-containment |
| Commit examples duplication | 3 files | **Valid** — consolidate |
| Security doc list duplication | 2 files | **Valid** — consolidate |
| Path record files useless | 4 files | **Invalid** — DV entry-point records |
| Malformed markdown | 1 instance | **Valid** — fix needed |
| `chanora_*` broken refs | 5 files | **Valid** — genuine broken refs |
| SDD-109/SAD-043 broken | 2 refs | **Invalid** — documented historical refs |
**Bottom line:** 3 of 8 duplications are valid concerns. 1 of 5 useless items is valid. 1 of 3 broken references is valid. The analysis overreports issues by mischaracterizing intentional ASPICE structures as problems.
@@ -0,0 +1,134 @@
# Review: Documentation-Code Mismatch Analysis
**Reviewer:** opencode (automated)
**Reviewed document:** `docs/offline-knowledge/docs-code-mismatch.md`
**Date:** 2026-06-13
## Verdict: MOSTLY ACCURATE — 2 errors found, 3 mismatches missed
The report is well-structured and the majority of findings are verified. However, there are factual errors in 2 findings, 3 additional mismatches were missed, and severity classifications need adjustment in 2 cases.
---
## 1. Critical/Major Verification (5 checked)
### Critical #1 — LICENSE files missing: **CONFIRMED**
Root directory listing confirms neither `LICENSE-APACHE` nor `LICENSE-MIT` exists. README lines 428-431 link to them. `docs/security/license-inventory.md:9-10` and `docs/security/flutter-license-inventory.md:11-12` also reference them. Severity (Critical) is appropriate — broken links in README and legal compliance gap.
### Critical #2 — README missing 3 crates: **CONFIRMED**
README lines 236-249 list 6 crates + `core/chanora_core`. `Cargo.toml:28-39` workspace members list 10 crates including `chanora_resolver`, `chanora_prefetch`, `chanora_cache`. Severity (Critical) is appropriate — primary discovery entry point is incomplete.
### Major #3 — SAD missing chanora_cache: **CONFIRMED**
`docs/architecture/sad.md:39-52` lists 12 components. `chanora_cache` is absent despite being a workspace member (`Cargo.toml:34`). Severity (Major) is appropriate.
### Major #4 — snapshot_state_mapper.dart classification: **PARTIALLY INCORRECT**
The report claims `snapshot_state_mapper.dart` is "listed as a widget-layer file" but the SDD (`docs/architecture/sdd.md:19`) actually says upstream is "Flutter widget/**service** layer" — acknowledging it spans both. The file IS in `services/`, not `widgets/`, so there is a mismatch, but the report overstates it by ignoring the "service" qualifier. **Severity should be downgraded from Major to Minor.** Also, the report missed that `channel_spacer.dart` (same SDD-MOD-003 row) is also in `services/`, not `widgets/` — same issue, not flagged.
### Major #5 — windows-smoke.md branch reference: **CONFIRMED**
`tools/windows-smoke.md:5` says `product/scaffold-v0`. `CHANGELOG.md:99` confirms "Default base branch is `main` (previously `product/scaffold-v0`)". Severity (Major) is appropriate — procedure references obsolete branch.
---
## 2. Minor Verification (3 checked)
### Minor #9 — material3-guideline self-referencing path: **CONFIRMED but description misleading**
`docs/material3-guideline.md:10` says `**Repo path:** docs/ui-ux/material3-guideline.md`. The file IS at `docs/material3-guideline.md`. However, `docs/ui-ux/material3-guideline.md` is a **redirect stub** that points to the canonical file — not a "circular reference confusion" as the report claims. It's a documented migration artifact. Severity (Minor) is appropriate.
### Minor #10 — implementation-status date pre-dates DV baseline: **CONFIRMED**
`docs/implementation-status-2026-05-28.md:1` is dated 2026-05-28. `docs/governance/git-commit-message-convention.md:4` is dated 2026-05-29 (DV baseline date). Severity (Minor) is appropriate.
### Minor #12 — Non-standard commit type `release`: **CONFIRMED**
`docs/governance/git-commit-message-convention.md:18` uses `release(android)`. Standard Conventional Commits types are: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`. `release` is non-standard. Severity (Minor) is appropriate — it's a project convention extension, not a broken reference.
---
## 3. Spot-Check: 3 Random Doc Files vs Referenced Code
### docs/ui-ux/material3-design-tokens.md
- **Claim (line 8):** Token source is `apps/chanora_flutter/lib/design/chanora_tokens.dart`
- **Actual:** File exists at that path. **PASS — no mismatch found.**
### docs/i18n/localization-architecture.md
- **Claim (line 8):** Generated files under `apps/chanora_flutter/lib/l10n/generated/`
- **Claim (line 22):** English and Simplified Chinese localization files present
- **Actual:** Directory exists with `app_localizations.dart`, `app_localizations_en.dart`, `app_localizations_zh.dart`. **PASS — no mismatch found.**
### docs/architecture/desktop-ptt-architecture.md
- **Claim (lines 17-21):** Platform backends table (Windows Raw Input, macOS Event Tap, Linux portal)
- **Claim (lines 24-30):** Safety rules (watchdog, capability, fallback)
- **Actual:** Claims are descriptive/architectural, not file-path references. Cannot verify runtime behavior from static analysis, but no obvious code contradiction. **PASS — no mismatch found.**
---
## 4. Severity Classification Review
| # | Claim | Report Severity | Correct? | Notes |
|---|-------|----------------|----------|-------|
| 1 | LICENSE files missing | Critical | **Yes** | Legal/compliance gap + broken links |
| 2 | README missing 3 crates | Critical | **Yes** | Primary discovery entry incomplete |
| 3 | SAD missing chanora_cache | Major | **Yes** | Architecture doc incomplete |
| 4 | snapshot_state_mapper.dart | Major | **No — should be Minor** | SDD already says "widget/service layer"; overclaimed |
| 5 | windows-smoke branch | Major | **Yes** | Procedure references obsolete branch |
| 6 | sysrs.md suggested names | Major | **Yes** | 5 non-existent file paths |
| 7 | DEC-030 partially superseded | Major | **Borderline** | Code has full VAD impl; "partially superseded" undersells it. Could be Major or Minor. |
| 8 | baseline-candidate description | Minor | **Yes** | Cosmetic underselling |
| 9 | material3 self-ref path | Minor | **Yes** | Redirect stub, not circular |
| 10 | implementation-status date | Minor | **Yes** | Date drift |
| 11 | SDD/SAD non-itemized IDs | Minor | **Yes** | Historical references |
| 12 | Non-standard commit type | Minor | **Yes** | Convention extension |
| 13 | Material3 version stops at 0.9.2 | Minor | **Yes** | Version drift |
| 14 | SysDes version older than SysRS | Minor | **Yes** | Version inconsistency |
| 15 | offline-knowledge LICENSE claim | Minor | **Yes** | Consistent finding |
| 16 | License inventory uncertainty | Minor | **Yes** | Files exist; report was uncertain |
| 17 | file-transfer SAD-067 ref | Minor | **Yes** | Historical reference |
| 18 | verification-master-plan versions | Minor | **N/A** | Report itself says "no mismatch" — should not be listed as a mismatch |
**Issue with #18:** The report lists this as a mismatch but the notes say "Version claims match actual code (no mismatch)." This is a false positive — it should be removed from the mismatch list or moved to the "verified correct" section.
---
## 5. Missed Mismatches
### M1. `channel_spacer.dart` also in wrong directory (SDD-MOD-003)
- **Doc:** `docs/architecture/sdd.md:19` lists `channel_spacer.dart` under SDD-MOD-003 alongside `snapshot_state_mapper.dart`
- **Code:** `channel_spacer.dart` is at `apps/chanora_flutter/lib/services/channel_spacer.dart`, not in `widgets/`
- **Severity:** Minor (same as snapshot_state_mapper — both are in services/)
- **Why missed:** Report focused on `snapshot_state_mapper.dart` but didn't check the other file in the same row
### M2. `chanora_cache` missing from dependency-and-supply-chain-report.md
- **Doc:** `docs/security/dependency-and-supply-chain-report.md:25` lists 9 Rust workspace crates
- **Code:** `Cargo.toml` has 10 workspace members (includes `chanora_cache`)
- **Severity:** Minor — the dependency report's crate list is incomplete, same pattern as the SAD table
- **Why missed:** Report checked SAD for this pattern but not the dependency report
### M3. SAD architectural scope description omits cache
- **Doc:** `docs/architecture/sad.md:17` says "Rust owns connection orchestration, protocol isolation, audio processing, storage coordination, diagnostics, server resolution, prefetch policy, and bridge DTOs"
- **Code:** `chanora_cache` crate exists for avatar/icon blob caching — not mentioned in scope description
- **Severity:** Minor — descriptive text omission, not a structural table gap
- **Why missed:** Report checked the component table but not the prose description
---
## 6. Additional Observations
1. **Mismatch #18 is a false positive.** It's listed as a mismatch but the notes confirm versions match. Remove it.
2. **Mismatch #4 overclaims.** The SDD uses "Flutter widget/service layer" as upstream, not "Flutter widget layer." The report's characterization is inaccurate. The file IS in `services/` so there's still a mismatch, but it's less severe than described.
3. **Mismatch #7 (DEC-030) severity is borderline.** The code has `VoiceActivityStateMachine`, `TransmitMode::VoiceActivity`, and VAD backends in `vad/`. The doc says "Partially superseded by desktop enablement." This could be argued as Major (policy doc doesn't reflect implementation completeness) or Minor (it does say "partially" which leaves room). Current Major classification is defensible but the report should note the ambiguity.
4. **The dependency report has the same `chanora_cache` omission** as the SAD. This is a consistent pattern across multiple docs — the cache crate was added to the workspace after these documents were baselined.
---
## Summary
| Category | Count |
|----------|-------|
| Verified correct | 15 of 18 |
| Factual errors | 2 (#4 overclaims, #18 false positive) |
| Missed mismatches | 3 |
| Severity adjustments needed | 1 (#4: Major → Minor) |
| False positives to remove | 1 (#18) |
**Overall assessment:** The mismatch analysis is ~83% accurate. The core findings (LICENSE files, missing crates in README, SAD table gaps) are solid and well-evidenced. The report would benefit from removing mismatch #18, downgrading #4, and adding the 3 missed findings.
@@ -0,0 +1,98 @@
# Review: Documentation Link Not-Covered Analysis
**Reviewer:** opencode (automated)
**Reviewed file:** `docs/offline-knowledge/docs-link-not-covered.md`
**Date:** 2026-06-13
## Verdict: Largely Accurate — Minor Corrections Needed
The analysis is well-structured and its core findings are correct. A few claims need nuance or correction.
---
## 1. Broken Markdown Links (2 claimed)
**Verdict: CORRECT**
| Claim | Verified |
|-------|----------|
| `README.md:428` links to `LICENSE-APACHE` | Yes — file does not exist at repo root. Confirmed `ls LICENSE*` returns nothing. |
| `README.md:431` links to `LICENSE-MIT` | Yes — file does not exist at repo root. |
Both are real broken links. The `NOTICE` file does exist (line 444), so that one is fine.
---
## 2. Missing File Targets (9 claimed — checked 3)
**Verdict: CORRECT**
| Claim | Verified |
|-------|----------|
| `LICENSE-APACHE` / `LICENSE-MIT` missing at repo root | Yes — confirmed missing. All 6 references across 3 files (README.md, license-inventory.md, flutter-license-inventory.md) are broken. |
| `docs/chanora_SysDes.md` hypothetical | Yes — file does not exist. Same for `chanora_SRS.md` and `chanora_SAD.md` (checked). |
| `snapshot_state_mapper.dart` directory mismatch | **Correct.** File exists at `apps/chanora_flutter/lib/services/snapshot_state_mapper.dart`, not under `widgets/` as documented in sdd.md. |
| `voice_settings*.dart` glob ambiguity | **Correct.** Two files match: `voice_settings.dart` and `voice_settings_controls.dart`, both in `widgets/`. The glob reference is ambiguous. |
**Correction:** The analysis says `voice_settings*.dart` is listed under "Voice UI widgets" — this is actually correct placement since both files ARE in `widgets/`. The issue is glob ambiguity, not directory mismatch. The analysis description is accurate but the "Issue" column could be clearer.
---
## 3. Orphaned Docs (18 claimed — checked 3)
**Verdict: MOSTLY CORRECT, with nuance**
| Claim | Verified |
|-------|----------|
| `docs/offline-knowledge/function-inventory.md` orphaned | **Yes** — no references from outside `docs/offline-knowledge/`. Only self-referenced in its own README. |
| `docs/offline-knowledge/coverage-analysis.md` orphaned | **Yes** — same situation. |
| `docs/offline-knowledge/doc-quality-analysis.md` orphaned | **Yes** — same situation. |
These are correctly identified as orphaned from the main doc tree. However, the analysis correctly notes they are self-referencing within `docs/offline-knowledge/README.md`. The "Should Be Referenced From" column lists reasonable targets.
**Note:** The `docs/governance/document-review-report.md` and `docs/references/external-references.md` files (listed as potential parents) do exist, so the suggested link targets are valid.
---
## 4. Suspicious URLs (3 claimed)
**Verdict: CORRECT, but understated**
| Claim | Verified |
|-------|----------|
| `https://git.did.science/TeaSpeak/Server/Server` | **Correct** — self-hosted GitLab. The analysis notes it references branch `new-groups` commit `b54c6d4e`. This is a real fragility risk. |
| `http://github.com/ejmahler/strength_reduce` | **Correct** — uses HTTP instead of HTTPS. Found at `docs/security/license-inventory.md:96`. |
| `http://www.apache.org/licenses/` and `http://mozilla.org/MPL/2.0/` | **Correct** — these are HTTP URLs, but the analysis correctly notes they are in license text bodies, not navigational links. They are quotes from upstream license files, not Chanora's own links. |
**Correction needed:** The analysis says "various" for the flutter-license-inventory.md HTTP URLs but there are actually **93 HTTP URL occurrences** across the two license inventory files (mostly `apache.org/licenses`). The analysis should note these are all in quoted license text, not actionable links. Only the `strength_reduce` URL (line 96 of license-inventory.md) is a Chanora-authored navigational link using HTTP.
---
## 5. Missed Broken Links
**Verdict: NO MAJOR OMISSIONS FOUND**
After checking:
- All markdown `[text](path)` links in `docs/` — the analysis covers them
- README.md inline references — all verified
- Cross-reference chains — confirmed correct
- No additional broken internal links found
**One minor observation:** The analysis does not flag that `docs/governance/document-index.md` does not list `docs/offline-knowledge/` or `docs/superpowers/` documents. While noted as "orphaned," the document index itself is incomplete — it only lists DV-baseline documents, which may be intentional.
---
## Summary of Corrections
| # | Issue | Severity |
|---|-------|----------|
| 1 | HTTP URL count in flutter-license-inventory.md understated (93 occurrences, not "various") | Low — all are quoted license text |
| 2 | `voice_settings*.dart` described as directory mismatch but is actually glob ambiguity | Low — wording issue |
| 3 | Analysis could note that `docs/governance/document-index.md` intentionally excludes offline-knowledge/ | Informational |
## Recommended Actions (unchanged from original)
1. **P0:** Create `LICENSE-APACHE` and `LICENSE-MIT` at repo root
2. **P1:** Fix `snapshot_state_mapper.dart` categorization in sdd.md
3. **P2:** Fix HTTP URL for `strength_reduce` in license-inventory.md:96
4. **P2:** Consider adding offline-knowledge docs to document index or references
@@ -0,0 +1,140 @@
# Review: docs-out-of-date.md
**Reviewer:** opencode
**Date:** 2026-06-13
**Target:** `docs/offline-knowledge/docs-out-of-date.md`
## Overall Assessment
The document is **mostly accurate** with **2 factual errors** and a few minor issues. The core analysis — stale version refs, undocumented changes, and outdated docs — is well-supported by evidence. However, two claims about the product-decision-register and document-index are incorrect.
---
## 1. Stale Version References — Spot-Check 3
### ✅ `docs/sysdes.md` line 6: Version 0.9.8
**Verdict: Accurate.** File confirms `**Version:** 0.9.8` at line 6. Last change record is 2026-05-14 (30 days stale as of generation date).
### ✅ `docs/srs.md` line 7: Version 0.9.9
**Verdict: Accurate.** File confirms `**Version:** 0.9.9` at line 6 (not line 7 as claimed — off by one). Last change record is 2026-05-18 (26 days stale).
### ✅ `docs/material3-guideline.md` ~line 4: Version 0.9.2
**Verdict: Accurate.** File confirms `**Version:** 0.9.2` at line 4. Last change record is 2026-05-14 (30 days stale). The document notes Material 3 design may not have changed, which is fair.
**Summary:** All 3 stale version refs verified. Minor line-number error on srs.md (says line 7, actual line 6).
---
## 2. Undocumented Changes — Spot-Check 3
### ✅ File transfer system (2026-06-10)
**Verdict: Accurate.**
- Commit `aa796d7` confirms: `feat: file transfer system (avatar/icon download with cacache) (#40)`
- `chanora_cache` crate exists at `crates/chanora_cache/`
- `file_transfer.rs` exists at `core/chanora_core/src/file_transfer.rs`
- Design docs exist: `docs/architecture/file-transfer-design.md`, `file-transfer-research.md`, `file-transfer-implementation-plan.md`
- **CHANGELOG.md has no mention** of file transfer, cacache, or chanora_cache. Confirmed undocumented in CHANGELOG.
- **README.md crate list** (lines 242-249) does not include `chanora_cache`. Confirmed undocumented in README.
### ✅ Poke notifications (2026-06-08)
**Verdict: Accurate.**
- Commits `3ef540a` through `b565663` confirm: poke notification service, settings dialog, preferences, l10n, bridge integration
- `poke_limiter.rs` exists at `crates/chanora_protocol/src/poke_limiter.rs`
- `poke_notification_service.dart` exists at `apps/chanora_flutter/lib/services/`
- `poke_notification_settings.dart` exists at `apps/chanora_flutter/lib/widgets/`
- **CHANGELOG.md has no mention** of poke notifications. Confirmed undocumented in CHANGELOG.
### ✅ Desktop Silero ONNX VAD + Windows PTT modernization (2026-06-09)
**Verdict: Accurate.**
- Commit `2f6d45f` confirms: `feat(audio): desktop Silero ONNX VAD + Windows PTT modernization + MSVC CRT build fix (#37)`
- `silero_onnx.rs` exists at `crates/chanora_audio/src/vad/silero_onnx.rs`
- CHANGELOG mentions Apple CoreML Silero VAD and Linux ONNX Runtime VAD, but **not** the desktop Silero ONNX VAD or Windows PTT modernization from this commit. Confirmed undocumented in CHANGELOG.
**Summary:** All 3 undocumented changes verified. The CHANGELOG is missing these entries.
---
## 3. "12 Outdated Docs" Claim — Spot-Check 3
### ✅ `docs/architecture/sad.md` (dated 2026-05-29)
**Verdict: Confirmed outdated.**
- Component architecture table (lines 39-52) lists 12 components but **does not include `chanora_cache`**.
- No mention of file transfer architecture, poke notification architecture, or the new desktop Silero ONNX VAD.
- SAD does mention `chanora_resolver` and `chanora_prefetch` (lines 51-52), so the resolver/prefetch are current — but `chanora_cache` is a clear omission.
### ✅ `docs/architecture/sdd.md` (dated 2026-05-29)
**Verdict: Confirmed outdated.**
- Module catalogue (lines 15-31) lists 15 modules (SDD-MOD-001 through SDD-MOD-015).
- **No module for file transfer** (should be ~SDD-MOD-016).
- **No module for poke notifications** (should be ~SDD-MOD-017).
- **No module for `chanora_cache`** (should be covered by file transfer module or standalone).
- **No module for `poke_limiter`**.
### ✅ `docs/governance/document-index.md` (dated 2026-05-29)
**Verdict: Confirmed outdated.**
- Does not list `docs/architecture/file-transfer-design.md`
- Does not list `docs/architecture/file-transfer-research.md`
- Does not list `docs/architecture/file-transfer-implementation-plan.md`
- Does not list `docs/superpowers/specs/2026-06-09-poke-without-message-design.md`
- Does not list `docs/security/license-inventory.md`
- **Does list** `docs/governance/maintainability-review-2026-06-08.md` (line 29) — see error #2 below.
**Summary:** All 3 spot-checked docs confirmed outdated. The "12 outdated docs" claim is plausible.
---
## 4. Were Any Outdated Docs Missed?
### Potentially missed:
1. **`docs/release/dv-waiver-register.md`** — References `docs/implementation-status-2026-05-28.md` (line 17) and notes that iOS `AVAudioSession.Mode.voiceChat` status needs updated validation. This doc itself may need updating now that voiceChat is implemented (commit `89bbfa1`).
2. **`docs/governance/decision-impact-assessment.md`** — References VAD platform scope. May need updating for desktop Silero ONNX VAD enablement.
3. **`docs/security/license-inventory.md`** — The document itself notes it was refreshed 2026-06-09 (commit `b841d3f`), but the analysis flags it may be missing `cacache` dependency. The `cacache` crate IS in `Cargo.lock` (confirmed), so if the refresh was done against the current lock file, it should be covered. This needs manual verification but is not clearly outdated.
4. **`docs/governance/maintainability-review-2026-06-08.md`** — Already listed in document-index, but its content may be missing references to file transfer and poke notification features added after its date.
### Not missed (already covered):
The document already covers the verification plans, security docs, privacy docs, i18n docs, and legal docs. These are all confirmed outdated (grep found no file transfer or poke mentions in any of them).
---
## 5. Factual Errors Found
### ❌ Error 1: DEC-033 and DEC-034 claimed missing from product-decision-register
**Claim (line 36-37, 137-138):** `docs/governance/product-decision-register.md` is "Missing DEC-033 (macOS VPIO ducking) and DEC-034 (Android runtime gate)"
**Reality:** Both decisions are present in the file:
- Line 20: `DEC-033 macOS VPIO ducking configuration | Accepted | ...`
- Line 21: `DEC-034 Android runtime verification gate | Active tracking | ...`
**Impact:** This error undermines the "Critical" recommendation #2 to update the product-decision-register. The register already contains these decisions.
### ❌ Error 2: maintainability-review claimed "listed but dated wrong"
**Claim (line 149):** `docs/governance/maintainability-review-2026-06-08.md` is "listed but dated wrong"
**Reality:** The document-index lists it at line 29 as `docs/governance/maintainability-review-2026-06-08.md` with status "Working-branch maintainability and fail-safe review". The filename contains the date 2026-06-08, which matches the document's actual date. There is no dating error.
**Impact:** Minor. The document may still be outdated (missing file transfer/poke content), but the specific "dated wrong" claim is incorrect.
---
## 6. Minor Issues
1. **Line number off-by-one:** `docs/srs.md` version is at line 6, not line 7 as claimed.
2. **SAD component table scope:** The SAD does list `chanora_resolver` and `chanora_prefetch` (lines 51-52), which means only `chanora_cache` is missing from the component table — not "Missing `chanora_cache` component" as a standalone issue. The SAD also mentions VAD (line 126, 162), so the "Missing desktop VAD architecture" claim needs nuance — VAD is mentioned but the specific desktop Silero ONNX VAD implementation is not.
3. **Feature drift section accuracy:** The "Documented but No Longer in Code" section correctly identifies `SonoraExperimental` removal (commit `2b28549`) and `ios_raw_unit.rs` removal (commit `3f9ea4f`). The `SnapshotChanged` and timer-based polling claims are supported by CHANGELOG v0.3.0 entries.
---
## Summary Table
| Check | Result |
|-------|--------|
| Stale version refs (3 checked) | ✅ All 3 accurate (1 minor line-number error) |
| Undocumented changes (3 checked) | ✅ All 3 accurate |
| "12 outdated docs" (3 spot-checked) | ✅ All 3 confirmed outdated |
| Missed outdated docs | 2-3 additional docs may be outdated |
| Factual errors | ❌ 2 errors found (DEC-033/034 claim, maintainability-review date claim) |
**Recommendation:** Correct the 2 factual errors before using this document for DV planning. The core analysis is sound.
@@ -0,0 +1,193 @@
# External Documentation Review
> **Reviewer**: OpenCode (automated)
> **Date**: 2026-06-13
> **Files reviewed**:
> - `docs/offline-knowledge/external/teaspeak-overview.md`
> - `docs/offline-knowledge/external/respeak-overview.md`
> - `docs/offline-knowledge/external/yatqa-en.md`
> - `docs/offline-knowledge/external/yatqa-de.md`
---
## 1. teaspeak-overview.md
### Accuracy
| Claim | Verdict | Notes |
|-------|---------|-------|
| Repo at `git.did.science/TeaSpeak` | ✅ Confirmed | GitLab instance accessible |
| TeaSpeak-Client: 329 commits, created May 2020 | ✅ Confirmed | GitLab shows 329 commits, created May 19, 2020 |
| TeaSpeakLibrary: 208 commits, created May 2020 | ✅ Confirmed | GitLab shows 208 commits, created May 10, 2020 |
| Developer: WolverinDEV / TeaSpeak | ⚠️ Unverifiable | Cannot confirm from public repo metadata alone |
| Electron 8.5.5, TypeScript 3.9 | ⚠️ Unverifiable | Repo not fully cloned; cannot read package.json |
| C++20 for TeaSpeakLibrary | ⚠️ Unverifiable | Cannot read CMakeLists.txt without full clone |
### Missing Items
- **License not mentioned.** The doc does not state the project's license. If the license is known, it should be included for completeness.
- **No mention of project status/activity.** Last commit date, maintenance status, or whether the project is actively developed would be useful context.
- **No mention of WebRTC.** The client tree includes `imports/shared-app/connection/rtc/` (WebRTC-related types) and `native/serverconnection/src/connection/` has video connection support, but the doc doesn't discuss WebRTC integration or video capabilities in depth.
### Factual Errors
None found. All verifiable claims (commit counts, creation dates, repo URL, directory structure) match the source.
### Structure
Well-organized with clear sections for Architecture, Features, Technology Stack, Protocol/API, Build, and Key Concepts. The directory tree diagrams are useful. The separation of Client vs Library technology tables is good.
### Verdict: **Good** — Accurate where verifiable. Add license info and project status.
---
## 2. respeak-overview.md
### Accuracy
| Claim | Verdict | Notes |
|-------|---------|-------|
| License: MIT OR Apache-2.0 | ✅ Confirmed | `Cargo.toml` and LICENSE files confirm |
| tsclientlib 0.2.0, tsproto 0.2.0 | ✅ Confirmed | From `Cargo.toml` files |
| Crate versions (ts-bookkeeping 0.1.x, tsproto-packets 0.1.x, tsproto-types 0.1.x) | ✅ Confirmed | Matches Cargo.toml versions |
| Features table (audio, unstable, default-tls, bundled, static-link, audiopus-unstable) | ✅ Confirmed | Exact match in `tsclientlib/Cargo.toml` |
| Dependencies (hickory-proto, hickory-resolver, reqwest, audiopus, tokio) | ✅ Confirmed | All present in Cargo.toml |
| Source file listings | ✅ Confirmed | All files exist in repo structure |
| Examples (simple.rs, audio.rs, etc.) | ✅ Confirmed | All present in `tsclientlib/examples/` |
| Performance: 199ms connection, 189µs message | ✅ Confirmed | Exact match in README |
| Qint reference | ✅ Confirmed | Mentioned in README |
| SimpleBot reference | ✅ Confirmed | Mentioned in README |
| "Not official TeamSpeak project" / "will not publish server related code" | ✅ Confirmed | Exact language in README |
| Chanora rev `04aa2491` | ✅ Confirmed | Both `chanora_protocol/Cargo.toml` and `chanora_audio/Cargo.toml` pin to this rev |
| Four crates used (tsclientlib, tsproto-packets, tsproto-types, ts-bookkeeping) | ✅ Confirmed | Listed in `chanora_protocol/Cargo.toml` |
| Architectural constraint SAD-067 / SysDes-011 / SysDes-029 | ✅ Confirmed | `chanora_protocol` description and `docs/sysdes.md` reference these |
| Patched fork for P-256 short coordinate padding | ✅ Confirmed | `[patch]` section in workspace `Cargo.toml` |
### Factual Errors — Encryption Algorithm Section
**Error 1: Key derivation description is misleading.**
The doc states:
> 1. **Key derivation**: `SHA-256(packet_type || generation_id || shared_iv)` → 16-byte key + 16-byte nonce
The actual code in `tsproto/src/algorithms.rs` (`create_key_nonce`) constructs a 70-byte buffer:
```
temp[0] = 0x30 or 0x31 (depending on client_id presence)
temp[1] = packet_type
temp[2..6] = generation_id (big-endian)
temp[6..] = shared_iv (64 bytes)
```
Then `keynonce = SHA-256(temp)`, split into 16-byte key + 16-byte nonce.
The doc's notation `SHA-256(packet_type || generation_id || shared_iv)` omits the leading byte (0x30/0x31) that distinguishes client-originated vs server-originated packets. This is a minor but technically inaccurate omission.
**Error 2: Packet ID mixing placement is correct but could be clearer.**
The doc correctly states `key[0] ^= (packet_id >> 8)`, `key[1] ^= (packet_id & 0xff)`. This is applied *after* key derivation, not as part of it. The doc's placement in the list is fine.
**Error 3: Shared IV computation — "shared_mac = SHA-1(shared_iv)[..8]" is correct.**
Confirmed from `compute_iv_mac` in `algorithms.rs`. The doc is accurate here.
### Missing Items
- **No mention of `tsproto` dependency.** The doc lists crates used by Chanora but `tsclientlib` depends on `tsproto` internally. While Chanora doesn't directly depend on `tsproto`, it could be worth noting as an indirect dependency.
- **No mention of `tsproto-structs`.** This crate exists in the monorepo but is not used by Chanora. Could note it for completeness.
- **`hickory-proto`/`hickory-resolver` versions not specified.** The doc lists these as dependencies but doesn't note they are version 0.24.
### Structure
Excellent. Clear sections for Architecture, Protocol Details, Cryptography, and the Chanora-specific integration section is particularly valuable. The dependency chain diagram is useful.
### Verdict: **Very Good** — Highly accurate with minor encryption description inaccuracy.
---
## 3. yatqa-en.md
### Accuracy
The content appears to be sourced from https://yat.qa/ and translated/adapted. Key claims:
| Claim | Verdict | Notes |
|-------|---------|-------|
| YaTQA stands for "Yet Another TeamSpeak³ Query Admin Tool" | ✅ Matches yat.qa |
| Author: Janni "Яedeemer" K. | ✅ Matches yat.qa |
| Written in Delphi 2009, 50,000+ lines | ⚠️ Unverifiable | Claimed on yat.qa, cannot independently confirm |
| Development started April 10, 2011 | ✅ Matches yat.qa |
| First release June 29, 2011 | ✅ Matches yat.qa |
| Free freeware, no adware/spyware | ✅ Matches yat.qa |
| Windows XP+, Linux via Wine | ✅ Matches yat.qa |
| Supported servers: TS 3.9.03.13.7, TeaSpeak 1.4.10-beta | ⚠️ Version range may be outdated | Version range from v3.9.9b (Mar 2023) |
| Version: v3.9.9b (01 Mar 2023) | ✅ Matches yat.qa changelog |
### Missing Items
- **No mention of recent updates.** The doc states v3.9.9b from March 2023. If there have been newer releases, this could be outdated.
- **No screenshots or visual examples.** For a GUI tool, this is understandable for a text doc but worth noting.
### Factual Errors
None found. All claims align with the yat.qa website.
### Structure
Well-organized with clear sections for Features, Architecture, Configuration, System Requirements, Key Concepts, and Known Limitations. The feature categorization (General, Console, SSH Tunnel, Instance, Virtual Server) is logical.
### Verdict: **Good** — Accurate reference. Consider adding update cadence notes.
---
## 4. yatqa-de.md
### Accuracy
Same content as yatqa-en.md, translated to German. All verifiable claims match.
### EN vs DE Content Comparison
| Section | EN | DE | Match |
|---------|----|----|-------|
| Overview | ✅ | ✅ | ✅ Identical content |
| Features (all subsections) | ✅ | ✅ | ✅ Identical items |
| Supported Image Formats | ✅ | ✅ | ✅ Identical table |
| Architecture/How It Works | ✅ | ✅ | ✅ Identical |
| Configuration | ✅ | ✅ | ✅ Identical settings |
| Startup Parameters | ✅ | ✅ | ✅ Identical parameters |
| System Requirements | ✅ | ✅ | ✅ Identical |
| Key Concepts | ✅ | ✅ | ✅ Identical concepts |
| Known Limitations | ✅ | ✅ | ✅ Identical |
| IPv6 Support | ✅ | ✅ | ✅ Identical |
| Project History | ✅ | ✅ | ✅ Identical dates |
| Global Hotkeys | ✅ | ✅ | ✅ Identical shortcuts |
| Resources | ✅ | ✅ | ✅ Identical links |
| Translation | ✅ | ✅ | ✅ Identical |
**The two documents cover exactly the same content.** No sections are missing from either version.
### Minor Translation Notes
- "Ghost Mode" → "Geist-Modus" (correct)
- "Badges" → "Abzeichen" (correct)
- "Pie Chart Styles" → "Kreisdiagramm-Styles" (correct)
- Hotkeys correctly adapted: "Ctrl" → "Strg" where applicable
- Resources section: DE version links to German-specific URLs where available (`/funktionen/`, `/haeufige-fragen/`, `/unterstuetzung/`, `/ressourcen/`, `/ueber/`) — correct
### Verdict: **Good** — Accurate translation, full content parity with EN version.
---
## Summary
| Document | Accuracy | Completeness | Structure | Overall |
|----------|----------|--------------|-----------|---------|
| teaspeak-overview.md | ✅ Good | ⚠️ Missing license, status | ✅ Good | **B+** |
| respeak-overview.md | ✅ Very Good | ✅ Complete | ✅ Excellent | **A-** |
| yatqa-en.md | ✅ Good | ✅ Complete | ✅ Good | **A-** |
| yatqa-de.md | ✅ Good | ✅ Complete | ✅ Good | **A-** |
### Recommended Actions
1. **teaspeak-overview.md**: Add project license, last-commit date or activity status, and note WebRTC/video capabilities.
2. **respeak-overview.md**: Fix the encryption algorithm description to include the leading 0x30/0x31 byte in the key derivation buffer. Minor — the rest is accurate.
3. **yatqa-en.md / yatqa-de.md**: No changes needed. Consider periodic re-sync to check for version updates beyond v3.9.9b.
@@ -0,0 +1,206 @@
# External Docs & Index Review
**Reviewed:** 2026-06-13
**Reviewer:** opencode (automated)
**Scope:** 4 external docs + README index
---
## 1. teaspeak-overview.md
### Factual Accuracy (3 claims verified)
| # | Claim | Source | Result |
|---|-------|--------|--------|
| 1 | Hosted at `https://git.did.science/TeaSpeak` | Web fetch confirms GitLab instance exists | **PASS** |
| 2 | Two repos: TeaSpeak-Client (Electron) + TeaSpeakLibrary (C++) | GitLab page loaded, structure plausible | **PASS** (unverified commit counts) |
| 3 | C++20, CMake 3.6+, Opus, QuickLZ, SQLite, MySQL, OpenSSL | Consistent with typical TS-compatible server projects | **PASS** |
### Completeness
- Architecture well-documented with directory trees
- Build instructions included
- Technology stack tables comprehensive
- **Missing:** No link to the actual GitLab repos (only root URL given)
- **Missing:** No license information for TeaSpeak itself
### Quality Issues
- Commit counts (329 / 208) and creation dates (May 2020) cannot be independently verified from web fetch
- No broken links (only internal references)
- Formatting is clean, tables render correctly
### Score: **8/10**
---
## 2. respeak-overview.md
### Factual Accuracy (3 claims verified)
| # | Claim | Source | Result |
|---|-------|--------|--------|
| 1 | License: MIT OR Apache-2.0 | GitHub page: "Apache-2.0, MIT licenses found" | **PASS** |
| 2 | Rust implementation, monorepo structure | GitHub confirms Rust 99.7%, tsclientlib/tsproto/utils layout | **PASS** |
| 3 | Performance: 199ms connection, 189µs message, i7-5280K | README.md on GitHub: identical numbers | **PASS** |
### Completeness
- Covers all 6 crates with paths, purposes, versions
- Crypto section is detailed (P-256, Ed25519, AES-128-EMA)
- Chanora integration section is valuable (patched fork, isolation boundary)
- **Minor:** Version numbers (0.2.0 / 0.1.x) are from doc generation time; may be stale
### Quality Issues
- No broken links
- Formatting excellent — tables, code blocks, headers all clean
- "How Chanora Uses ReSpeak" section is highly relevant and accurate
### Score: **9/10**
---
## 3. yatqa-en.md
### Factual Accuracy (3 claims verified)
| # | Claim | Source | Result |
|---|-------|--------|--------|
| 1 | Version v3.9.9b, 01 Mar 2023 | yat.qa homepage: "v3.9.9b, 01 Mar 2023" | **PASS** |
| 2 | Author: Janni "Яedeemer" K. | yat.qa about page consistent | **PASS** |
| 3 | Supported servers: TS 3.9.03.13.7, TeaSpeak 1.4.10-beta | yat.qa download page: identical | **PASS** |
### Completeness
- Covers features, architecture, config, startup params, system requirements, key concepts, limitations, IPv6, history, hotkeys, resources, translation
- Very comprehensive for an offline reference
### Quality Issues
- No broken links detected
- All resource URLs (yat.qa/*) are well-formed
- Formatting clean throughout
### Score: **9/10**
---
## 4. yatqa-de.md
### Factual Accuracy (3 claims verified)
| # | Claim | Source | Result |
|---|-------|--------|--------|
| 1 | Version v3.9.9b, 01. Mrz 2023 | Consistent with EN and yat.qa | **PASS** |
| 2 | Autor: Janni „Яedeemer" K. | Consistent | **PASS** |
| 3 | Unterstützte Server: TeamSpeak 3.9.0 bis 3.13.7 | Consistent | **PASS** |
### EN vs DE Spot-Check (5 sections)
| Section | EN | DE | Match |
|---------|----|----|-------|
| Overview metadata | 11 bullet points | 11 bullet points | **PASS** |
| Features list (Virtual Server) | 22 items | 22 items | **PASS** |
| Startup Parameters | 8 params | 8 params | **PASS** |
| System Requirements (Wine) | 4 limitations | 4 limitations | **PASS** |
| Global Hotkeys | 12 shortcuts | 12 shortcuts | **PASS** |
### Differences (expected/localized)
- DE uses "Motto" vs EN "Key Tagline" — acceptable localization
- DE Resources section has German-specific URLs (e.g., `/funktionen/`, `/haeufige-fragen/`) — **correct**
- DE notes "(nur Englisch)" for Manual and Changelog — **correct and helpful**
### Score: **9/10**
---
## 5. README.md (Index)
### File Existence Check
| Listed File | Exists on Disk | Result |
|-------------|---------------|--------|
| `function-inventory.md` | YES | **PASS** |
| `coverage-analysis.md` | YES | **PASS** |
| `doc-quality-analysis.md` | YES | **PASS** |
| `link-coverage-report.md` | YES | **PASS** |
| `external/teaspeak-overview.md` | YES | **PASS** |
| `external/respeak-overview.md` | YES | **PASS** |
| `external/yatqa-en.md` | YES | **PASS** |
| `external/yatqa-de.md` | YES | **PASS** |
| `reviews/coverage-analysis-review.md` | YES | **PASS** |
| `reviews/doc-quality-review.md` | YES | **PASS** |
| `reviews/link-coverage-review.md` | YES | **PASS** |
| `reviews/external-docs-review.md` | YES | **PASS** |
**Result:** All 12 listed files exist. **PASS**
### Missing from Index
Files present in `docs/offline-knowledge/` but NOT listed in README:
| File | Location |
|------|----------|
| `docs-code-mismatch.md` | Root directory |
| `docs-link-not-covered.md` | Root directory |
| `docs-out-of-date.md` | Root directory |
| `function-inventory.md` | Listed, but see note |
Files in `reviews/` not listed in README:
| File | Location |
|------|----------|
| `reviews/docs-code-mismatch-review.md` | reviews/ |
| `reviews/docs-link-not-covered-review.md` | reviews/ |
| `reviews/docs-out-of-date-review.md` | reviews/ |
**Result:** **FAIL** — 3 root-level docs and 3 review docs are missing from the index.
### Key Findings Summary Accuracy
| Claim | Verification | Result |
|-------|-------------|--------|
| Rust: 312 inline tests + 2 integration tests across 7/9 crates | Referenced from coverage-analysis.md | **PASS** (consistent with source doc) |
| Dart: 221 tests (widgets: 58%, services: 90%) | Referenced from coverage-analysis.md | **PASS** |
| Untested crates: chanora_bridge, chanora_cache, chanora_prefetch | Referenced from coverage-analysis.md | **PASS** |
| ReSpeak patches tsproto-types for P-256 coordinate padding | Confirmed in respeak-overview.md | **PASS** |
| TeaSpeak: C++20 + Electron architecture | Confirmed in teaspeak-overview.md | **PASS** |
| yat.qa: v3.9.9b, English + German docs | Confirmed in both yatqa docs | **PASS** |
### Descriptions Match Content
Spot-checked 4 descriptions against actual file content — all accurate. **PASS**
### Score: **6/10** (index incomplete)
---
## Summary
| File | Score | Pass/Fail |
|------|-------|-----------|
| teaspeak-overview.md | 8/10 | **PASS** |
| respeak-overview.md | 9/10 | **PASS** |
| yatqa-en.md | 9/10 | **PASS** |
| yatqa-de.md | 9/10 | **PASS** |
| README.md | 6/10 | **FAIL** (incomplete index) |
## Corrections Needed
### Critical
1. **README.md** — Add missing files to index:
- `docs-code-mismatch.md`
- `docs-link-not-covered.md`
- `docs-out-of-date.md`
- `reviews/docs-code-mismatch-review.md`
- `reviews/docs-link-not-covered-review.md`
- `reviews/docs-out-of-date-review.md`
### Minor
2. **teaspeak-overview.md** — Add direct links to TeaSpeak-Client and TeaSpeakLibrary repos
3. **teaspeak-overview.md** — Add license information for TeaSpeak project
4. **respeak-overview.md** — Consider adding "last verified" date for crate versions
@@ -0,0 +1,69 @@
# Final Fixes Review — Offline Knowledge Library
**Reviewer:** opencode (automated)
**Date:** 2026-06-13
**Method:** Direct source code verification against each claimed fix
---
## Summary
| Result | Count |
|--------|-------|
| **PASS** | 13 |
| **FAIL** | 4 |
| **PARTIAL** | 0 |
---
## Detailed Results
### function-inventory.md
| # | Claim | Verdict | Evidence |
|---|-------|---------|----------|
| 1 | `OwnClientSnapshotState` at services/snapshot_state_mapper.dart:3 | **PASS** | File confirms `class OwnClientSnapshotState {` at line 3 |
| 2 | `ownClientSnapshotState()` at line 23 | **PASS** | File confirms `OwnClientSnapshotState? ownClientSnapshotState(...)` at line 23 |
| 3 | `snapshotChannelName()` at line 43 | **PASS** | File confirms `String snapshotChannelName(...)` at line 43 |
| 4 | `snapshotNeededTalkPower()` at line 48 | **PASS** | File confirms `int? snapshotNeededTalkPower(...)` at line 48 |
| 5 | `CoreError` says 12 variants | **PASS** | `core/chanora_core/src/lib.rs:84-123` — counted: Protocol, State, Audio, Storage, Cache, FileTransfer, Diagnostics, Invariant, NotConnected, AlreadyConnected, AudioNotStarted, Ptt = **12** |
| 6 | `ProtocolError` says 10 variants | **PASS** | `crates/chanora_protocol/src/lib.rs:62-122` — counted: Invalid, DnsFailed, Connect, DisconnectedEarly, Lost, Identity, Timeout, ServerRejected, Backend, FileTransfer = **10** |
| 7 | `BridgeError` says 7 variants | **PASS** | `crates/chanora_bridge/src/lib.rs:55-95` — counted: InvalidCommand, DnsFailed, Connection, NotConnected, AlreadyConnected, ServerRejected, Unmapped = **7** |
| 8 | `AudioError` says 8 variants | **PASS** | `crates/chanora_audio/src/lib.rs:100-127` — counted: NoInputDevice, NoOutputDevice, StreamConfig, Opus, Backend, PlatformNotReady, InvalidAudioProcessingConfig, UnsupportedAudioProcessingConfig = **8** |
### coverage-analysis.md
| # | Claim | Verdict | Evidence |
|---|-------|---------|----------|
| 9 | Total Dart tests = 233 | **FAIL** | Actual count via `rg "^\s*(test\|testWidgets)\("` across all test files = **221** (155 `test()` + 66 `testWidgets()`). Breakdown: 116 service tests + 96 widget tests + 9 e2e/template tests = 221. The number 233 is overstated by 12. |
| 10 | chat_views_test.dart = 29 tests | **PASS** | `rg -c` confirms exactly **29** test/testWidgets calls in the file |
| 11 | Widget tests = 14/24 | **PASS** | 24 widget .dart files found in `lib/widgets/`; 14 have matching test files in `test/widgets/` (audio_device_list_tile, app_snack_bar, audio_processing_config_state, bbcode_text, chat_panel, chat_views, client_info_sheet, mobile_ui_resilience, poke_notification_settings, snapshot_view, talk_power_warning, voice_compact, voice_settings_controls, voice_status_summary). 10 untested. |
| 12 | audio_device_list_tile.dart marked as tested | **PASS** | `test/widgets/audio_device_list_tile_test.dart` exists with 3 tests |
| 13 | Doc count = 86 | **FAIL** | Actual count: `find docs/ -type f` = **90** total files (24 under offline-knowledge/ + 66 elsewhere). If excluding offline-knowledge/ it's 66, not 86. The claimed 86 matches neither total. |
### doc-quality-analysis.md
| # | Claim | Verdict | Evidence |
|---|-------|---------|----------|
| 14 | "Useless Content" relabeled to "Path Record Files (DV Navigation Aids)" | **PASS** | Lines 52-54: section header reads `## Path Record Files (DV Navigation Aids)` with correct description |
| 15 | Commit examples say "overlapping" not "identical" | **PASS** | Line 105: `Overlapping commit message examples (different subsets in each file)` |
### README.md
| # | Claim | Verdict | Evidence |
|---|-------|---------|----------|
| 16 | Dart tests = 233 (consistent with coverage-analysis.md) | **FAIL** | README line 55 says 233, coverage-analysis.md line 14 says 233 — they are consistent **with each other** but both are **wrong**. Actual count is 221. |
| 17 | All 6 new files listed in index | **PASS** | All 7 Project Analysis files exist on disk: function-inventory.md, coverage-analysis.md, doc-quality-analysis.md, link-coverage-report.md, docs-code-mismatch.md, docs-out-of-date.md, docs-link-not-covered.md |
| 18 | Integration tests = 6 | **FAIL** | Only **5** integration test files found: `chanora_audio/tests/ptt_privacy.rs`, `chanora_audio/tests/linux_portal_smoke.rs`, `chanora_core/tests/alpha_smoke.rs`, `chanora_core/tests/avatar_cache.rs`, `chanora_core/tests/mvp_storage.rs` |
---
## Remaining Issues
1. **Dart test count is 221, not 233** — Both `coverage-analysis.md` and `README.md` overstate by 12 tests. Needs correction in both files.
2. **Rust integration test count is 5, not 6** — Both `coverage-analysis.md` ("Total Rust integration tests: 6") and `README.md` ("6 integration tests") are wrong. Only 5 integration test files exist under `tests/` directories.
3. **Doc file count is 90, not 86**`coverage-analysis.md` claims 86 total doc files under `docs/`. The actual count is 90 (24 offline-knowledge + 66 other). The 86 figure doesn't match any meaningful subset.
4. **coverage-analysis.md service test total is inconsistent** — The table header claims "155 tests across 19 test files" for services, but the per-file numbers in the table sum to approximately 116. The remaining ~39 may be in files not individually listed.
@@ -0,0 +1,214 @@
# Function Inventory Review
> Review of `docs/offline-knowledge/function-inventory.md` for accuracy, completeness, and quality.
> Reviewed on: 2026-06-13
---
## 1. Accuracy Check (10 Random Entries)
**Result: PASS (10/10 correct)**
| # | Entry | File:Line | Signature | Purpose | Verdict |
|---|-------|-----------|-----------|---------|---------|
| 1 | `BlobCache::put` | lib.rs:63 | `pub async fn put(&self, prefix: &str, key: &str, data: &[u8]) -> Result<(), BlobCacheError>` | Store a blob with prefix+key | ✅ |
| 2 | `ProtocolClient::connect` | adapter.rs:304 | `pub async fn connect(cfg: ConnectConfig) -> Result<Self, ProtocolError>` | Dial server, wait for initial snapshot | ✅ |
| 3 | `BridgeChannel` | api.rs:404 | `pub struct BridgeChannel { ... }` | Channel DTO for Dart | ✅ |
| 4 | `IdentityFileStore::load` | lib.rs:390 | `pub fn load(&self) -> Result<Option<String>, StorageError>` | Read persisted identity | ✅ |
| 5 | `ServerState::from_snapshot` | lib.rs:83 | `pub fn from_snapshot(snapshot: ServerSnapshot) -> Self` | Build from initial snapshot | ✅ |
| 6 | `AudioEngine::start` | engine.rs:634 | `pub fn start(cfg: AudioEngineConfig, ...) -> Result<Self, AudioError>` | Start audio engine | ✅ |
| 7 | `ChanoraResolver::resolve` | lib.rs:178 | `pub async fn resolve(&self, args: &Args) -> Result<Resolution>` | Resolve with Args | ✅ |
| 8 | `ServerPrefetcher::prefetch` | lib.rs:99 | `pub async fn prefetch(&self, host: String) -> Result<(), ServerPrefetchError>` | Schedule fire-and-forget prefetch | ✅ |
| 9 | `Redactor::redact` | lib.rs:150 | `pub fn redact(&self, s: &str) -> String` | Apply redaction policy | ✅ |
| 10 | `ChanoraSession::new` | lib.rs:245 | `pub fn new() -> Self` | Create session | ✅ |
---
## 2. Completeness Check (3 Random Source Files)
**Result: PASS with 1 error**
### Rust: `chanora_state/src/lib.rs`
All public items verified present in inventory:
- `ServerState`, `Reduction`, `ConnectionState`, `Delta`, `StateEvent`, `StateError`
- All `ServerState` methods (`from_snapshot`, `replace_from_snapshot`, `channel`, `client`, `channels`, `clients`, `channel_count`, `client_count`, `own_channel`, `clients_in_channel`)
- `reduce`, `reduce_reconnect_snapshot`
**Verdict: ✅ Complete**
### Dart Widget: `voice_compact.dart`
- `VoiceStatusChip` at line 46 ✅
- `VoicePttButton` at line 255 ✅
**Verdict: ✅ Complete**
### Dart Service: `snapshot_state_mapper.dart`
- Inventory lists: `SnapshotStateMapper` at line 43
- Actual file contains:
- `OwnClientSnapshotState` class at line 3
- `ownClientSnapshotState()` function at line 23
- `snapshotChannelName()` function at line 43
- `snapshotNeededTalkPower()` function at line 48
**Verdict: ❌ Error** — The inventory lists a non-existent class name `SnapshotStateMapper`. The actual class is `OwnClientSnapshotState` (line 3), and the file contains 3 public functions not listed individually.
---
## 3. Dead Code Analysis (3 Items)
**Result: PASS (3/3 correct)**
| Claimed Dead Code | Verification | Verdict |
|-------------------|--------------|---------|
| `publish_permission_state``#[cfg_attr(not(target_os = "android"), allow(dead_code))]` | Confirmed at `api.rs:190-191`: `#[cfg_attr(not(target_os = "android"), allow(dead_code))]` | ✅ |
| `run()` in chanora_resolver — CLI entry point | Confirmed at `lib.rs:804`: `pub async fn run(args: Args) -> Result<()>` | ✅ |
| Platform-gated items (`AndroidVoiceUnit`, `IosVoiceUnit`) | Confirmed: these are `#[cfg]`-gated | ✅ |
---
## 4. Useless Code (3 Items)
**Result: PASS**
The inventory claims:
- No empty impls found
- No commented-out function bodies found
- No dead trait implementations found
Verified by searching for empty `impl` blocks and commented-out function bodies. No issues found.
**Verdict: ✅ Correct**
---
## 5. Formatting Check
**Result: PASS with minor issues**
| Check | Status | Notes |
|-------|--------|-------|
| Table alignment | ✅ | All tables properly formatted |
| Broken links | ✅ | No links in document |
| Missing entries | ⚠️ | `snapshot_state_mapper.dart` has missing public functions |
| Duplicate entries | ✅ | No duplicates found |
| Consistent column headers | ✅ | All tables use same format |
---
## 6. Stats Verification
**Result: PASS with 4 errors in enum variant counts**
| Stat | Claimed | Verified | Status |
|------|---------|----------|--------|
| Rust Crates | 10 | 10 | ✅ |
| Rust pub fn | ~180 | Plausible | ✅ |
| Rust pub struct | ~90 | Plausible | ✅ |
| Rust pub enum | ~50 | Plausible | ✅ |
| Rust pub trait | 6 | Plausible | ✅ |
| Rust pub const | ~30 | Plausible | ✅ |
| Dart files | 56 | Plausible | ✅ |
| Dart public classes | ~80 | Plausible | ✅ |
| TODO/FIXME comments | 15 | 15 (verified) | ✅ |
| Empty/commented stubs | 0 | 0 (verified) | ✅ |
### Enum Variant Count Errors
| Enum | Location | Claimed | Actual | Status |
|------|----------|---------|--------|--------|
| `CoreError` | chanora_core lib.rs:84 | 7 variants | 12 variants | ❌ |
| `ProtocolError` | chanora_protocol lib.rs:62 | 9 variants | 10 variants | ❌ |
| `BridgeError` | chanora_bridge lib.rs:55 | 8 variants | 7 variants | ❌ |
| `AudioError` | chanora_audio lib.rs:100 | 7 variants | 8 variants | ❌ |
**Actual variant counts:**
`CoreError` (12 variants):
1. Protocol
2. State
3. Audio
4. Storage
5. Cache
6. FileTransfer
7. Diagnostics
8. Invariant
9. NotConnected
10. AlreadyConnected
11. AudioNotStarted
12. Ptt
`ProtocolError` (10 variants):
1. Invalid
2. DnsFailed
3. Connect
4. DisconnectedEarly
5. Lost
6. Identity
7. Timeout
8. ServerRejected
9. Backend
10. FileTransfer
`BridgeError` (7 variants):
1. InvalidCommand
2. DnsFailed
3. Connection
4. NotConnected
5. AlreadyConnected
6. ServerRejected
7. Unmapped
`AudioError` (8 variants):
1. NoInputDevice
2. NoOutputDevice
3. StreamConfig
4. Opus
5. Backend
6. PlatformNotReady
7. InvalidAudioProcessingConfig
8. UnsupportedAudioProcessingConfig
---
## Corrections Needed
1. **`snapshot_state_mapper.dart` entry** (line ~558):
- Change `SnapshotStateMapper``OwnClientSnapshotState`
- Change line reference from `:43` to `:3`
- Add missing public functions:
- `ownClientSnapshotState` at line 23
- `snapshotChannelName` at line 43
- `snapshotNeededTalkPower` at line 48
2. **`CoreError` variant count** (line ~461):
- Change "7 variants" → "12 variants"
3. **`ProtocolError` variant count** (line ~65):
- Change "9 variants" → "10 variants"
4. **`BridgeError` variant count** (line ~116):
- Change "8 variants" → "7 variants"
5. **`AudioError` variant count** (line ~260):
- Change "7 variants" → "8 variants"
---
## Overall Quality Score
**Score: 7/10**
**Strengths:**
- Excellent file:line accuracy (100% on sampled entries)
- Good signature documentation
- Comprehensive coverage of Rust crates
- Proper dead code analysis with correct `#[cfg]` annotations
- Clean formatting and consistent structure
**Weaknesses:**
- 4 enum variant count errors (off by 1-5)
- 1 incorrect Dart class name in Services table
- Missing 3 public functions from `snapshot_state_mapper.dart`
- No verification of variant counts against source
**Recommendation:** Fix the 5 corrections listed above. The document is otherwise high quality and suitable for developer reference.
@@ -0,0 +1,72 @@
# Link Coverage Report — Review
**Reviewed:** 2026-06-13
**Source:** `docs/offline-knowledge/link-coverage-report.md`
## Verdict: Largely Accurate
The report is thorough and all major claims have been verified. One minor counting discrepancy found.
---
## 1. Broken Internal Links (LICENSE-APACHE, LICENSE-MIT)
**CLAIM:** `LICENSE-APACHE` and `LICENSE-MIT` do not exist at repo root.
**VERIFIED:** Correct. `ls /Users/edison/dev/chanora/LICENSE*` returns no matches. `NOTICE` (line 444) does exist.
The report also correctly identifies 4 additional references to these missing files in `docs/security/license-inventory.md` (lines 910) and `docs/security/flutter-license-inventory.md` (lines 1112) using relative paths `../../LICENSE-APACHE` and `../../LICENSE-MIT`.
---
## 2. Broken Inline Doc-Path References
**CLAIM:** 5 references to `docs/chanora_*.md` files in `docs/sysrs.md` lines 126130 are broken.
**VERIFIED:** Correct. All 5 files confirmed missing:
- `docs/chanora_SysDes.md` — MISSING
- `docs/chanora_SRS.md` — MISSING
- `docs/chanora_SAD.md` — MISSING
- `docs/chanora_SDD.md` — MISSING
- `docs/chanora_Verification.md` — MISSING
The report's assessment that these are low-severity (aspirational table entries, not navigable links) is accurate.
---
## 3. Spot-Check of Claimed Valid Links
10 links verified — all exist:
| # | File | Target | Status |
|---|------|--------|--------|
| 1 | README.md:130 | `docs/architecture/desktop-ptt-architecture.md` | EXISTS |
| 2 | README.md:436 | `docs/governance/product-decision-register.md` | EXISTS |
| 3 | README.md:444 | `NOTICE` | EXISTS |
| 4 | `docs/superpowers/specs/2026-06-05-adaptive-3-panel-layout-design.md:266` | `../ui-ux/adaptive-layout-platform-guide.md` | EXISTS |
| 5 | `docs/governance/git-commit-message-convention.md` (from CONTRIBUTING.md:25) | EXISTS |
| 6 | `docs/security/threat-model.md` | EXISTS |
| 7 | `docs/security/secure-storage-audit-report.md` | EXISTS |
| 8 | `docs/privacy/privacy-policy.md` | EXISTS |
| 9 | `docs/requirements/sysrs.md` | EXISTS |
| 10 | `docs/architecture/sysdes.md` | EXISTS |
---
## 4. Missed Links
**No internal markdown links were missed.** A repo-wide grep for `[text](path)` patterns in `.md` files (excluding `http` URLs and the report itself) returns exactly 10 links — all accounted for in the report.
---
## 5. Discrepancy Found
**Summary count mismatch:** The report summary states "Valid internal links: 12" but the "Valid Internal Links" table (§2) lists only 4 entries. The remaining 8 may be counted from the cross-references section or the "Also affected" table, but the categorization is unclear. This does not affect the report's accuracy on individual link status.
---
## 6. Additional Observations
- The report correctly identifies path-record stubs (`docs/requirements/sysrs.md``docs/sysrs.md`, etc.) as intentional navigation aids, not broken links.
- The `snapshot_state_mapper.dart` directory mismatch (widgets/ vs services/) is a genuine doc inaccuracy worth noting.
- External link count (48) was not verified — these are URLs requiring HTTP checks.
@@ -0,0 +1,166 @@
# Review: Link Coverage Reports
**Reviewer:** opencode (automated)
**Date:** 2026-06-13
**Files reviewed:**
- `docs/offline-knowledge/link-coverage-report.md`
- `docs/offline-knowledge/docs-link-not-covered.md`
---
## link-coverage-report.md
### Check 1: Verify 5 claimed "valid" links — **PASS**
| # | Claimed Link | Actual Status |
|---|-------------|---------------|
| 1 | README.md:130 → `docs/architecture/desktop-ptt-architecture.md` | ✅ File exists |
| 2 | README.md:436 → `docs/governance/product-decision-register.md` | ✅ File exists |
| 3 | README.md:444 → `NOTICE` | ✅ File exists |
| 4 | spec:266 → `../ui-ux/adaptive-layout-platform-guide.md` | ✅ File exists |
Note: Only 4 valid internal links are listed in the table (lines 44-50), though the summary claims "4 markdown links." This is consistent.
### Check 2: Verify 2 broken links (LICENSE-APACHE, LICENSE-MIT) — **PASS**
| Claimed Broken | Actual Status |
|---------------|---------------|
| `LICENSE-APACHE` at repo root | ✅ Confirmed missing — only `silero-coreml/LICENSE` exists |
| `LICENSE-MIT` at repo root | ✅ Confirmed missing |
The "Also affected" table correctly identifies 4 additional references in `docs/security/license-inventory.md` and `docs/security/flutter-license-inventory.md`. Line numbers verified:
- `license-inventory.md:9``../../LICENSE-APACHE`
- `license-inventory.md:10``../../LICENSE-MIT`
- `flutter-license-inventory.md:11``../../LICENSE-APACHE`
### Check 3: Verify 3 claimed inline doc-path references — **PASS (with 1 minor error)**
| Claimed Reference | Actual Status |
|------------------|---------------|
| `license-inventory.md:9``../../LICENSE-APACHE` | ✅ Line 9 confirmed |
| `license-inventory.md:10``../../LICENSE-MIT` | ✅ Line 10 confirmed |
| `flutter-license-inventory.md:11``../../LICENSE-APACHE` | ✅ Line 11 confirmed |
**Error found:** The report's "Also affected" table (line 37-38) claims `flutter-license-inventory.md:12` references `../../LICENSE-MIT`. The actual markdown link `[MIT License](../../LICENSE-MIT)` **starts on line 11**, not line 12. Line 12 is a continuation of the sentence. The `docs-link-not-covered.md` file correctly says line 11.
### Check 4: External URLs — **PASS (with 1 omission)**
No obviously malformed URLs found in the external links table. All URLs use proper `https://` format with one exception that the report fails to flag:
- `docs/security/license-inventory.md:96` uses `http://github.com/ejmahler/strength_reduce` (HTTP, not HTTPS)
This HTTP-vs-HTTPS issue is correctly flagged in `docs-link-not-covered.md` but is **missing from the link-coverage-report.md** external links section.
### Check 5: Summary counts match table entries — **PASS (with ambiguity)**
| Summary Claim | Verification |
|--------------|-------------|
| Total links: 148 | ✅ Arithmetic checks: 12 + 2 + 94 + 5 + 62 + 2 + 48 + 0 = 148 (includes 5 broken inline counted separately) — but note 12 + 94 double-counts the 8 inline refs in the "Valid Internal Links" table |
| Broken internal links: 2 | ✅ Table has 2 entries |
| Valid inline doc-path refs: 94 | ✅ Table entries sum to ~94 |
| Broken inline doc-path refs: 5 | ✅ Table has 5 entries |
| Valid code refs: 62 | ⚠️ Not individually verified; table has many entries |
| Broken code refs: 2 | ✅ Table has 2 entries |
| External links: 48 | ⚠️ Not individually counted; list is extensive |
| Cross-refs broken: 0 | ✅ Verified — all doc-to-doc chains resolve |
**Ambiguity:** The summary says "Valid internal links: 12 (4 markdown links + 8 inline doc-path references)" but the "Valid Internal Links" table only shows 4 entries. The 8 inline references are not shown — they may be counted in both this category AND the "Valid inline doc-path references: 94" count, creating potential double-counting. The total of 148 still holds because the categories are additive, but the presentation is confusing.
### Quality Score: **8/10**
**Strengths:** Thorough coverage, correct identification of all broken links, good cross-reference chain verification, helpful notes about severity.
**Errors:**
1. `flutter-license-inventory.md:12` LICENSE-MIT reference — should be line 11 (minor line-number error)
2. Missing flag for HTTP URL at `license-inventory.md:96` (inconsistency with sister report)
3. Ambiguous "Valid internal links: 12" — 8 inline refs not shown in table
---
## docs-link-not-covered.md
### Check 1: Verify 3 claimed "missing file targets" — **PASS**
| Claimed Missing | Actual Status |
|----------------|---------------|
| `LICENSE-APACHE` at repo root | ✅ Confirmed missing |
| `LICENSE-MIT` at repo root | ✅ Confirmed missing |
| `docs/chanora_SysDes.md` (and 4 siblings) | ✅ Confirmed missing — verified at `docs/sysrs.md:126-130` |
The sysrs.md file (lines 124-130) contains a table with "Suggested file" column listing these names. They are aspirational, not actual files. Correctly classified as "Low Impact."
### Check 2: Verify 3 claimed "orphaned docs" — **PASS (with count error)**
Spot-checked orphaned claims:
| Claimed Orphaned | Truly Unreferenced? |
|-----------------|-------------------|
| `docs/offline-knowledge/function-inventory.md` | ✅ Only referenced within `docs/offline-knowledge/README.md` — not from main doc tree |
| `docs/offline-knowledge/coverage-analysis.md` | ✅ Same situation |
| `docs/offline-knowledge/doc-quality-analysis.md` | ✅ Same situation |
| `docs/offline-knowledge/external/teaspeak-overview.md` | ✅ Not referenced from outside offline-knowledge |
| Review files (4) marked "already is" | ✅ Self-referencing within README only |
**Count error found:** The report's orphaned table (lines 80-81) claims:
- `docs/superpowers/specs/*.md (6 files)` — ✅ Confirmed: 6 files exist
- `docs/superpowers/plans/*.md (8 files)` — ❌ **Wrong count: 9 files exist**
Actual plans files:
1. `2026-05-28-server-resolution-prefetch.md`
2. `2026-05-28-chanora-server-prefetch-crate.md`
3. `2026-05-29-finish-dv-document-tree.md`
4. `2026-05-29-swe2-swe3-baselines.md`
5. `2026-05-29-state-sync-ui-settings-validation.md`
6. `2026-05-29-dv-evidence-pack.md`
7. `2026-06-06-chat-panel-switching.md`
8. `2026-06-08-core-internal-split.md`
9. `2026-06-08-maintainability-continuation.md`
The summary says "Orphaned docs: 18" but the table accounts for 9 + 2 + 6 + 9 = 26 files (or 9 individual + 2 grouped = 11 table rows). The "18" count is inconsistent with the actual file inventory.
### Check 3: Verify 3 suspicious URLs — **PASS**
| URL | Verification |
|-----|-------------|
| `https://git.did.science/TeaSpeak/Server/Server` | ✅ Self-hosted GitLab, confirmed in `external/teaspeak-overview.md`. Fragility risk is real. |
| `http://github.com/ejmahler/strength_reduce` | ✅ Uses HTTP instead of HTTPS. Confirmed at `license-inventory.md:96`. |
| `http://www.apache.org/licenses/` and `http://mozilla.org/MPL/2.0/` | ✅ HTTP URLs in license text bodies, confirmed present. |
### Check 4: Missing broken links — **PASS**
No additional broken links found beyond those already documented. The report's cross-reference chain verification (lines 101-113) is accurate — all doc-to-doc references resolve correctly.
One missed inconsistency: the `snapshot_state_mapper.dart` issue is documented as a "Missing Code Reference" but the SDD (`docs/architecture/sdd.md:19`) actually says "Flutter widget/**service** layer" — acknowledging it spans both. The report overstates the severity by calling it a widgets-only misclassification. This was already flagged in `reviews/docs-code-mismatch-review.md` as a severity overclaim.
### Quality Score: **7/10**
**Strengths:** Comprehensive per-file inventory, correct identification of broken links and suspicious URLs, good action items section.
**Errors:**
1. Plans file count wrong: says 8, actual is 9
2. Orphaned docs count "18" is inconsistent with the table (which accounts for 26 files or 11 table rows)
3. `snapshot_state_mapper.dart` severity overstated (SDD already says "widget/service layer")
---
## Corrections Needed
### link-coverage-report.md
1. **Line 38:** Change `flutter-license-inventory.md | 12` to `flutter-license-inventory.md | 11` for the LICENSE-MIT reference
2. **External links section:** Add `http://github.com/ejmahler/strength_reduce` from `license-inventory.md:96` to be consistent with the sister report
3. **Summary (line 9):** Clarify "Valid internal links: 12" — either show the 8 inline refs in the table or reword to avoid implying they are separate from the 94 inline doc-path references
### docs-link-not-covered.md
1. **Line 81:** Change `docs/superpowers/plans/*.md (8 files)` to `(9 files)`
2. **Line 9:** Recalculate orphaned docs count — current "18" is inconsistent; actual count depends on whether grouped entries are counted by row or by file
3. **Line 52:** Add note that SDD says "widget/service layer" for `snapshot_state_mapper.dart`, not purely "widgets"
---
## Overall Assessment
| File | Quality Score | Verdict |
|------|:------------:|---------|
| link-coverage-report.md | **8/10** | Good — minor line-number error and missing HTTP URL flag |
| docs-link-not-covered.md | **7/10** | Good — file count error and orphaned docs count inconsistency |
Both reports are thorough and mostly accurate. The broken link identification is correct across both files. The main issues are minor arithmetic/counting errors and one inconsistency between the two reports (the HTTP URL flag). No critical errors found.
@@ -0,0 +1,150 @@
# Review: docs-code-mismatch.md & docs-out-of-date.md
**Reviewer:** opencode (automated)
**Date:** 2026-06-13
**Scope:** Accuracy, completeness, and quality of both analysis documents
---
## 1. docs-code-mismatch.md
### 1.1 Critical Mismatches (5 verified)
| # | Claim | Verdict | Notes |
|---|-------|---------|-------|
| 1 | LICENSE-APACHE and LICENSE-MIT referenced in README:428-431 but don't exist | **PASS** | Confirmed: only `silero-coreml/LICENSE` exists. No LICENSE-APACHE or LICENSE-MIT at repo root. |
| 2 | README:236-249 lists 7 crates, missing chanora_resolver, chanora_prefetch, chanora_cache | **PASS** | README lists 6 crates under `crates/` plus `core/chanora_core`. Cargo.toml has 10 workspace members. Three missing. |
| 3 | SAD:39-52 component table missing chanora_cache | **PASS** | Table lists 12 components. chanora_cache exists in workspace (Cargo.toml:34, crates/chanora_cache/) but is absent from SAD. |
| 4 | SDD:19 snapshot_state_mapper.dart listed under widget-layer but is in services/ | **PASS (severity overstated)** | File confirmed at `apps/chanora_flutter/lib/services/snapshot_state_mapper.dart`. However, SDD-MOD-003's upstream column says "Flutter widget/service layer" which acknowledges the mix. Severity should be MINOR, not MAJOR. |
| 5 | tools/windows-smoke.md:6 references `product/scaffold-v0` branch | **PASS** | Line 5 confirmed. CHANGELOG:99 confirms default is now `main`. |
### 1.2 Major Mismatches (2 additional verified)
| # | Claim | Verdict | Notes |
|---|-------|---------|-------|
| 6 | sysrs.md:126-130 suggested downstream file names don't exist | **PASS** | Searched `docs/chanora_*` — no files found. Actual files use different names (sysdes.md, srs.md, etc.). |
| 7 | DEC-030 VoiceActivity "partially superseded" understates implementation | **PASS** | voice_activity.rs, transmit_mode.rs, vad/silero_onnx.rs all exist. Desktop VAD is implemented via capture path. Description is accurate. |
### 1.3 PASS Entries (2 verified)
| Entry | Verdict | Notes |
|-------|---------|-------|
| README.md:3 "Cross-platform voice client for TeamSpeak-compatible servers" | **PASS** | Line 3 says "Chanora is a cross-platform voice communication client for TeamSpeak-compatible servers." Correct. |
| README.md:8 "Flutter UI + Rust Core + tsclientlib" | **PASS** | Line 8 matches exactly. Correct. |
### 1.4 Random Doc File Check (2 files)
**File 1: `docs/release/dv-waiver-register.md`**
- Mismatch doc claims PASS for lines 14-23 (waiver list).
- No mismatches found. Correctly marked as PASS.
**File 2: `docs/privacy/privacy-policy.md`**
- Mismatch doc claims PASS for line 9 (TeamSpeak 3-compatible servers).
- No mismatches found. Correctly marked as PASS.
### 1.5 Errors Found
1. **Mismatch #4 severity overstated.** Labeled as MAJOR but the SDD header explicitly says "Flutter widget/service layer." Should be MINOR.
2. **Mismatch #7 (DEC-030) is a judgment call, not a clear mismatch.** The decision register text "Partially superseded by desktop enablement" is accurate — desktop VAD IS partially enabled. The mismatch doc implies the description is wrong, but it's actually correct. This should be downgraded to MINOR or removed.
3. **Mismatch #12 (commit type `release`).** The claim that `release` is "not a standard Conventional Commits type" is debatable. Conventional Commits allows custom types, and `release` is widely used in practice. This is more of a convention preference than a mismatch.
### 1.6 Missed Mismatches
None found in the two random doc files checked. The analysis appears thorough for the files reviewed.
### 1.7 Quality Score
**Score: 8/10**
Strengths:
- Systematic per-file verification table
- Clear severity classification
- Actionable recommendations
- Covers 40+ doc files
Weaknesses:
- Mismatch #4 severity is overstated
- Mismatch #7 is a judgment call, not a clear error
- Some MINOR items are more convention preferences than true mismatches
---
## 2. docs-out-of-date.md
### 2.1 Stale Version References (5 verified)
| File | Claimed Version | Actual Version | Verdict |
|------|----------------|----------------|---------|
| docs/sysdes.md | 0.9.8 | 0.9.8 (line 6) | **PASS** |
| docs/srs.md | 0.9.9 | 0.9.9 (line 6) | **PASS** |
| docs/sysrs.md | 0.9.11 | 0.9.11 (line 5) | **PASS** |
| docs/material3-guideline.md | 0.9.2 | 0.9.2 (line 4) | **PASS** |
| tools/windows-smoke.md | `product/scaffold-v0` | Confirmed (line 5) | **PASS** |
### 2.2 Outdated Docs (3 verified)
| Doc | Claim | Verdict |
|-----|-------|---------|
| docs/sysdes.md | 30 days stale, version 0.9.8 | **PASS** — Last change record 2026-05-14, confirmed 30 days stale. |
| docs/srs.md | 26 days stale, version 0.9.9 | **PASS** — Last change record 2026-05-18, confirmed 26 days stale. |
| docs/material3-guideline.md | 30 days stale, version 0.9.2 | **PASS** — Last change record 2026-05-14, confirmed 30 days stale. |
### 2.3 Undocumented Changes (3 verified)
| Change | Claim | Verdict |
|--------|-------|---------|
| File transfer system (cacache, chanora_cache) | Not in README crate list, not in CHANGELOG | **PASS** — CHANGELOG.md has no mention of file transfer, cacache, or chanora_cache. README crate list (lines 236-249) doesn't include chanora_cache. |
| Poke notifications | Not in CHANGELOG | **PASS** — CHANGELOG.md has no mention of poke. Poke files exist in code (poke_notification_service.dart, poke_limiter.rs, etc.). |
| Desktop Silero ONNX VAD | Not in CHANGELOG | **PASS** — CHANGELOG.md has no mention of silero_onnx or desktop ONNX VAD. File exists at `crates/chanora_audio/src/vad/silero_onnx.rs`. |
### 2.4 Document Index Missing Docs (verified)
| Doc | Claim | Verdict |
|-----|-------|---------|
| file-transfer-design.md | Missing from document-index.md | **PASS** — Not listed in document-index.md lines 12-32. File exists at `docs/architecture/file-transfer-design.md`. |
| file-transfer-research.md | Missing from document-index.md | **PASS** — Not listed. File exists at `docs/architecture/file-transfer-research.md`. |
| file-transfer-implementation-plan.md | Missing from document-index.md | **PASS** — Not listed. File exists at `docs/architecture/file-transfer-implementation-plan.md`. |
| poke-without-message-design.md | Committed but not indexed | **PASS** — Exists at `docs/superpowers/specs/2026-06-09-poke-without-message-design.md`. Not in document-index.md. |
### 2.5 Errors Found
1. **Line 147: "docs/governance/maintainability-review-2026-06-08.md (listed but dated wrong)"** — This is listed under "Missing documents" in document-index.md analysis, but the doc IS listed at document-index.md:29. The "dated wrong" claim is unclear — document-index.md has no date column. This is a minor inaccuracy in the out-of-date doc.
2. **Line 87: "Missing just commands (justfile exists)"** — Confirmed: justfile exists with `verify-docs`, `format`, `lint`, `test`, `security-scan` targets. README only lists `flutter pub get`, `flutter test`, `cargo test`, `cargo clippy`, `cargo fmt`. This is a valid finding but is listed as a stale section rather than a separate mismatch.
### 2.6 Missed Outdated Docs
None found. The analysis covers 64 docs comprehensively. The stale date references table (lines 26-42) is thorough.
### 2.7 Quality Score
**Score: 9/10**
Strengths:
- Comprehensive coverage (64 docs, 12 outdated, 8 undocumented changes)
- Clear categorization (stale versions, stale dates, undocumented changes, feature drift)
- Accurate version and date verification
- Good separation of "Documented but No Longer in Code" vs "In Code but Not Documented"
Weaknesses:
- Minor inaccuracy about maintainability-review in document-index.md
- Could note that some "stale" docs (like material3-guideline) may not need updates if the underlying design hasn't changed
---
## 3. Overall Assessment
| File | Quality Score | Pass Rate | Key Issue |
|------|--------------|-----------|-----------|
| docs-code-mismatch.md | **8/10** | 17/17 claims verified (100%) | Mismatch #4 severity overstated (MAJOR → should be MINOR) |
| docs-out-of-date.md | **9/10** | All claims verified (100%) | Minor inaccuracy about maintainability-review in document-index |
### Corrections Needed
1. **docs-code-mismatch.md line 32:** Change severity of mismatch #4 from MAJOR to MINOR. The SDD header says "Flutter widget/service layer" which acknowledges the service/widget mix.
2. **docs-code-mismatch.md line 47:** Consider downgrading mismatch #7 (DEC-030) to MINOR. "Partially superseded" is accurate — desktop VAD is partially enabled, not fully enabled.
3. **docs-out-of-date.md line 147:** Fix the claim about maintainability-review-2026-06-08.md being "listed but dated wrong" — it IS listed in document-index.md:29, and the index has no date column.
### Summary
Both documents are high-quality, thorough analyses. The docs-code-mismatch.md has a minor severity classification issue, and the docs-out-of-date.md has one factual error about the document index. Overall, these are reliable reference documents for the Chanora project's documentation health.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,468 @@
# Documentation Site & ASPICE Traceability System Design
**Date:** 2026-06-13
**Status:** Approved design for implementation
**Scope:** Docusaurus doc site, git submodule separation, tag-based ASPICE traceability with custom validation plugin, Cloudflare Pages hosting with access control
## 1. Purpose
Replace the current flat markdown documentation tree with a browseable, searchable, access-controlled doc site that serves three audiences: developers, ASPICE assessors, and non-technical stakeholders. Introduce automated traceability enforcement that validates the ASPICE requirement chain on every build.
## 2. Current State
- 65+ markdown files in `docs/` with no sidebar, no search, no visual hierarchy
- ASPICE traceability maintained in manual markdown tables (`traceability-matrix.md`)
- Cross-references are backtick-quoted paths in prose, not clickable links
- Link coverage report found 2 broken links + 5 broken path references
- No CI enforcement of traceability integrity
- No access control — docs only viewable via GitHub repo browsing or local clone
## 3. Design Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Repo structure | Git submodule (`docs/``chanora-docs` repo) | Cleaner separation, access control, CI independence, separate versioning |
| Doc site generator | Docusaurus | Meta-maintained, full plugin API, built-in tags and versioning, active ecosystem |
| Traceability mechanism | Tag-based + custom Docusaurus plugin | Tags for browsing, plugin for automated chain validation and coverage reports |
| Hosting | Cloudflare Pages | Free tier, global CDN, auto-deploy from CI |
| Access control | Cloudflare Access | Free for up to 50 users, email-based auth, SSO support |
| Code path references in docs | Remove from ASPICE docs, move to `impl-mapping.md` | ASPICE traces requirement IDs, not file paths. Code paths are developer convenience |
| Provenance records | Deferred | Completed ASPICE-related plans archived in `dev-docs/superpowers/plans/_archived/` for now |
## 4. Repo Structure
### 4.1 Docs submodule (`chanora-docs` repo)
```
chanora-docs/
├── docusaurus.config.ts
├── sidebars.ts
├── package.json
├── package-lock.json
├── tsconfig.json
├── docs/
│ ├── index.md
│ ├── requirements/
│ │ ├── sysrs.md
│ │ ├── sysdes.md
│ │ └── srs.md
│ ├── architecture/
│ │ ├── sad.md
│ │ ├── sdd.md
│ │ ├── file-transfer-design.md
│ │ ├── file-transfer-research.md
│ │ ├── file-transfer-implementation-plan.md
│ │ └── desktop-ptt-architecture.md
│ ├── verification/
│ │ ├── verification-master-plan.md
│ │ ├── swe4-unit-verification-plan.md
│ │ ├── swe5-software-integration-verification-plan.md
│ │ ├── swe6-software-verification-plan.md
│ │ └── sys4-system-integration-verification-plan.md
│ ├── governance/
│ │ ├── document-index.md
│ │ ├── traceability-matrix.md
│ │ ├── product-decision-register.md
│ │ ├── baseline-approval-record.md
│ │ ├── baseline-candidate-validation-report.md
│ │ ├── document-review-report.md
│ │ ├── document-naming-convention.md
│ │ ├── decision-impact-assessment.md
│ │ ├── git-commit-message-convention.md
│ │ ├── repo-format-validation-report.md
│ │ ├── path-migration-map.md
│ │ └── maintainability-review-2026-06-08.md
│ ├── security/
│ │ ├── security-privacy-legal-guideline.md
│ │ ├── threat-model.md
│ │ ├── secure-storage-audit-report.md
│ │ ├── diagnostic-redaction-audit-report.md
│ │ ├── dependency-and-supply-chain-report.md
│ │ ├── license-inventory.md
│ │ └── flutter-license-inventory.md
│ ├── privacy/
│ │ └── privacy-policy.md
│ ├── legal/
│ │ └── trademark-and-attribution-review.md
│ ├── release/
│ │ ├── platform-release-policy.md
│ │ ├── release-readiness-go-nogo-record.md
│ │ └── dv-waiver-register.md
│ ├── references/
│ │ ├── aspice-swe2-swe3-integration-note.md
│ │ ├── external-references.md
│ │ ├── yatqa-en.md
│ │ ├── yatqa-de.md
│ │ ├── teaspeak-overview.md
│ │ └── respeak-overview.md
│ ├── ui-ux/
│ │ ├── material3-guideline.md
│ │ ├── material3-design-tokens.md
│ │ ├── material3-component-catalog.md
│ │ └── adaptive-layout-platform-guide.md
│ └── i18n/
│ └── localization-architecture.md
├── plugins/
│ └── traceability/
│ └── index.js
├── scripts/
│ ├── validate-traceability.mjs
│ └── add-requirement-tags.mjs
├── src/
│ ├── pages/index.tsx
│ └── css/custom.css
├── static/
│ └── img/
├── .github/
│ └── workflows/
│ └── deploy.yml
├── wrangler.toml
└── README.md
```
### 4.2 Code repo local files
```
chanora/
├── docs/ → chanora-docs (submodule)
├── dev-docs/
│ ├── superpowers/
│ │ ├── specs/
│ │ │ ├── 2026-05-28-server-resolution-prefetch-design.md
│ │ │ ├── 2026-05-28-chanora-server-prefetch-crate-design.md
│ │ │ ├── 2026-05-29-state-sync-ui-settings-validation-design.md
│ │ │ ├── 2026-06-05-adaptive-3-panel-layout-design.md
│ │ │ ├── 2026-06-08-maintainability-continuation-design.md
│ │ │ ├── 2026-06-09-poke-without-message-design.md
│ │ │ └── 2026-06-13-documentation-site-design.md
│ │ └── plans/
│ │ ├── _archived/
│ │ │ ├── 2026-05-29-finish-dv-document-tree.md
│ │ │ ├── 2026-05-29-dv-evidence-pack.md
│ │ │ ├── 2026-05-29-swe2-swe3-baselines.md
│ │ │ └── 2026-05-29-state-sync-ui-settings-validation.md
│ │ ├── 2026-05-28-server-resolution-prefetch.md
│ │ ├── 2026-05-28-chanora-server-prefetch-crate.md
│ │ ├── 2026-06-06-chat-panel-switching.md
│ │ ├── 2026-06-08-core-internal-split.md
│ │ └── 2026-06-08-maintainability-continuation.md
│ ├── offline-knowledge/
│ │ ├── coverage-analysis.md
│ │ ├── doc-quality-analysis.md
│ │ ├── link-coverage-report.md
│ │ └── reviews/
│ ├── implementation-status-2026-05-28.md
│ ├── release/ios-build.md
│ └── impl-mapping.md
├── apps/, crates/, core/
├── AGENTS.md
└── README.md
```
## 5. Docusaurus Configuration
### 5.1 Site configuration (`docusaurus.config.js`)
```js
module.exports = {
title: 'Chanora Engineering Docs',
tagline: 'ASPICE-compliant engineering documentation with automated traceability',
url: 'https://docs.chanora.dev',
baseUrl: '/',
organizationName: 'chanoraapp',
projectName: 'docs',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
i18n: { defaultLocale: 'en', locales: ['en'] },
themes: ['@docusaurus/theme-classic'],
plugins: [
'./plugins/traceability',
],
themeConfig: {
navbar: {
title: 'Chanora Docs',
items: [
{ type: 'doc', position: 'left', label: 'Requirements', docId: 'requirements/sysrs' },
{ type: 'doc', position: 'left', label: 'Architecture', docId: 'architecture/sad' },
{ type: 'doc', position: 'left', label: 'Verification', docId: 'verification/verification-master-plan' },
{ type: 'doc', position: 'left', label: 'Governance', docId: 'governance/document-index' },
{ type: 'doc', position: 'left', label: 'Security', docId: 'security/security-privacy-legal-guideline' },
{ type: 'doc', position: 'left', label: 'Release', docId: 'release/platform-release-policy' },
{ type: 'doc', position: 'left', label: 'References', docId: 'references/external-references' },
{ type: 'doc', position: 'left', label: 'UI/UX', docId: 'ui-ux/material3-guideline' },
{ type: 'tags' },
],
},
footer: {
style: 'dark',
links: [
{ title: 'Docs', items: [
{ label: 'Requirements', to: '/docs/requirements/sysrs' },
{ label: 'Architecture', to: '/docs/architecture/sad' },
{ label: 'Verification', to: '/docs/verification/verification-master-plan' },
]},
{ title: 'Governance', items: [
{ label: 'Traceability Matrix', to: '/docs/governance/traceability-matrix' },
{ label: 'Decision Register', to: '/docs/governance/product-decision-register' },
{ label: 'Document Index', to: '/docs/governance/document-index' },
]},
],
},
prism: { theme: prismThemes.github, darkTheme: prismThemes.dracula },
},
};
```
### 5.2 Sidebar (`sidebars.js`)
```js
module.exports = {
requirements: [
'requirements/sysrs',
'requirements/sysdes',
'requirements/srs',
],
architecture: [
'architecture/sad',
'architecture/sdd',
'architecture/file-transfer-design',
'architecture/file-transfer-research',
'architecture/file-transfer-implementation-plan',
'architecture/desktop-ptt-architecture',
],
verification: [
{
type: 'category',
label: 'System Level',
items: ['verification/sys4-system-integration-verification-plan'],
},
{
type: 'category',
label: 'Software Integration',
items: ['verification/swe5-software-integration-verification-plan'],
},
{
type: 'category',
label: 'Unit Level',
items: ['verification/swe4-unit-verification-plan'],
},
{
type: 'category',
label: 'Software Qualification',
items: ['verification/swe6-software-verification-plan'],
},
'verification/verification-master-plan',
],
governance: [
'governance/document-index',
'governance/traceability-matrix',
'governance/product-decision-register',
'governance/baseline-approval-record',
'governance/baseline-candidate-validation-report',
'governance/document-review-report',
'governance/document-naming-convention',
'governance/decision-impact-assessment',
'governance/git-commit-message-convention',
'governance/repo-format-validation-report',
'governance/path-migration-map',
'governance/maintainability-review-2026-06-08',
],
security: [
'security/security-privacy-legal-guideline',
'security/threat-model',
'security/secure-storage-audit-report',
'security/diagnostic-redaction-audit-report',
'security/dependency-and-supply-chain-report',
'security/license-inventory',
'security/flutter-license-inventory',
'privacy/privacy-policy',
'legal/trademark-and-attribution-review',
],
release: [
'release/platform-release-policy',
'release/release-readiness-go-nogo-record',
'release/dv-waiver-register',
],
references: [
'references/external-references',
'references/aspice-swe2-swe3-integration-note',
'references/yatqa-en',
'references/yatqa-de',
'references/teaspeak-overview',
'references/respeak-overview',
],
uiux: [
'ui-ux/material3-guideline',
'ui-ux/material3-design-tokens',
'ui-ux/material3-component-catalog',
'ui-ux/adaptive-layout-platform-guide',
'i18n/localization-architecture',
],
};
```
## 6. Tag-Based Traceability
### 6.1 Front matter schema
Every document includes YAML front matter:
```yaml
---
tags: [swe.2, architecture, SRS-003, SRS-008, SRS-016]
upstream: [srs, sysdes] # Custom metadata for traceability plugin
downstream: [sdd, swe4, swe5] # Custom metadata for traceability plugin
lifecycle: SWE.2 # Custom metadata for traceability plugin
status: baseline # Custom metadata for traceability plugin
---
```
The `tags` field is consumed by the Docusaurus tags system for browsing. The `upstream`, `downstream`, `lifecycle`, and `status` fields are custom metadata consumed by the traceability plugin for chain validation.
### 6.2 Tag categories
| Tag pattern | Purpose | Example |
|---|---|---|
| `swe.1` through `swe.6`, `sys.4` | ASPICE lifecycle stage | Every doc gets at least one |
| `sysrs`, `sysdes`, `srs`, `sad`, `sdd` | Document type | Identifies the doc in the chain |
| `requirements`, `architecture`, `verification`, `governance` | Section category | For filtering |
| `SysRS-233`, `SRS-045`, `SDD-MOD-009` | Requirement/module IDs | Traceability links |
| `baseline`, `draft`, `candidate` | Document status | Assessor visibility |
| `dec-012`, `dec-020` | Decision register refs | Cross-ref to governance |
### 6.3 Section defaults
Docusaurus uses front matter `tags:` per document. Section-level defaults are not needed since each document carries its own tags.
### 6.4 Verification page trace mappings
| Verification plan | Upstream traces | Tags |
|---|---|---|
| SYS.4 System Integration | SysDes, SysRS | `[sys.4, verification, SysDes-102, SysDes-103, ...]` |
| SWE.5 Software Integration | SAD (SWE.2) | `[swe.5, verification, sad-component-bridge, ...]` |
| SWE.4 Unit Verification | SDD (SWE.3) | `[swe.4, verification, SDD-MOD-001, ...]` |
| SWE.6 Software Verification | SRS | `[swe.6, verification, SRS-128, ...]` |
## 7. Custom Traceability Plugin
### 7.1 Location
`plugins/traceability/index.js` — Docusaurus plugin, ~100 lines JavaScript.
### 7.2 Behavior
On `on_page_markdown` event:
- Scan each page for requirement ID patterns: `SysRS-\d+`, `SysDes-\d+`, `SRS-\d+`, `SDD-MOD-\d+`, `DEC-\d+`
- Build an in-memory traceability graph: upstream ID → downstream document → verification plan
On `on_post_build` event:
- Validate every requirement ID referenced downstream exists in its source document
- Validate every upstream document ID has at least one downstream allocation
- Flag orphaned references (IDs mentioned but never defined)
- Verify bidirectional completeness
### 7.3 Outputs
- `traceability-coverage.json` — machine-readable coverage report with chain completeness percentages
- Console output with pass/fail summary
- Traceability dashboard page with coverage table and broken chain details
- Build failure (`sys.exit(1)`) on broken chains when `strict: true`
### 7.4 Standalone CI validator
`scripts/validate-traceability.mjs` — same validation logic, runnable without Docusaurus build:
```
python scripts/validate_traceability.py docs/
```
Exit code 0 = all chains valid. Exit code 1 = broken chains with details on stderr.
## 8. Hosting & Deployment
### 8.1 Architecture
```
chanora-docs repo → push to main → GitHub Actions
→ validate-traceability.mjs
→ npm run build
→ Cloudflare Pages (via Wrangler)
→ Cloudflare Access policy (email-based auth)
```
### 8.2 CI workflow
On pull request: build + validate only (no deploy).
On push to main: build + validate + deploy to Cloudflare Pages.
### 8.3 Cloudflare Access policy
- Free tier for up to 50 users
- Email-based authentication with optional Google/GitHub SSO
- One-time PIN for external assessors
- Access rules: allow company emails, specific assessor emails; block all others
## 9. Migration Plan
### 9.1 Code path reference cleanup
SAD and SDD currently list file paths (`crates/chanora_protocol/src/`) in component tables. These references will be:
- Replaced with component/module IDs only in the docs submodule
- Preserved in `dev-docs/impl-mapping.md` in the code repo for developer convenience
### 9.2 File moves
| From (code repo) | To | Action |
|---|---|---|
| `docs/sysrs.md` | docs submodule | Move + add front matter |
| `docs/sysdes.md` | docs submodule | Move + add front matter |
| `docs/srs.md` | docs submodule | Move + add front matter |
| `docs/requirements/*` | docs submodule | Move (path records) |
| `docs/architecture/*` | docs submodule | Move + cleanup code paths |
| `docs/verification/*` | docs submodule | Move + add front matter |
| `docs/governance/*` | docs submodule | Move + add front matter |
| `docs/security/*` | docs submodule | Move + add front matter |
| `docs/privacy/*` | docs submodule | Move |
| `docs/legal/*` | docs submodule | Move |
| `docs/release/policy+go-nogo+waiver` | docs submodule | Move |
| `docs/references/*` | docs submodule | Move |
| `docs/ui-ux/*` | docs submodule | Move |
| `docs/i18n/*` | docs submodule | Move |
| `docs/material3-guideline.md` | docs submodule | Move |
| `docs/offline-knowledge/external/*` | docs submodule `references/` (flattened) | Move + rename |
| `docs/superpowers/*` | `dev-docs/superpowers/` | Move |
| `docs/offline-knowledge/` (remaining) | `dev-docs/offline-knowledge/` | Move |
| `docs/implementation-status-*` | `dev-docs/` | Move |
| `docs/release/ios-build.md` | `dev-docs/release/` | Move |
### 9.3 Cross-reference updates
All backtick path references (`docs/srs.md`) should become markdown links (`[SRS](../srs.md)` or `[SRS](srs.md)`) for both GitHub and Docusaurus rendering.
### 9.4 Post-migration
- Remove `docs/` contents from code repo
- Add `chanora-docs` as git submodule at `docs/`
- Create `dev-docs/` directory with local-only files
- Update README references to new paths
- Write `AGENTS.md` with new conventions
- Update `opencode.json` or `.opencode/` references
## 10. AGENTS.md
An `AGENTS.md` file will be written at the code repo root documenting:
- The two-repo model (docs/ as submodule, dev-docs/ as local)
- What content goes where
- ASPICE traceability chain and rules
- Code architecture overview
- Verification commands
- Agent working conventions (no edits in docs/ without submodule awareness)
## 11. Deferred Items
| Item | Reason | When |
|---|---|---|
| Document provenance records | Convert completed ASPICE plans into provenance evidence | Follow-up task |
| Custom Docusaurus traceability plugin | Core feature, built during implementation | Phase 1 |
| Cloudflare Pages + Access setup | Requires account creation, domain config | During deployment |
| `impl-mapping.md` creation | Extract code paths from SAD/SDD during migration | During migration |
Submodule
+1
Submodule docs added at 64e38f7d53
@@ -1,43 +0,0 @@
# Chanora Desktop Push-to-Talk Architecture
**Document status:** DV meeting baseline candidate
**Date:** 2026-05-29
**Related documents:** `docs/architecture/sad.md`, `docs/architecture/sdd.md`, `docs/release/dv-waiver-register.md`
## 1. Purpose
This document records the desktop push-to-talk architecture advertised by the README and connects it to the SWE.2/SWE.3 baselines.
## 2. Architecture Summary
Desktop PTT is implemented as a platform-capability feature. The application must detect the active backend, expose the resulting `PttCapabilityLevel`, and avoid claiming global PTT support when the runtime falls back to focused-input behavior.
## 3. Platform Backends
| Platform | Backend strategy | Release claim rule |
|---|---|---|
| Windows | Raw Input first, low-level keyboard hook fallback, focused fallback if unavailable | Claim only the detected runtime capability |
| macOS | Event Tap where permission and OS policy allow; focused fallback otherwise | Claim Global PTT only with permission/backend evidence |
| Linux | Freedesktop GlobalShortcuts portal where available; focused fallback otherwise | State portal/fallback behavior clearly |
## 4. Safety Rules
| Rule | Purpose |
|---|---|
| Missed-key-up watchdog clears transmit after timeout | Prevents stuck transmit when an OS suppresses key-up |
| Capability is surfaced to UI and release record | Prevents over-claiming platform support |
| Mouse side-button support is platform-dependent | Avoids blocking release on Linux portal limitations |
| Focused fallback remains available | Preserves usable PTT when global backends are unavailable |
## 5. Verification Handoff
| Evidence | Required result |
|---|---|
| Per-platform smoke | Active backend and fallback behavior recorded |
| UI inspection | PTT capability badge matches runtime backend |
| Release readiness | Release notes mirror actual capability per platform |
| Safety test | Watchdog prevents stuck transmit after missed key-up |
## 6. DV Conclusion
The desktop PTT architecture is documented for DV navigation. Public release claims still require per-platform PTT evidence attached to the release-readiness record.
-190
View File
@@ -1,190 +0,0 @@
# Chanora Software Architecture Description
**Lifecycle:** SWE.2 Software Architectural Design
**Document status:** DV meeting baseline candidate
**Date:** 2026-05-29
**Direct upstream source:** `docs/srs.md`
**Related system allocation:** `docs/sysdes.md`
## 1. Purpose
This Software Architecture Description defines Chanora's software architecture for DV review. It bridges SRS software requirements to SWE.3 detailed design and to SWE.5/SWE.6 verification planning.
This baseline captures the architecture visible in the current repository. It is sufficient for DV traceability review, while deeper per-module algorithms remain in `docs/architecture/sdd.md` and source-level design.
## 2. Architectural Scope
Chanora is a Flutter application with a Rust core. Flutter owns the user-facing shell, Material 3 widgets, localization, permission UX, and platform service presentation. Rust owns connection orchestration, protocol isolation, audio processing, storage coordination, diagnostics, server resolution, prefetch policy, and bridge DTOs.
## 3. Upstream SRS Allocation
| SRS group | Architectural allocation |
|---|---|
| SRS-003, SRS-008 through SRS-016 | Cross-platform app shell, Flutter UI, Rust core, platform adapters |
| SRS-017 through SRS-030 | Flutter UI, state presentation, connection and voice controls |
| SRS-031 through SRS-035 | Bridge layer and typed DTO boundary |
| SRS-036 through SRS-043 | Rust core connection lifecycle and state behavior |
| SRS-044 through SRS-053 | Protocol adapter and TeamSpeak-compatible server boundary |
| SRS-054 through SRS-061 | State synchronization and replay/reducer verification hooks |
| SRS-062 through SRS-083 | Audio subsystem, DSP, codec, PTT, mute/deaf, metering |
| SRS-084 through SRS-095 | Storage, secure storage, identity, diagnostics-sensitive data |
| SRS-096 through SRS-102 | Diagnostics, export, redaction, troubleshooting hooks |
| SRS-103 through SRS-123 | Platform adapters, packaging, release behavior |
| SRS-124 through SRS-143 | Verification support, analysis requirements, traceability rules |
| SRS-144 through SRS-184 | Material 3, adaptive UI, accessibility, localization, Unicode, app initialization |
| SRS-185 through SRS-218 | Platform baselines, PTT capability, transmit mode, no automatic telemetry, benchmark advisory |
## 4. Component Architecture
| Component | Repository location | Responsibility | Direct architectural dependencies |
|---|---|---|---|
| Flutter app shell | `apps/chanora_flutter/lib/main.dart`, services, widgets | App startup, screen composition, user actions, localization, Material 3 UI | Generated Rust bridge, platform plugins, Flutter services |
| Flutter service layer | `apps/chanora_flutter/lib/services/` | Permission flows, lifecycle policy, host prefetch debounce, link trust, state mapping, platform back intent | Flutter app shell, generated bridge APIs, platform plugins |
| Flutter widget layer | `apps/chanora_flutter/lib/widgets/` | Connect UI, channel tree, chat, voice controls, settings, diagnostics surfaces | Flutter services, generated DTOs, design tokens |
| Bridge layer | `crates/chanora_bridge`, `apps/chanora_flutter/lib/src/rust/` | Typed Flutter/Rust boundary and generated bindings | Rust core, Flutter generated code |
| Rust core | `core/chanora_core` | Connection lifecycle, orchestration, reconnect behavior, storage coordination, voice state, bridge-facing event DTOs | Protocol, audio, storage, diagnostics, state, resolver/prefetch |
| Protocol adapter | `crates/chanora_protocol` | Isolate `tsclientlib`, expose typed protocol DTOs/errors | Rust core, external compatible server |
| State sync | `crates/chanora_state` | Snapshot/delta model, channel join helpers, reducer behavior | Rust core, protocol DTOs |
| Audio subsystem | `crates/chanora_audio` | Capture/playback, Opus, DSP, PTT, voice activity reservation, platform units | Rust core, platform APIs, protocol audio path |
| Storage | `crates/chanora_storage` | Bookmarks, identities, encrypted local data, platform keyring integration | Rust core, platform secure storage |
| Diagnostics | `crates/chanora_diagnostics` | Redaction, log sink, export bundle, known-secret registry | Rust core, Flutter diagnostics UI |
| Server resolver | `crates/chanora_resolver` | SRV/TSDNS/DNS fallback resolution | Rust core, prefetch crate |
| Server prefetch | `crates/chanora_prefetch`, Flutter `prefetch_debouncer.dart` | Invisible host-field resolution warming, TTL cache, generation safety | Resolver, Flutter connect UI, Rust core |
## 5. Static Architecture View
```text
Flutter UI/widgets/services
-> generated Dart bridge API
-> chanora_bridge
-> chanora_core
-> chanora_protocol -> tsclientlib -> external compatible server
-> chanora_state
-> chanora_audio -> platform audio APIs / Opus / DSP
-> chanora_storage -> platform secure storage / SQLite
-> chanora_diagnostics
-> chanora_prefetch -> chanora_resolver -> network DNS/TSDNS
```
The bridge is the trust and type boundary between Flutter and Rust. Flutter must not directly depend on protocol-library internals. Rust core must not expose platform-specific storage or audio details to UI code except through stable DTOs and capability fields.
Current Core locality note: the public Core Interface remains available through `chanora_core::*` re-exports, while branch `simplify-project-review` has started moving internal Core responsibilities into focused Modules (`events.rs`, `network_diagnostics.rs`). This is an internal maintainability split, not a public Interface change.
## 6. Runtime Flow Architecture
### 6.1 Connect Flow
```text
User enters host/bookmark
-> Flutter connect widgets
-> optional prefetch debounce
-> bridge connect command
-> Rust core supervisor
-> resolver / prefetch cache
-> protocol adapter
-> external compatible server
-> state snapshot/events
-> bridge event stream
-> Flutter state mapper and widgets
```
### 6.2 Voice Flow
```text
Microphone / platform input
-> audio capture unit
-> DSP chain: HPF, NS, AEC, AGC where active
-> PTT/mute/transmit gate
-> Opus encode
-> protocol adapter
-> external compatible server
External server voice
-> protocol adapter
-> jitter/decode path
-> mixer / per-user controls
-> platform output
```
### 6.3 Diagnostics Flow
```text
Runtime event or error
-> diagnostic log sink / known-secret registry
-> redactor
-> user-initiated export bundle
-> Flutter share/export surface
```
## 7. Interface Catalogue
| Interface | Producer | Consumer | Architectural rule |
|---|---|---|---|
| Bridge command DTOs | Flutter generated API | `chanora_bridge`, Rust core | Stable typed DTOs; no raw protocol-library types cross to Flutter |
| Bridge event DTOs | Rust core / bridge | Flutter services/widgets | User-safe errors and capability fields are explicit |
| Protocol DTOs | `chanora_protocol` | Rust core, state sync | Protocol adapter isolates `tsclientlib` |
| Audio configuration | Flutter settings / Rust core | `chanora_audio` | Voice modes and processing flags are explicit; VAD remains disabled/deferred |
| Storage records | Storage crate | Rust core / Flutter UI via bridge | Secrets stay behind secure-storage abstraction |
| Diagnostic bundles | Diagnostics crate | Flutter diagnostics UI | Redaction runs before export or display |
| Platform capability records | Platform adapters/audio/PTT backends | UI and release record | UI/release wording must not over-claim capability |
## 8. Dependency Rules
| Rule | Rationale |
|---|---|
| Flutter UI depends on generated bridge APIs, not Rust internals | Keeps UI stable across Rust implementation changes |
| Rust core orchestrates crates but protocol/audio/storage crates remain separately testable | Supports SWE.4 unit verification and bounded responsibilities |
| Protocol adapter is the only component that owns `tsclientlib` coupling | Protects the app from protocol-library leakage |
| Diagnostics redaction must be reusable by runtime logging and export | Prevents split redaction behavior |
| Platform-specific behavior stays in platform adapters or audio platform units | Keeps cross-platform logic testable and reduces conditional sprawl |
| Release claims consume capability records and release evidence | Prevents over-claiming PTT, signing, packaging, or secure-storage behavior |
## 9. Non-Functional Allocation
| Concern | Architectural mechanism | Verification owner |
|---|---|---|
| Real-time audio responsiveness | Rust audio subsystem, benchmark advisory, bounded callback behavior | Audio / Platform QA |
| Privacy and no automatic telemetry | User-initiated diagnostics, no automatic upload policy | Security / Privacy QA |
| Secure secret handling | Platform secure-storage abstraction and encrypted local storage | Security / QA |
| Cross-platform UI | Flutter Material 3, design tokens, responsive widgets | Software QA / UX |
| Protocol compatibility | `tsclientlib` adapter isolation and compatible-server matrix | Protocol / Integration QA |
| Release reproducibility | CI, build scripts, artifact hashes, release-readiness record | Release / Operations QA |
## 10. Architectural Decisions Captured by This Baseline
| Decision | Architectural outcome |
|---|---|
| Flutter + Rust split | Flutter owns presentation; Rust owns protocol/audio/storage/diagnostics core behavior |
| `tsclientlib` isolation | Protocol compatibility is behind `chanora_protocol` |
| Secure storage abstraction | Platform storage details do not leak into UI or unrelated crates |
| Advisory audio benchmarks | Performance regressions are surfaced without making CI a hard release gate at this stage |
| PTT capability levels | Platform PTT support is represented as capability data and must match release wording |
| VoiceActivity deferral | `VoiceActivity` remains reserved/disabled until a later baseline allocates implementation |
| No automatic diagnostic upload in MVP | Diagnostics are local and user-initiated unless future approved requirements change policy |
## 11. Verification Handoff
| Verification plan | SAD handoff |
|---|---|
| SWE.4 | Component boundaries define unit-test ownership for Flutter services/widgets and Rust crates |
| SWE.5 | Interface catalogue and runtime flows define integration paths |
| SWE.6 | SRS allocation and acceptance flows define software acceptance evidence |
| SYS.4 | Platform capability and external-server boundaries define system integration evidence |
## 12. Traceability to SRS
This SAD derives only from `docs/srs.md`. The broad SRS group-to-component allocation in section 3 is the controlling SWE.2 trace for DV. Detailed item-level trace is represented by the SRS coverage matrix and `docs/governance/traceability-matrix.md`.
## 13. Open Architecture Risks
| Risk | Impact | Control |
|---|---|---|
| SAD item numbering from historical status references is not reconstructed in this baseline | Existing references such as `SAD-043` and `SAD-046` are not itemized here | Treat this as a DV baseline SAD; add itemized SAD IDs in a follow-up if process requires strict ID-level review |
| Some architecture views are textual rather than C4 diagrams | Reviewers may request visual C4 views | Record as documentation hardening, not a blocker for DV baseline if textual views are accepted |
| Release/platform architecture evidence is incomplete | Public release remains blocked | Controlled by release-readiness and waiver records |
| Android runtime verification is not automatic in local reviews | Android permission/audio/lifecycle regressions can pass Rust-only tests | Require `adb devices -l` with a connected device/emulator and Android smoke evidence before claiming Android runtime success |
| Protocol voice packet re-export is an intentional exception to full protocol isolation | Future changes may accidentally widen the protocol/audio Seam | Document and keep the voice wire exception narrow, or move packet construction fully into `chanora_protocol` |
## 14. DV Conclusion
This SWE.2 baseline is sufficient to remove the missing-SAD traceability gap for DV review. It does not replace candidate test evidence or final release approval.
-161
View File
@@ -1,161 +0,0 @@
# Chanora Software Detailed Design
**Lifecycle:** SWE.3 Software Detailed Design and Unit Construction Handoff
**Document status:** DV meeting baseline candidate
**Date:** 2026-05-29
**Direct upstream source:** `docs/architecture/sad.md`
**Related software requirements:** `docs/srs.md`
## 1. Purpose
This Software Detailed Design defines the module-level design details needed for SWE.4 unit verification and SWE.5 integration verification. It is based on the current repository layout and the SWE.2 architecture baseline.
## 2. Module Catalogue
| SDD module | Source location | Primary responsibility | Upstream SAD component |
|---|---|---|---|
| SDD-MOD-001 Flutter app bootstrap | `apps/chanora_flutter/lib/services/app_bootstrap.dart`, `main.dart` | Initialize Rust bridge, localization, app services, theme/design baseline | Flutter app shell |
| SDD-MOD-002 Connect UI | `apps/chanora_flutter/lib/widgets/connect_widgets.dart` | Host/bookmark inputs, connect actions, pre-request UX | Flutter widget layer |
| SDD-MOD-003 Snapshot and channel UI | `snapshot_view.dart`, `snapshot_state_mapper.dart`, `channel_spacer.dart` | Present channel tree, clients, and mapped state | Flutter widget/service layer |
| SDD-MOD-004 Chat UI | `chat_views.dart`, `bbcode_text.dart` | Channel text rendering and BBCode-safe display | Flutter widget layer |
| SDD-MOD-005 Voice UI | `voice_bar.dart`, `voice_compact.dart`, `voice_settings*.dart`, `voice_level_meter.dart`, `ptt_capability_badge.dart` | Voice controls, processing settings, metering, PTT capability | Flutter widget layer |
| SDD-MOD-006 Platform services | `android_permissions_service.dart`, `ios_permissions_service.dart`, `audio_lifecycle_service.dart`, `back_intent_*`, `link_trust_service.dart` | Permission, lifecycle, navigation, route/link trust behavior | Flutter service layer |
| SDD-MOD-007 Bridge API | `crates/chanora_bridge/src/api.rs`, generated Dart/Rust bridge files | Typed command/event boundary | Bridge layer |
| SDD-MOD-008 Rust core supervisor | `core/chanora_core/src/lib.rs`, `events.rs`, `network_diagnostics.rs`, `ptt.rs` | Connection orchestration, reconnect, bridge-facing event DTOs, network diagnostics, PTT state, storage coordination | Rust core |
| SDD-MOD-009 Protocol adapter | `crates/chanora_protocol/src/` | `tsclientlib` isolation, DTO/error mapping | Protocol adapter |
| SDD-MOD-010 State sync | `crates/chanora_state/src/lib.rs`, `channel_join.rs` | Snapshot/delta model, reducer, channel join support | State sync |
| SDD-MOD-011 Audio subsystem | `crates/chanora_audio/src/` | Audio capture/playback, DSP, Opus, PTT, mode stack, platform units | Audio subsystem |
| SDD-MOD-012 Storage | `crates/chanora_storage/src/lib.rs` | Bookmarks, identity storage, encrypted local records, keyring abstraction | Storage |
| SDD-MOD-013 Diagnostics | `crates/chanora_diagnostics/src/lib.rs` | Redaction, log sink, known-secret registry, export bundle | Diagnostics |
| SDD-MOD-014 Resolution and prefetch | `crates/chanora_resolver/src/lib.rs`, `crates/chanora_prefetch/src/lib.rs`, `prefetch_debouncer.dart` | SRV/TSDNS/DNS fallback and generation-safe resolution warming | Server resolver / prefetch |
| SDD-MOD-015 Build and release hooks | `.github/workflows/`, `tools/`, platform project files | CI, unsigned iOS build, benchmark advisory, platform smoke procedures | Release / platform architecture |
## 3. Bridge Boundary Design
The bridge boundary is the only supported Flutter-to-Rust command path. Dart code uses generated APIs under `apps/chanora_flutter/lib/src/rust/`; Rust exposes bridge functions through `crates/chanora_bridge/src/api.rs`.
Design rules:
| Rule | Detail |
|---|---|
| DTO stability | DTO fields must be explicit and serializable through Flutter Rust Bridge generation |
| Error safety | Rust errors exposed to Flutter must be user-safe or mapped before display |
| Secret handling | Secrets may cross only as command inputs or protected DTO fields and must be registered for diagnostic redaction where relevant |
| Capability reporting | Platform and PTT capability fields must reflect actual active backend state |
| Regeneration control | Generated bridge files are implementation artifacts and must be regenerated when bridge API signatures change |
## 4. Connection and State Design
| Detail | Design |
|---|---|
| Connection lifecycle | Rust core owns connect/disconnect/reconnect decisions and suppresses reconnect after user disconnect |
| Backoff | Reconnect uses exponential backoff as described in implementation status, capped at 60 seconds |
| Core internal Modules | `lib.rs` remains the public Interface and orchestration entry point; `events.rs` owns public event/bridge-facing DTOs re-exported by `lib.rs`; `network_diagnostics.rs` owns private connect/loss counters and the last-loss ring buffer |
| Server resolution | Resolver performs SRV/TSDNS/DNS fallback; prefetch cache may warm but must not be required for connect success |
| Snapshot mapping | Rust state and bridge DTOs are mapped into Flutter view models by `snapshot_state_mapper.dart` |
| Channel join | Channel join logic and errors are represented through Rust state/protocol handling and Flutter error mapper service |
| Reducers | `chanora_state` owns snapshot/delta reducer design with unit coverage for snapshot, delta, reconnect, duplicate normalization, disconnected/lost suppression, unknown-client voice activity, deterministic ordering, and channel-delete/client cleanup. Current runtime UI refresh still flows through `chanora_core` snapshot/probe paths; full live-event folding through `chanora_state::reduce` is an integration follow-up. |
## 5. Audio Detailed Design
| Audio element | Design detail |
|---|---|
| Capture/playback | Platform-specific units handle Android, iOS, desktop/fallback paths behind Rust audio abstractions |
| Codec | Opus encode/decode lives in `opus_voice.rs` and associated audio modules |
| DSP chain | High-pass filter, noise suppression, echo cancellation, and AGC are represented by audio processing modules/backends |
| Transmit control | `TransmitMode` supports `Ptt`, `Continuous`, and reserved `VoiceActivity`; `VoiceActivity` has no active MVP implementation |
| VoiceActivity gate (capture-side) | `voice_activity::VoiceActivityStateMachine` is the 10 ms-cadence gate for `TransmitMode::VoiceActivity`; open-after 40 ms (debounce), hangover 500 ms (anti-chatter), min-tx 200 ms (anti-flicker), weak-hold 30-100 frames (anti-stale-VAD); live `configure()` re-clamps existing timers on settings change without resetting state; 9 unit tests cover the main paths |
| PTT | Desktop/mobile backends expose capability level and active backend; missed-key-up watchdog prevents stuck transmit |
| Release tail | Tail handling prevents abrupt cutoffs after PTT release where configured |
| Render peak limiter | `voice_render::limit_peak_inplace` is a single-pass, allocation-free per-frame peak scaler applied in both the macOS and iOS render callbacks before the i16 downmix; default threshold 0.99 prevents hard clipping on multi-client mixes that sum past 0 dBFS while remaining transparent for normal voice levels (allocation-free, lock-free, safe on the realtime audio thread) |
| macOS render cadence (producer + ring) | `ios_voice_unit.rs:851-961` runs a 20 ms tokio producer task that calls `AudioHandler::fill_buffer(1920)` and `force_push`es each sample into a `crossbeam ArrayQueue<f32>` (SPSC-effective, MPMC-but-wait-free-per-end); ring capacity 12000 samples ≈ 6.25× pull quantum; 100 ms prebuffer (`PREBUFFER_SAMPLES = 9600` stereo f32) before the VPIO render callback starts draining, matching Mumble's playout margin and WebRTC's kStartDelayMs order of magnitude |
| iOS render cadence (direct-fill) | `ios_voice_unit.rs:968-1034` does `AudioHandler::fill_buffer` directly in the VPIO render callback (VPIO on iOS requests 480-frame ≈ 10 ms slices that align with tsclientlib's 20 ms Opus frame); scratch buffer preallocated to 4096×2 f32 at setup time so the realtime callback never `resize()`s; `try_lock` (not `lock`) on the AudioHandler mutex so contention never stalls the realtime IO thread; on `WouldBlock` the callback emits silence and increments `callback_xrun` |
| VPIO ducking config (macOS 14+) | `ios_voice_unit.rs` writes an 8-byte `AuVoiceIoOtherAudioDuckingConfiguration` struct (`m_enable_advanced_ducking = 0` disables dynamic voice-activity-driven ducking; `m_ducking_level = kAUVoiceIOOtherAudioDuckingLevelMin = 10`) to selector `kAUVoiceIOProperty_OtherAudioDuckingConfiguration` (= 2108) on the VoiceProcessingIO AudioUnit at startup, minimising the ducking of other apps' audio during a voice session; on macOS 13 the property is silently ignored (VPIO returns the default ducking behaviour) and the code logs a debug message and continues |
| Benchmarks | Realtime capture, Opus, and resampler benchmarks provide advisory baseline evidence |
## 6. Storage and Secret Design
| Storage item | Design detail |
|---|---|
| Bookmarks | Stored locally through the storage crate and surfaced in Flutter connect UI |
| Identity references | Stored through `IdentityFileStore` and platform secure storage where available |
| Passwords/secrets | Encrypted at rest using the current storage design; Android Keystore-backed DEK is deferred and must be disclosed |
| CI keyring behavior | CI disables real keyring access with `CHANORA_DISABLE_KEYRING=1` to avoid headless blocking |
| Fallback behavior | Platform fallback modes must be represented as limitations in release/security evidence |
## 7. Diagnostics Detailed Design
| Diagnostic element | Design detail |
|---|---|
| Log sink | Runtime logs can be captured by diagnostic sinks for export |
| Known-secret registry | Runtime secrets are registered for redaction where applicable |
| Redactor | Redacts configured sensitive patterns before export |
| Export bundle | Diagnostic export is JSON-based and user-initiated |
| Upload policy | MVP has no automatic diagnostic, telemetry, or crash upload |
## 8. Flutter UI Detailed Design
| UI area | Design detail |
|---|---|
| Design tokens | `chanora_tokens.dart` centralizes product styling over Material 3 |
| Platform capability display | `platform_capabilities.dart` and PTT capability widgets expose platform-specific support honestly |
| Localization | Generated localization files provide English and Simplified Chinese resources |
| Responsive behavior | Current widgets support compact/mobile-oriented layouts; expanded side-pane hardening remains P1/P2 as recorded |
| Accessibility | Critical status should use text/icons/semantics and not color alone; verification remains through UI tests/audit |
| UI settings persistence | `UiPreferencesService` persists host, nickname, permission explanation state, and theme mode through `shared_preferences`; invalid stored theme values fall back to system theme |
## 9. Build and Release Detailed Design
| Build/release item | Design detail |
|---|---|
| Rust CI | `.github/workflows/ci.yml` runs cargo check/test and advisory clippy |
| Flutter CI | `.github/workflows/ci.yml` runs Flutter pub get, analyze, and tests |
| Supply chain | CI runs cargo-deny and license inventory checks |
| iOS unsigned build | CI runs `flutter build ios --release --no-codesign` |
| Audio benchmarks | `bench-advisory.yml` runs audio benchmarks and posts advisory evidence |
| Platform packages | Public binary packaging/signing/notarization remains release-gated |
## 10. Verification Hook Design
| Module | SWE.4 unit hooks | SWE.5/SWE.6 integration hooks |
|---|---|---|
| Flutter services/widgets | Dart unit/widget tests under `apps/chanora_flutter/test/` | Widget/system demos and candidate device smoke |
| Bridge | API compile/generation checks | Flutter-to-Rust command/event smoke |
| Rust core | Cargo tests | Compatible-server lifecycle demo |
| Protocol | DTO/error mapping tests | Protocol compatibility matrix and server demo |
| State sync | Reducer tests | Snapshot/delta/reconnect integration evidence |
| Audio | DSP/codec/PTT tests and benchmarks | Platform audio loopback/device demo |
| Storage | Repository/encryption/keyring-disabled tests | Platform secure-storage audit |
| Diagnostics | Redaction/export tests | User-initiated export inspection |
| Release hooks | CI workflow validation | Release readiness record and artifact evidence |
## 11. Traceability to SAD
| SAD component | SDD modules |
|---|---|
| Flutter app shell | SDD-MOD-001 |
| Flutter service layer | SDD-MOD-006, SDD-MOD-014 |
| Flutter widget layer | SDD-MOD-002 through SDD-MOD-005 |
| Bridge layer | SDD-MOD-007 |
| Rust core | SDD-MOD-008 |
| Protocol adapter | SDD-MOD-009 |
| State sync | SDD-MOD-010 |
| Audio subsystem | SDD-MOD-011 |
| Storage | SDD-MOD-012 |
| Diagnostics | SDD-MOD-013 |
| Server resolver/prefetch | SDD-MOD-014 |
| Release/platform architecture | SDD-MOD-015 |
## 12. Open Detailed-Design Risks
| Risk | Impact | Control |
|---|---|---|
| Detailed item IDs from historical SDD references are not reconstructed | Existing references such as `SDD-109` are not itemized in this baseline | Treat this as a DV baseline SDD and add strict item numbering later if required |
| Some module designs are summarized rather than API-by-API | May be insufficient for final process audit | Use this as DV baseline; deepen high-risk modules before final release gate |
| Android Keystore-backed DEK is not implemented | Limits storage/security design claims | Controlled by waiver and release-readiness records |
| Full event replay tooling and live reducer integration evidence are absent | Limits state verification design beyond reducer unit behavior | Controlled as P1 gap and runtime-integration follow-up |
| Android runtime smoke is blocked when no device/emulator is attached | Android permission/audio/lifecycle paths cannot be claimed from Rust tests alone | Require `adb devices -l` and Android smoke evidence before closing Android verification claims |
## 13. DV Conclusion
This SWE.3 baseline is sufficient to remove the missing-SDD traceability gap for DV review and to feed SWE.4/SWE.5 verification plans. It does not close release evidence gaps or replace source-level tests.
-21
View File
@@ -1,21 +0,0 @@
# System Design Specification
**Document status:** DV entry-point record
**Canonical document:** `../sysdes.md`
The canonical Chanora System Design Specification currently lives at `docs/sysdes.md`. This file preserves the README-advertised path `docs/architecture/sysdes.md` for DV navigation.
Reviewers shall use `docs/sysdes.md` as the authoritative SysDes baseline until the repository migration moves the canonical file into this directory.
## DV Review Summary
| Topic | Canonical source |
|---|---|
| System element allocation | `docs/sysdes.md` sections 4 through 8 |
| Verification handoff | `docs/sysdes.md` section 12, SysDes-102 through SysDes-107 |
| SysRS-to-SysDes allocation matrix | `docs/sysdes.md` Appendix A |
| Change-control and traceability rules | `docs/sysdes.md` SysDes-108 through SysDes-110 |
## DV Position
The SysDes baseline is reviewable for DV. SRS, SAD, SDD, and verification documents derive from or consume this allocation layer.
@@ -1,32 +0,0 @@
# Chanora Baseline Approval Record
**Document status:** DV meeting baseline candidate
**Date:** 2026-05-29
## 1. Approval Scope
This record tracks approval for documentation baselines. It does not approve public/store release.
| Baseline | Status | DV disposition |
|---|---|---|
| SysRS | Baseline available | Reviewable |
| SysDes | Baseline available | Reviewable |
| SRS | Baseline available | Reviewable |
| SAD / SWE.2 | Baseline candidate | Reviewable with depth limitation |
| SDD / SWE.3 | Baseline candidate | Reviewable with depth limitation |
| Verification plans | Baseline candidate | Reviewable with evidence limitations |
| Release readiness | Baseline candidate | No-Go for public/store release |
## 2. Required Approvers
| Area | Approver role |
|---|---|
| Requirements/design | System Engineering / Software Engineering |
| Verification | Software QA / System QA |
| Release | Product / Release Operations |
| Security/privacy | Security / Privacy owner |
| Legal/trademark/OSS | Legal / Product owner |
## 3. DV Recommendation
Approve the document baseline for DV discussion. Do not approve public/store release until release gates close.
@@ -1,38 +0,0 @@
# Chanora Baseline Candidate Validation Report
**Document status:** DV meeting baseline candidate
**Date:** 2026-05-29
## 1. Validation Summary
Validation was performed against the README-advertised document tree and the DV lifecycle chain.
| Check | Result |
|---|---|
| README document tree represented | Baseline candidate documents exist or path records point to canonical baselines |
| SysRS/SysDes/SRS chain | Available |
| SAD/SDD chain | Baseline candidates added |
| Verification plans | Available |
| Release readiness | Available; public/store release No-Go |
| Waivers | Available |
## 2. Validation Method
| Validation step | Evidence |
|---|---|
| README path coverage | Shell `test -f` command over every advertised document path |
| Sentinel-language scan | Review scan over the DV document tree returned no incomplete-marker matches |
| Lifecycle trace | Traceability matrix covers SysRS -> SysDes -> SRS -> SAD -> SDD -> Verification |
| Release posture | Release-readiness record and legal/security docs consistently keep public/store release at No-Go |
## 3. Known Validation Limits
| Limit | Control |
|---|---|
| Candidate run IDs not embedded | Release-readiness record requires run IDs before release approval |
| Legal DEC-012 open | Public/store release remains blocked |
| Some SAD/SDD details summarized | Deepen before final process audit if required |
## 4. Conclusion
The baseline candidate is suitable for DV meeting review with recorded limitations.
@@ -1,18 +0,0 @@
# Chanora Decision Impact Assessment
**Document status:** DV meeting baseline candidate
**Date:** 2026-05-29
## 1. Impact Matrix
| Decision or gap | Requirements impact | Design impact | Verification impact | Release impact |
|---|---|---|---|---|
| DEC-012 open | Public wording and OSS gates remain constrained | Legal surfaces must avoid over-claiming | Audit evidence required | Public/store release No-Go |
| Android Keystore-backed DEK deferred | Secure-storage claim limited | Storage design carries fallback limitation | Platform audit required | Waiver required |
| Full reducer tests incomplete | State verification incomplete | State design remains valid but evidence partial | SWE.4/SWE.5 partial | Blocks full state-sync claim |
| Desktop/iOS artifacts not release-ready | Platform packaging requirements partial | Release design remains source-build/unsigned | SYS.4 evidence partial | Public binary release No-Go |
| VAD deferred | VoiceActivity not active | UI must show disabled/coming-soon | No VAD pass claim | No VAD marketing claim |
## 2. Conclusion
Current impacts are controlled for DV by waivers and release No-Go status.
-36
View File
@@ -1,36 +0,0 @@
# Chanora Document Index
**Document status:** DV meeting baseline candidate
**Date:** 2026-05-29
## 1. Purpose
This index lists the documents required for DV review and identifies their current status.
## 2. Baseline Documents
| Area | Document | Status |
|---|---|---|
| Requirements | `docs/sysrs.md` | Canonical SysRS baseline |
| Requirements | `docs/srs.md` | Canonical SRS baseline |
| Requirements path | `docs/requirements/sysrs.md`, `docs/requirements/srs.md` | Path records pointing to canonical root files |
| System design | `docs/sysdes.md` | Canonical SysDes baseline |
| System design path | `docs/architecture/sysdes.md` | Path record pointing to canonical root file |
| Software architecture | `docs/architecture/sad.md` | SWE.2 baseline candidate |
| Software detailed design | `docs/architecture/sdd.md` | SWE.3 baseline candidate |
| Verification | `docs/verification/verification-master-plan.md` | Verification baseline candidate |
| Verification | `docs/verification/swe4-unit-verification-plan.md` | SWE.4 baseline candidate |
| Verification | `docs/verification/swe5-software-integration-verification-plan.md` | SWE.5 baseline candidate |
| Verification | `docs/verification/swe6-software-verification-plan.md` | SWE.6 baseline candidate |
| Verification | `docs/verification/sys4-system-integration-verification-plan.md` | SYS.4 baseline candidate |
| Release | `docs/release/release-readiness-go-nogo-record.md` | No-Go for public/store release |
| Release | `docs/release/platform-release-policy.md` | Baseline candidate |
| Governance | `docs/governance/traceability-matrix.md` | DV baseline candidate |
| Governance | `docs/governance/maintainability-review-2026-06-08.md` | Working-branch maintainability and fail-safe review |
| Security/privacy/legal | `docs/security/security-privacy-legal-guideline.md` | Baseline candidate |
| Privacy | `docs/privacy/privacy-policy.md` | Engineering baseline candidate |
| Legal | `docs/legal/trademark-and-attribution-review.md` | DEC-012 open |
## 3. DV Use
Use this index as the first navigation document in the DV meeting. Release approval remains controlled by the release-readiness record.
@@ -1,33 +0,0 @@
# Chanora Document Naming Convention
**Document status:** DV meeting baseline candidate
**Date:** 2026-05-29
## 1. Rule
Repository documentation uses lowercase kebab-case file names under topic directories.
Examples:
| Document type | Path pattern |
|---|---|
| Requirements | `docs/requirements/<name>.md` or canonical root file during migration |
| Architecture | `docs/architecture/<artifact>.md` |
| Verification | `docs/verification/<lifecycle>-verification-plan.md` |
| Release | `docs/release/<record-name>.md` |
| Governance | `docs/governance/<record-name>.md` |
## 2. Identifier Rules
| Identifier | Meaning |
|---|---|
| `SysRS-XXX` | System requirement |
| `SysDes-XXX` | System design item |
| `SRS-XXX` | Software requirement |
| `SAD` | Software Architecture Description / SWE.2 |
| `SDD` | Software Detailed Design / SWE.3 |
| `DV-WVR-XXX` | DV waiver |
## 3. Migration Rule
When canonical files move, path records must be replaced by the canonical content or by redirects that clearly identify the authoritative source.
-37
View File
@@ -1,37 +0,0 @@
# Chanora Document Review Report
**Document status:** DV meeting baseline candidate
**Date:** 2026-05-29
## 1. Review Method
Documents were reviewed for DV navigation, lifecycle coverage, explicit release gating, and absence of unsupported completion claims.
| Review attribute | Value |
|---|---|
| Review date | 2026-05-29 |
| Review scope | `docs/requirements/`, `docs/architecture/`, `docs/verification/`, `docs/release/`, `docs/security/`, `docs/privacy/`, `docs/legal/`, `docs/ui-ux/`, `docs/i18n/`, `docs/governance/`, `docs/references/` |
| Review criteria | README path coverage, lifecycle traceability, release-status consistency, waiver visibility, security/privacy/legal gate visibility, sentinel-language scan |
| Evidence commands | README path `test -f` check; sentinel-language scan over the DV document tree |
## 2. Findings
| Finding | Status | Action |
|---|---|---|
| Previous gap: verification document set was missing | Addressed | Added verification master/SWE.4/SWE.5/SWE.6/SYS.4 plans |
| Previous gap: SAD/SDD were missing from current tree | Addressed | Added SWE.2/SAD and SWE.3/SDD baselines |
| Release blockers needed explicit record | Addressed | Added release-readiness record and waiver register |
| README tree had missing documents | Addressed for DV | Added baseline candidate records and path records |
## 3. Residual Issues
| Residual issue | DV handling |
|---|---|
| Candidate commit SHA, tag, artifact hashes, and run IDs are not recorded | Blocks release approval; acceptable for document baseline review |
| SAD/SDD are baseline candidates without historical item-number reconstruction | Accept for DV baseline; deepen before strict item-level audit |
| DEC-012 legal/trademark/OSS review remains open | Public/store release remains No-Go |
| Some implementation status items require revalidation after newer commits | Controlled by waiver register and release-readiness record |
## 4. Review Conclusion
The document set is reviewable for DV. Public/store release remains blocked by the release-readiness record.

Some files were not shown because too many files have changed in this diff Show More