Adopt a call-scoped VoIP audio session lifecycle so other apps' audio is
not stopped while Chanora is idle and the in-call session does not get
clobbered by media-server resets unrelated to voice.
AppDelegate.swift
- Set .ambient + .mixWithOthers as the idle baseline so the app does not
hold a VoiceChat session when no call is active.
- Switch to .playAndRecord + .voiceChat + .mixWithOthers + .duckOthers
on demand via the new chanora/ios_audio_session MethodChannel, and
revert to .ambient on deactivate.
- Gate the media-services-reset rebuild on voiceSessionActive so a
stray reset during idle no longer reactivates VoiceChat.
ios_audio_session_controller.dart (new)
- Thin Dart wrapper around chanora/ios_audio_session with activate/
deactivate; no-op on non-iOS; swallows PlatformException to keep
audio start/stop resilient to platform-side races.
main.dart
- Activate the iOS audio session on BridgeEvent_AudioStarted, deactivate
on BridgeEvent_AudioStopped, fire-and-forget via unawaited().
ios_voice_unit.rs
- Add the 10 local bindings required by the render-callback closure
preamble (wav_recorder_for_render, render_recorder_active,
render_ref_len, render_ref_accum, cb_count, last_num_frames,
num_frames_changes, callbacks_with_audio, callbacks_with_silence)
so the iOS target compiles cleanly with the new lifecycle wiring.
Tests
- 5 unit tests in test/services/ios_audio_session_controller_test.dart
cover activate/deactivate on iOS, no-op on non-iOS, and graceful
PlatformException handling.
Docs
- SRS SRS-110 expanded to cover the call-scoped lifecycle invariant.
- SysDes mobile-voice row updated to reflect the MethodChannel and
.ambient idle baseline.
- implementation-status-2026-05-28 voiceChat row flipped to done.
Verification
- flutter analyze: No issues found (2.8s)
- flutter test: 195 passed / 2 skipped / 0 failed
- cargo build -p chanora_audio --target aarch64-apple-ios: clean
- cargo build -p chanora_audio (macOS host): clean
Device QA matrix (Spotify-keeps-playing-while-idle, mix-during-call,
revert-on-call-end, media-services-reset-during-idle) remains pending
on physical hardware.
Inventory regenerated after refactor(audio): share AudioHandler between iOS and macOS, bump deps (6214139), which dropped windows-core 0.54.0 (and its windows-result 0.1.2 transitive dep). Apache-2.0 crate count drops from 333 to 327. No license-class change.
docs/architecture/sdd.md: three new rows in the Audio Detailed Design table. 'VoiceActivity gate (capture-side)' documents voice_activity::VoiceActivityStateMachine — the 10 ms-cadence gate for TransmitMode::VoiceActivity with open-after (40 ms) / hangover (500 ms) / min-tx (200 ms) / weak-hold (30-100 frames) timers, plus live configure() re-clamping behaviour. 'macOS render cadence (producer + ring)' documents the 20 ms tokio producer task + crossbeam ArrayQueue ring with 100 ms prebuffer, the design chosen to decouple ingress quantums (20 ms Opus frames) from egress quantums (whatever VPIO asks for). 'iOS render cadence (direct-fill)' documents the direct-fill callback path with preallocated 4096x2 f32 scratch buffer and try_lock semantics (not blocking lock).
Closes the VoiceActivity / macOS-producer-ring / iOS-direct-fill doc gaps flagged in the PR #27 'Deferred' list.
- docs/sysrs.md: raised macOS minimum runtime in SysRS-310 from 10.15 to 13.0 to match the actual floor in apps/chanora_flutter/macos/chanora_bridge.podspec (MACOSX_DEPLOYMENT_TARGET = 13.0) and macos_deployment_target.rb; added a change-log entry for the raise. SysRS-051 gained a note documenting the iOS/macOS audio-lifecycle asymmetry (iOS has full AVAudioSession lifecycle; macOS is limited to launch-time mic permission + VPIO engine restart on default-device change + VPIO startup readback in the current baseline).
- docs/architecture/sdd.md: added two rows to the Audio Detailed Design table. 'Render peak limiter' documents voice_render::limit_peak_inplace (single-pass, allocation-free, threshold 0.99, applied in both Apple render callbacks before i16 downmix). 'VPIO ducking config (macOS 14+)' documents the 8-byte AuVoiceIoOtherAudioDuckingConfiguration struct write to selector 2108 on the VoiceProcessingIO AudioUnit at startup, with the macOS 13 silent-fallback behaviour.
- docs/governance/product-decision-register.md: added DEC-033 recording the VPIO ducking configuration decision (advanced ducking off, level = Min, macOS 14+ only).
* fix(audio): eliminate Android output stutter via Oboe config + lock-free callback
Phase 1 — Oboe configuration:
- Change output stream from Usage::VoiceCommunication to Usage::Game with
ContentType::Sonification to avoid forcing the Legacy (OpenSL ES) data
path on most devices (Oboe issue #2075)
- Switch output format from i16 Mono to f32 Stereo, matching Qint's proven
configuration and eliminating per-callback downmix conversion
- Set buffer size to 2x burst after stream open, reducing default buffer
from 8-20x burst to 2x burst for lower latency
- Remove scratch Mutex<Vec<f32>>; callback writes directly to Oboe buffer
Phase 2 — Lock-free output callback:
- Add audio_event_queue.rs: lock-free SPSC bridge using crossbeam ArrayQueue
with separate packet (lossy) and control (reliable) channels
- OutputCallback now owns AudioHandler directly (no Arc<Mutex<>> on Android)
- Inbound forwarder pushes packets via AudioEventProducer (no mutex)
- set_client_volume pushes control commands via event queue on Android
- iOS/desktop Arc<Mutex<AudioHandler>> path unchanged
* fix(audio): address PR #20 review findings
- Store AudioEventConsumer directly in OutputCallback to eliminate
per-callback Arc clone on the real-time audio thread
- Add SAFETY comment for the unsafe from_raw_parts_mut transmute
- Bound set_client_volume spin-loop to 64 retries with warn log
- Remove redundant crossbeam-utils direct dependency
- Regenerate license inventory for new crossbeam deps (CI fix)
* fix(audio): use ASCII TODO punctuation
* chore: regenerate Cargo.lock after rebase
* fix(ui): add 1s cool-down to prevent double-tap channel join
voiceJoin returns instantly (fire-and-forget protocol), so the
pending-join guard clears before a second tap lands. The cool-down
prevents the rapid channel oscillation and ClientIsFlooding (524)
that results from double-tapping.
* fix(ui): handle ChannelAlreadyIn as success, ClientIsFlooding with backoff
- ChannelAlreadyIn (0x0302): treat as silent success, update UI state
- ClientIsFlooding (0x020c): show localized snackbar, extend cooldown 5s
- Add l10n strings for flooding error (en + zh)
* fix(proto): use Windows TS3 client version for broadest compatibility
Matches Qint's default (Windows_3_X_X__1). Avoids server-side
behavioral differences with TS5 version strings.
* fix(proto): patch tsproto-types to handle short P-256 coordinates
BigInt::to_bytes_be() strips leading zeros, causing WrongPublicKeyLength
when a server's ephemeral key coordinate starts with 0x00. Patch from
EdisonJwa/tsclientlib fix/p256-short-coordinate-pad branch left-pads
coordinates to the P-256 field size instead of rejecting them.
* refactor(core): stop watchdog from emitting SnapshotChanged
The watchdog now serves only as a liveness probe (miss counting for
reconnection). UI updates are handled entirely by the event-driven
delta path (ProtocolDelta → SessionEvent → BridgeEvent → Flutter).
Removes signature tracking and SnapshotChanged emission from the
supervisor loop. The initial snapshot is still fetched via the
Connected event handler in Flutter.
* refactor(ui): remove channel-join cooldown guard
With event-driven deltas the UI updates instantly on channel moves,
so the 1-second cooldown is no longer needed. Double-taps are handled
by the server (ChannelAlreadyIn → success) and the pending-channel-id
guard prevents overlapping requests.
Also removes the _lastJoinCompletedAt field entirely.
* fix(core): reattach event forwarders after reconnect
The reconnect path swapped in a new ProtocolClient but never took
chat_rx, activity_rx, or delta_rx from it. After the first reconnect,
the event-driven UI pipeline was dead.
Fix by extracting spawn_event_forwarders() helper called on both
initial connect and reconnect. Also replaces lossy try_recv+sleep
polling with proper recv().await for push-based delivery.
* feat(protocol): enrich delta schema with all snapshot-visible fields
ClientJoined now carries input_muted, output_muted, is_server_query,
talk_power, talk_power_granted. ChannelAdded/ChannelUpdated now carry
has_password and needed_talk_power. ClientUpdated also carries
is_server_query, talk_power, talk_power_granted.
This prevents local snapshot drift where fabricated defaults could
hide password requirements, talk-power restrictions, or client type.
* refactor: remove dead SnapshotChanged variant end-to-end
SnapshotChanged is no longer emitted since the watchdog was refactored
to liveness-only. Removes the variant from SessionEvent, BridgeEvent,
and the Flutter switch statement. FRB bindings regenerated.
* fix(ci): regenerate license inventory and fix iOS submodule fetch
- Regenerate docs/security/license-inventory.md to match current lockfile
- Remove submodules: true from checkout (causes hard fail on private submodule)
- Add explicit git submodule update --init --depth=1 with || true fallback
- Check silero-coreml/Package.swift instead of directory existence
Closes DEC-032. Restores the canonical Android ABI set
{arm64-v8a, armeabi-v7a, x86_64} per SDD-073 item 4 / SDD-118 item 3.
Root cause was the audiopus_sys + cmake-rs + NDK toolchain-file gap:
cargo-ndk 4.x sets ANDROID_ABI / ANDROID_PLATFORM as env vars per
invocation, but upstream cmake-rs 0.x does not forward them to the
child cmake invocation as -D variables, so armeabi-v7a and x86_64
configure steps fell through to the toolchain-file default and
failed to build.
Fix:
- Cargo.toml: add a workspace [patch.crates-io] stanza pinning the
cmake crate to fork pr2502/cmake-rs @ commit
bdad5edc569d82151922c5c6c4685b1563f12aa1 (branch android-build),
which carries cmake-rs PR #257
(https://github.com/rust-lang/cmake-rs/pull/257). The patch is a
9-line addition that forwards ANDROID_ABI and ANDROID_PLATFORM
from the env to the child cmake as -D variables.
- Cargo.lock: regenerated by 'cargo update -p cmake'; the lone
cmake entry now points at the fork rev.
- apps/chanora_flutter/android/app/build.gradle.kts: restore
abiFilters to {arm64-v8a, armeabi-v7a, x86_64}; remove the
TODO(x86_64/armv7 follow-up) comment.
- docs/governance/product-decision-register.md: mark DEC-032 as
Resolved (2026-05-18) with the resolution mechanism, update the
§3 / §7 rows, and append a 0.9.8.1 change-history entry.
Verification (host: Linux):
cargo update -p cmake -> pulled fork rev
cargo check --workspace --all-targets -> PASS
cargo test --workspace -> PASS (no regressions)
cargo ndk --platform 28 -t arm64-v8a build -p chanora_bridge -> PASS
cargo ndk --platform 28 -t armeabi-v7a build -p chanora_bridge -> PASS
cargo ndk --platform 28 -t x86_64 build -p chanora_bridge -> PASS
Upstream tracking: re-evaluate the [patch.crates-io] override once
cmake-rs PR #257 merges and a fresh cmake release lands on
crates.io; at that point switch to a plain dep bump and remove the
override.
Author SysRS-310 ratifying the macOS minimum runtime baseline at 10.15
(Catalina) at the SysRS layer, parallel to SysRS-286 (iOS 13.0) and
SysRS-288 (Android API 28).
Closes the Wave 1.5 traceability-audit deferred-but-optional follow-up.
ID allocation: SysRS-310 (not SysRS-290, which is already allocated to
MVP single-active-server-connection scope); monotonic numbering
preserved.
Cross-references SAD-087, SDD-119, SysRS-286, SysRS-288. Verification:
Review + Platform Test on a macOS 10.15 system. Raising the baseline
(e.g., to 11.0 / Big Sur) requires a DEC entry.
Trace: SysRS v0.9.11, SAD-087, SDD-119.
Resolve the macOS half of the SDD-119 item 3 single-source-of-truth follow-up. The chanora_bridge cdylib macOS deployment-target floor ('10.15') was previously hard-coded at 7 sites across Podfile and chanora_bridge.podspec; this commit collapses them to a single Ruby constant declaration in a new SoT file.
Selected Option B (Ruby constant) over Option A (.xcconfig — rejected because the podspec prepare_command runs before any xcconfig is applied) and Option C (versioned text file — rejected as overkill given both consumers are already Ruby). Realizes SAD-087(a)'s 'single macOS build-configuration location' mandate.
Out of scope: the pbxproj 'MACOSX_DEPLOYMENT_TARGET = 10.15' lines (565/666/717) belong to the PBXProject default config and are independently overridden to '11.0' at the Runner PBXNativeTarget level; they are the Runner app's floor, not the bridge cdylib's floor. The iOS half (IPHONEOS_DEPLOYMENT_TARGET=13.0) remains an open follow-up.
Files: new apps/chanora_flutter/macos/macos_deployment_target.rb (MACOS_BRIDGE_DEPLOYMENT_TARGET = '10.15'.freeze); Podfile + chanora_bridge.podspec require_relative the constant and consume it at 7 sites; docs/architecture/sdd.md SDD-119 item 3 rewritten + Notes bullet updated + v0.9.17 changelog entry.
SDD-120 amendment v0.9.15 → v0.9.16 reflecting commit 3a7750a discovery.
The two post-processor tools (emit_baseline, compare_baseline) were originally
specified in SDD-120 §2 as residing in crates/chanora_audio/benches/, with
[[bin]] declarations in Cargo.toml. The implementation discovered that Cargo's
dependency resolver only routes [dev-dependencies] to [[test]], [[bench]], and
[[example]] targets — NOT to [[bin]] targets under src/bin/ or to ad-hoc paths.
Routing serde_json (required by both binaries) as a regular [dependencies] entry
to support [[bin]] placement would force it into the production cdylib build,
contradicting SDD-120 §10 release-artifact isolation: "no production telemetry
export" and "no benchmark surface in shipping artifacts." Confirmed by the user's
question about whether benchmark crates ship in the release artifact (they must
not).
The correct Cargo-idiomatic placement is examples/. Cargo auto-discovers files
under examples/ as example targets; they receive [dev-dependencies] routing; and
they are excluded from cargo build --release and from flutter build --release
artifacts by Cargo design.
Amendment scope (SDD-120 only):
- §1 item 4: [[bin]] → [[example]]; cargo run --bin → cargo run --example.
- §2: added item 5 with the rationale clause + Cargo dependency resolver
explanation.
- §5 item 1: path benches/ → examples/; invocation flag --bin → --example.
- §5 item 4: dev-dependencies routing clause updated.
- §6 step 6/8: --bin → --example.
- §7 step 5: --bin → --example.
- §10 item 1: release-artifact-isolation bullet extended to cite the
[dev-dependencies] Cargo design mechanism that enforces it.
- §11 verification matrix unchanged.
- "Allocated to" line + "Software units" list updated to reference examples/
paths.
No semantic change to SDD-120: same harness, same metrics, same workflows, same
out-of-scope deferrals. The implementation at 3a7750a already lives at the
corrected paths; this amendment brings the SDD text into agreement with the
code.
New controlled document docs/verification/ipad-p0-acceptance.md
extends iOS P0 coverage to iPad. The build artefact is identical
to iPhone — `TARGETED_DEVICE_FAMILY = "1,2"` in
ios/Runner.xcodeproj/project.pbxproj is the Universal family, so
the same Runner.app installs on iPad with the same personal-team
provisioning profile.
15-row checklist mirrors ios-p0-acceptance.md TC-1..TC-12 and adds
three iPad-specific rows:
TC-13 wide-mode landscape layout — iPad in landscape is well
above the 840 dp LayoutBuilder breakpoint shipped in
apps/chanora_flutter/lib/main.dart, so the connected view
splits into a 320 dp left column (banner + Voice Bar) plus
an expanding channel tree. Portrait rotation collapses
back to the stacked iPhone layout. Long channel-name pills
still ellipsize per the earlier voice_bar.dart fix.
TC-14 Split View / Slide Over no-crash — Apple iPad multitasking
is intentionally unsupported in P0. UIApplicationSupports\
MultipleScenes stays false. This row asserts the app does
not crash when iPadOS tries to host it in Split View; the
actual multi-scene wiring is P1.
TC-15 AirPlay 2 audio route — verifies AVAudioSession routing
honours an AirPlay 2 destination picked via Control
Center, and routes back cleanly when iPad is reselected.
docs/governance/document-index.md
Bumped to 0.9.9 with the change-history row noting the iPad
acceptance doc. No spec items added; DEC-025 was originally
iPhone-only for the mobile target and this row formally
extends P0 coverage to iPad within the same iOS toolchain.
python3 tools/validate_docs.py: clean (pre-existing 35-filename
[FAIL] retained, unchanged).
New controlled documents:
* docs/verification/macos-p0-acceptance.md
15-row human-must checklist for Apple Silicon macOS P0 sign-off.
Targets the SDD-085 CGEventTap backend (now fully live) +
SDD-094..097 audio lifecycle. Auto-test rows pre-filled from the
M1 Mac verification pass: chanora_audio 34 / 0 / 0 (Linux: 32;
macOS delta is the keymap + Send-bound + descriptor-builder
tests). Pre-flight covers the manual framework-wrap +
install_name_tool + ad-hoc codesign step via the new
tools/macos-postbuild.sh. TC-3 walks the Input Monitoring grant
+ descriptor watch transition timing (~1.5 s).
* docs/verification/ios-p0-acceptance.md
12-row human-must checklist for iOS P0 sign-off on a physical
iPhone via the developer's free Apple Personal Team. TC-3
documents the Focused-only PTT capability iOS gives us (no
global event tap analogue exists). TC-8 covers UIBackgroundModes
= audio. TC-9 covers AVAudioSession routing — phone-call
interruption, AirPods route, etc.
docs/governance/document-index.md
Bumped to 0.9.8 with a change-history row covering both new
acceptance documents. No spec items added; the existing SDD-085
(macOS) and SDD-094..097 (audio lifecycle) are what these
documents sign off.
python3 tools/validate_docs.py: clean (pre-existing 35-filename
[FAIL] retained, unchanged).
New controlled document docs/verification/linux-p0-acceptance.md mirrors
docs/verification/windows-p0-acceptance.md with 15 TC rows tuned for the
GNOME-on-Wayland target environment (DEC-025). Pre-flight calibrated to
the Arch verification host (100.74.219.114): pacman queries, path
prefixes, xdg-desktop-portal-gnome version notes. Auto-test sign-off
filled with the headless verification pass run over SSH:
cargo check --workspace --release clean (30.99 s)
cargo test --workspace --lib 78 / 0 / 1
cargo test ... linux_portal_smoke -- --ignored
-> 1 / 0 (GlobalShortcuts portal reachable, version = 1)
cargo test ... ptt_privacy 1 / 0 (DEC-027 holds)
The 15 GUI rows are marked pending physical-console pass. SDD-086 portal
flow is what this doc signs off; SDD-081/094..097 are referenced.
Document index bumped to 0.9.7 with the change-history row covering the
new acceptance doc. No spec items added; pre-existing 35-filename FAIL
in validate_docs.py retained.
- windows-p0-acceptance.md: bump auto-test row to reflect current
Windows test count (126/0/1 — was 124/0/2 before the two
new start_flips_armed_to_l2 tests landed).
- document-index.md: new 0.9.6 row marking DEC-031 (missed-key-up
watchdog disabled on P0) and the controlled status of
docs/verification/windows-p0-acceptance.md.
Captures the 15 TC rows the lead walks through on the Korean
Win 11 host before tagging v1.0.0-rc.8, plus the auto-test
sign-off matrix (Linux + Windows cargo + flutter analyze +
validator + windows-smoke).
Each TC row maps to a spec requirement (DEC / SRS / SDD) or to a
regression the rc.7 review found. Failures block the tag.
Known gaps recorded at the bottom: macOS / iOS / Android P0 are
separate documents; VAD (DEC-030) and missed-key-up watchdog
redesign (DEC-031) are P1; real RMS level meter is P1.
The watchdog spawned by ChanoraSession::start_audio cleared
ptt_held after 30 s of continuous PTT key-down. That was correct
for the 'OS lost the key-up event' failure mode the original
SAD-079 / DEC-028 was designed to catch, but it was the wrong
shape for real human speech: anyone holding the bound key for a
long answer got cut off mid-sentence.
For P0:
- Comment out the spawn site in ChanoraSession::start_audio with
the rationale + the P1 redesign options under consideration
(raised ceiling / OS key-state polling / RMS-silence fallback).
- Leave the MissedKeyUpWatchdog Rust type, its spawn / spawn_on_signal
entry points, and all unit tests in chanora_audio::ptt unchanged
so P1 can re-enable with the chosen detection strategy without
re-implementing anything.
Spec: new DEC-031 in product-decision-register.md supersedes
DEC-028 for the v1 ship. DEC-028 stays in the register as
historical context. The §7 open-decisions log + §8 change history
get matching 0.9.12 rows.
Note: Mumble and TeamSpeak ship without a comparable watchdog —
the 30 s ceiling was stricter than industry baseline. The
underlying protection (OS-level key-up loss) is still worth
solving, just not with a fixed timeout.
cargo test --workspace --lib: 80 passed / 0 failed / 1 ignored
(unchanged; the watchdog unit tests still run because the type
itself is unchanged).
docs validator: clean (pre-existing 35-filename warning only).
Add SysRS-303/304, SysDes-149/150/151, SRS-204/205/206/207,
SAD-081/082/083, SDD-094/095/096/097, DEC-029/030.
Captures the v1 lifecycle redesign:
- Drop manual Start-audio button; audio engine is bound to voice-channel
join/leave (ensure_running on first join, shutdown_if_idle on last
leave). Output stream opens regardless of mic-permission state so
listen-only is a first-class flow.
- TransmitMode enum (Ptt / Continuous / VoiceActivity-reserved).
Default Ptt on fresh install. Persisted per identity.
- PTT release tail: 200 ms default (0-500 ms configurable) before
transmit gate closes, avoiding clipped trailing syllables.
- Hard-mute toggle overrides transmit gate regardless of mode/PTT.
- Bridge surface: drop start_audio/stop_audio; add
voice_join(channel_id) / voice_leave() and BridgeEvent::VoiceState.
DEC-029 rejects Flutter global-hotkey packages (hotkey_manager,
super_hot_key) for PTT: they wrap RegisterHotKey/RegisterEventHotKey
which consume the key and don't fire key-up, wrong primitive for PTT.
Native Rust DesktopPttBackend (SDD-083/084/085) stays authoritative.
DEC-030 defers Voice Activity Detection to P1. RMS / WebRTC VAD /
Silero VAD trade-off review (binary-size, dependency-surface, CPU
profile) postponed; TransmitMode::VoiceActivity reserved on the enum
surface so a P1 increment is non-breaking.
Validator clean: 304/151/207/83/97 IDs, strict layered sourcing
preserved, no new warnings beyond the pre-existing 35 old-package-name
filenames.
A P0 traceability audit per the project's compliance workflow
found exactly one gap across the 162 P0 SRS items:
* SRS-200 (mouse side buttons as bindable inputs for desktop
Global PTT) had no dedicated SAD item. The earlier baseline
matrix folded SRS-200 under the narrative
`SRS-195..203 -> SAD-071..079` umbrella, which technically
covered the requirement at the table level but did not give
SRS-200 a one-to-one SAD source the validator's strict
discipline expects.
Per the project's gate-check workflow (Case C —
BLOCKED_MISSING_SAD) this commit closes the documentation chain
*before* claiming compliance for the already-merged mouse-side-
button code path:
* `docs/architecture/sad.md` v0.9.4 — new `SAD-080` sources
SRS-200, allocates `Audio (Windows / macOS / Linux), Bridge,
Flutter UI`, and records the cross-platform mouse-side-button
surface as a software-architecture item. The §27 coverage
matrix gains a dedicated SRS-200 -> SAD-080 row.
* `docs/architecture/sdd.md` v0.9.4 — new `SDD-093` sources
SAD-080. Specifies the `PttInputClass` enum surface
(`None` / `Keyboard` / `MouseSideButton`), the rebind
contract on the Windows + macOS backends, the Linux portal's
pass-through behaviour, and the Flutter
`PointerEvent.buttons` bitmask capture (back = `0x08`,
forward = `0x10`). The §11 coverage matrix gains the
SAD-080 -> SDD-093 row.
* `docs/governance/traceability-matrix.md` v0.9.4 — the
`(DEC-026 mouse buttons)` row moves from
`SAD-072..074 / SDD-082..086` to the dedicated
`SAD-080 / SDD-093`.
* `docs/governance/baseline-candidate-validation-report.md`
v0.9.4 — ID totals advance to 302 / 148 / 203 / 80 / 93;
direct-layer-rule and undefined-reference counts remain
zero.
* `docs/governance/repo-format-validation-report.md` v0.9.4 —
same ID totals update.
Audit summary
-------------
* 162 P0 SRS items audited.
* 1 SAD-coverage gap (SRS-200) — closed by this commit.
* 0 SDD-coverage gaps (every PTT SAD has explicit SDD coverage;
the inherited baseline `SAD-032/033` are covered through the
documented range row, not individually).
* All Priority: P0 software requirements now have a strict
SRS -> SAD -> SDD chain on file.
Validator output:
```
[OK] SysRS: 302 defined, 0 undefined references
[OK] SysDes: 148 defined, 0 undefined references
[OK] SRS: 203 defined, 0 undefined references
[OK] SAD: 80 defined, 0 undefined references
[OK] SDD: 93 defined, 0 undefined references
[OK] SRS direct SysRS references: 0
[OK] SAD direct SysRS references: 0
[OK] SAD direct SysDes references: 0
[OK] SDD direct SysRS references: 0
[OK] SDD direct SysDes references: 0
[OK] SDD direct SRS references: 0
```
Implementation status
---------------------
The mouse-side-button code was implemented in v1.0.0-rc.4 and
v1.0.0-rc.5 under the (then-implicit) PTT umbrella; the code
already matches the new SDD-093's contract verbatim. This
commit ships **documentation only** — it adds the SAD/SDD/matrix
rows that retroactively justify the existing implementation
under the strict traceability discipline. No code edits, no test
edits.
* `cargo test --workspace`: 67/67 green (unchanged).
* `cargo deny check`: advisories ok, bans ok, licenses ok,
sources ok.
* `cargo about generate`: zero new warnings (no Cargo.lock
delta).
* `flutter analyze`: clean.
* `tools/validate_docs.py`: all SRS/SAD/SDD coverage and
direct-layer-rule checks pass.
Outstanding open items remain live verification per
`docs/release/release-readiness-go-nogo-record.md`
(RR-PTT-001..006/008) and the DEC-012 legal review; both are
non-engineering work.
Audited every `Priority: P0` row in `docs/requirements/{sysrs,srs}.md`
against the live code. Three items needed work; this commit closes
all three.
Gap A — SysRS-262 + SysRS-282 (screen-reader semantics + accessible
labels for the PTT control)
-------------------------------------------------------------------
The Flutter PTT control is a custom `Listener` over a `Container`
— not a built-in `Button`, so the platform accessibility tree had
no idea it was an interactive control. Screen readers
(VoiceOver, TalkBack, NVDA, Orca) would have read the visible text
without announcing the control role or its toggled state.
Wrap the Listener in a `Semantics(button: true, toggled: _pressed,
label: …, hint: …, excludeSemantics: true)` so the platform
accessibility tree carries the right role, the current state
("Hold to talk" / "Transmitting"), and a usage hint. The
`excludeSemantics: true` argument suppresses the duplicate child
nodes the Container + Row + Icon + Text would otherwise generate
on top of our explicit label.
SysRS-263 (no colour-only state) is preserved: the visible label
and the mic icon already differentiate the two states without
relying on the colour transition.
New ARB key `pttHoldToTalkSemanticsHint` in `app_en.arb` and
`app_zh.arb`.
Gap B — SRS-198 (macOS async permission re-check)
-------------------------------------------------
The macOS backend queried `query_permission()` once at
construction and never re-checked. That violates SRS-198's
"upgrade to the appropriate Global level only after the user
grants the required permission" — once Chanora is running, a
runtime grant must lift the descriptor from `L0Focused` to a
Global level without an app restart.
Substantive rewrite of `crates/chanora_audio/src/ptt_backends/macos.rs`:
* `permission: PermissionState` becomes `permission: Arc<AtomicU8>`,
enabling cross-thread updates without a Mutex.
`PermissionState::{to_u8, from_u8}` carry the encoding.
* The backend owns a `tokio::sync::watch::Sender<PttBackendDescriptor>`
and overrides `DesktopPttBackend::descriptor_watch()` to hand
out subscribers; `chanora_core::ChanoraSession::start_audio`
already forwards transitions to `SessionEvent::PttCapability`.
* `start()` spawns a `chanora-perm-watch` OS thread that polls
`query_permission()` every 1.5 s and republishes the
descriptor on every transition. Polling rather than KVO /
notifications because Input-Monitoring has no public
change-notification API on macOS; 1.5 s is sufficient for a
user grant + return-to-Chanora cycle.
* `rebind()` also republishes the descriptor so a
`keyboard → mouse-side-button` change updates the badge.
* Six new unit tests on the platform-independent
`build_descriptor` and the atomic encoding contract. They
only compile under `target_os = "macos"` (consistent with
the rest of the module), so the Linux dev-host workspace
test count is unchanged.
`query_permission()` itself still returns `Undetermined` until
the IOKit live link lands in the macOS platform-verification
commit; the re-query loop will engage the upgrade path
automatically the moment that function returns real values.
Gap C — SRS-200 (Linux mouse-side-button portal-dependence)
-----------------------------------------------------------
`desktop-ptt-architecture.md` §5.3 already described the
heuristic classifier. Added one explicit sentence stating that
Linux mouse-side-button support is *portal-dependent*: Chanora
never claims a fixed Mouse4/Mouse5 binding on Linux; the portal
decides what inputs it accepts in the current session, and the
classifier degrades to `keyboard` whenever the portal's
description does not contain "mouse". This matches the SRS-200
text verbatim and removes the ambiguity over what "Linux
support follows the portal" means in practice.
Verification
------------
* `cargo test --workspace` (with `CHANORA_DISABLE_KEYRING=1`):
all 67 Linux-side tests green (unchanged). The new macOS
unit tests count under `target_os = "macos"` only — they
will report once the macOS reference host runs `cargo test`.
* `cargo deny check`: advisories ok, bans ok, licenses ok,
sources ok.
* `flutter analyze`: clean (no new accessibility warnings).
* Linux release bundle builds clean.
P0 audit summary
----------------
After this commit every Priority: P0 row in `sysrs.md` and
`srs.md` has a concrete implementation. The remaining open items
are all live verification, not code:
* Per-platform live PTT traces on Windows / macOS reference
hosts (RR-PTT-001..003, RR-PTT-008) — hosts unavailable
locally; queued for platform owners.
* Linux GNOME-Wayland live trace (RR-PTT-004) — implemented
in rc.5; awaiting live host trace.
* Linux non-tested compositor fallback trace (RR-PTT-005) —
Open.
* Diagnostic-export key-leak inspection (RR-PTT-006) — Open
but trivially testable on any host with PTT bound.
* DEC-012 legal review — engineering hand-off complete since
rc.2.
Promotes the Linux backend from probe-only to a live
`org.freedesktop.portal.GlobalShortcuts` session, closing the
gen2 v0.9.3 baseline's last Linux-side code item. Both gaps I
flagged on the review pass are addressed:
* Stop now closes the portal session through the dedicated
`org.freedesktop.portal.Session` interface (not the
request-cancel `Request` interface — that would only abort a
pending Request, not release the bound shortcuts).
* Ten new unit tests cover `classify_shortcuts_value`,
`publish_bound`, `publish_l0`, and the `SHORTCUT_ID` stability
contract using synthesised `OwnedValue` payloads. Live D-Bus
coverage stays in the `linux_portal_smoke` ignored
integration test (RR-PTT-004).
Live session lifecycle (gen2 Q5b — lazy, single backend instance):
1. `start(gate, binding)` spawns one `tokio::spawn` worker that
owns an async `zbus::Connection` (sharing the bridge's
tokio runtime per Q4a).
2. `CreateSession` with fresh random `handle_token` /
`session_handle_token` tokens. The worker awaits the portal
`Response` signal via a `RequestProxy` subscription and
extracts `session_handle` from the results dict.
3. `BindShortcuts(session_handle, [("chanora-ptt", { description
= "Chanora push-to-talk" })], "", {})`. The portal opens its
own system-managed dialog asking the user to choose a key
— Chanora itself never reads raw key events. The audio
engine continues at `L0Focused` while the dialog is open;
the descriptor watch publishes the transition once the
portal returns.
4. On `response_code == 0`: classify the `trigger_description`
substring (heuristic: contains "mouse" -> MouseSideButton,
else Keyboard), publish `L2GlobalHoldToTalk` (or `L3` for
mouse) through the watch sender. The raw trigger_description
string is never logged (DEC-027 / SRS-202).
5. On `response_code == 1` (cancelled) or `>= 2` (failure):
publish `L0Focused` through the watch sender. The user can
retry via the UI "Configure" button (gen2 Q6a).
6. The worker enters a `tokio::select!` loop multiplexing the
`cmd_rx` channel (Rebind / Stop) and the `Activated` /
`Deactivated` signals. Matching signals scoped to this
session handle and `chanora-ptt` shortcut id drive
`gate.set(true/false)`.
7. `Rebind` re-runs `BindShortcuts` on the same session.
8. `Stop` calls `org.freedesktop.portal.Session.Close()` on
the session-handle object path, clears the gate, exits.
UX (gen2 Q3a): when `_pttBackendId == 'gnome-wayland-portal'`,
the Flutter "Configure" button skips the in-app
`_PttBindingCaptureDialog` and shows a SnackBar telling the user
their desktop environment will open its own shortcut dialog.
The button delegates to `setPttBinding(keyboard, "portal")`
which nudges the backend; the portal handles the rest. New ARB
key `pttConfigurePortalRedirect` in en + zh-Hans.
Trait surface (cross-cutting):
* `DesktopPttBackend::descriptor_watch()` is a new trait method
with a default impl returning a never-firing receiver.
Backends with async capability transitions (only the Linux
portal backend today) override it to return the live watch
sender's receiver.
* `chanora_core::ChanoraSession::start_audio` subscribes to the
active backend's `descriptor_watch()` and spawns a forwarder
task that re-emits `SessionEvent::PttCapability` on every
transition. The initial value is emitted synchronously.
`Cargo.toml` (Linux-only):
* `futures-util` (std features, no executor) for stream
consumption on the portal signal subscriptions.
* `rand 0.8` for fresh per-process portal tokens.
* `zbus` continues at v5 with the `tokio` + `blocking-api`
features.
Tests
-----
* `chanora_audio` rises from 8 to 18 unit tests. New
coverage on the Linux module:
- `classify_returns_none_when_shortcut_id_missing`
- `classify_returns_keyboard_for_typical_trigger_description`
- `classify_returns_keyboard_when_trigger_description_missing`
- `classify_detects_mouse_substring`
- `classify_is_case_insensitive_on_mouse_substring`
- `publish_bound_keyboard_publishes_L2_with_keyboard_class`
- `publish_bound_mouse_publishes_L3`
- `publish_bound_none_publishes_L2_keyboard_default`
- `publish_l0_clears_descriptor`
- `shortcut_id_is_stable`
* Workspace total: 67 unit + integration tests, all green with
`CHANORA_DISABLE_KEYRING=1` (was 57 at v1.0.0-rc.4).
* New `crates/chanora_audio/tests/linux_portal_smoke.rs`
ignored integration test (RR-PTT-004 evidence path). Run on
a GNOME-on-Wayland host with
`cargo test -p chanora_audio --test linux_portal_smoke -- --ignored --nocapture`.
Documentation
-------------
* `docs/architecture/desktop-ptt-architecture.md` §5.3 rewritten
to describe the realised lifecycle; v0.9.4 change-history
entry added.
* `docs/governance/product-decision-register.md` v0.9.10
change-history entry recording the code-side promotion. No
decision rows mutate.
* `docs/release/release-readiness-go-nogo-record.md` RR-PTT-004
flipped from `Open` to `Implemented (live trace pending)`;
v0.9.5 change-history entry.
Verification
------------
* `cargo test --workspace`: 67/67 green.
* `cargo deny check`: advisories ok, bans ok, licenses ok,
sources ok.
* `cargo about generate --offline`: zero new warnings.
* `tools/dump_flutter_licenses.sh`: 94 packages, 0 without
LICENSE.
* `flutter analyze`: clean.
* `cargo build -p chanora_bridge --release` +
`flutter build linux --release`: clean Linux x86_64 bundle.
* Live portal trace (RR-PTT-004) — **not run**. The dev shell
is a TTY without a Wayland session. The user will run the
ignored smoke test from inside a GNOME-on-Wayland session
when available.
No Windows / macOS / iOS live verification in this commit (hosts
unavailable). The Windows + macOS backend scaffolds remain in
place reporting their target capability honestly; live OS-call
wiring is queued for their respective platform owners'
reference hosts per `docs/governance/staged-release-plan.md`.
Lands SDD-081..088 + SDD-092 implementations on top of v1.0.0-rc.3.
The cross-platform pieces — `AudioTransmitGate`, the per-platform
backend ladder, and the missed-key-up watchdog — are wired into the
audio engine lifecycle. Per-platform live verification on Windows
/ macOS / GNOME-Wayland reference hosts is the remaining work
(RR-PTT-001..006/008 in `release-readiness-go-nogo-record.md`).
`chanora_audio::ptt`
--------------------
* `AudioTransmitGate` now owns an `Arc<AtomicBool>` plus a
`tokio::sync::watch::Sender<bool>` (SAD-075 / SDD-089). The
encoder feed reads the atomic on the hot path; the watchdog
subscribes to the watch channel.
* `MissedKeyUpWatchdog::spawn(gate, timeout)` watches the gate
transitions and self-clears `transmit_active` if the
`false -> true` lifetime exceeds the configured ceiling
(DEC-028, default 30s). Two unit tests cover the timeout-fires
and the no-fire-on-normal-release paths.
`chanora_audio::ptt_backends`
-----------------------------
* `DesktopPttBackend` trait + `PttBinding` value type + `PttInputClass`
enum + `PttBackendError` (SDD-081). `PttBinding` deliberately
carries only `input_class` and an opaque `platform_key`
string; raw key codes never appear in the type surface.
* `select()` factory (SAD-071): runtime ladder evaluation per
OS. Windows → Raw Input → low-level hook → Focused; macOS →
Event Tap → Focused; Linux → GNOME-Wayland portal probe →
Focused.
* `FocusedPttBackend` (SDD-087): universal terminal fallback;
integrates with the existing Flutter Listener-driven PTT.
* `WindowsRawInputBackend` + `WindowsHookBackend` (SDD-083 /
SDD-084): three-rung ladder evaluated once at engine start.
Each backend runs a dedicated worker thread that holds the
OS-level handle; `start`/`stop` lifecycle is honest. Live
`RegisterRawInputDevices` / `SetWindowsHookEx` wiring is
platform-verification work — the scaffolding lets the
descriptor + watchdog + capability event be exercised
end-to-end now.
* `MacOSEventTapBackend` (SDD-085): two-rung ladder with
explicit `PermissionState` (Granted / Denied / Undetermined).
`Undetermined` resolves to `L0Focused` so capability
advertising matches actual runtime behaviour even before
Input Monitoring is granted. Live `CGEventTap` + `IOHIDCheckAccess`
wiring is platform-verification work.
* `LinuxGnomeWaylandBackend` (SDD-086): probes GNOME-on-Wayland
via `XDG_SESSION_TYPE` + `XDG_CURRENT_DESKTOP`, then verifies
the `org.freedesktop.portal.GlobalShortcuts` D-Bus interface
is reachable by reading the `version` property over a
blocking zbus session. Reports `gnome-wayland-portal` /
`L2GlobalHoldToTalk`. Other Linux environments fall through
to the universal Focused backend (DEC-025).
`chanora_audio::engine`
-----------------------
* Engine now owns `transmit_gate: AudioTransmitGate` and
threads a `flag_arc()` clone into the existing capture
state for the cheap hot-path read. `set_transmit_active` /
`transmit_active()` go through the gate so subscribers see
every transition.
* `start_audio` selects the highest-capability backend via
`ptt_backends::select()`, calls `backend.start(gate, none())`,
and spawns the watchdog. Both are released in `stop()` and
on Drop.
* New `engine.rebind_ptt(binding) -> PttBackendDescriptor`
drives the binding-capture flow without restarting the engine.
* New `engine.ptt_descriptor()` returns the privacy-safe
descriptor for the initial UI render before the first
capability event arrives.
`chanora_core`
--------------
* Re-exports `PttBinding` + `PttInputClass`.
* New `ChanoraSession::set_ptt_binding(binding)` — calls
`audio.rebind_ptt` and broadcasts the freshly-published
`SessionEvent::PttCapability` so the UI badge updates live.
* New `ChanoraSession::ptt_descriptor()` for the initial render.
`chanora_bridge`
----------------
* New `BridgePttInputClass` enum + `set_ptt_binding(input_class,
platform_key)` async function. The `platform_key` string is
opaque to the bridge and never logged.
* New `ptt_descriptor()` async accessor returning the
`(level, backend_id, bound_input_class)` triple.
Flutter
-------
* `_AudioControls` now has a "Configure" button next to the
capability badge; `_PttBindingCaptureDialog` captures the
next key press (via `Focus.onKeyEvent`) or mouse side button
(via `Listener.onPointerDown` filtered to button bitmasks
`0x08` / `0x10`). The captured value is the platform-neutral
`LogicalKeyboardKey.keyLabel` or `mouse-side-button:{button}`.
* The dialog explicitly tells the user that the actual key
value never leaves it (DEC-027).
* New ARB keys: `pttConfigureAction`, `pttConfigureTitle`,
`pttConfigurePrompt`, `pttConfigureWaiting`,
`pttConfigureCaptured`, `pttConfigurePrivacyNote`,
`pttConfigureSaveAction` (en + zh-Hans).
Dependencies
------------
* `chanora_audio` adds (Linux only) `zbus = "5"` with the
`tokio` runtime selector + `blocking-api` feature for the
GlobalShortcuts portal probe.
* `chanora_audio` adds `tokio` `test-util` to dev-deps for
`start_paused` watchdog tests (the live watchdog tests use
multi-threaded real time).
Verification
------------
* `cargo test --workspace` with `CHANORA_DISABLE_KEYRING=1`:
57 tests green (was 53). chanora_audio rises from 4 to 8.
* `cargo deny check`: advisories ok, bans ok, licenses ok,
sources ok.
* `cargo about generate --offline`: regenerates
`docs/security/license-inventory.{md,html}`. The crate count
rises from 364 to 383 with the addition of the zbus tree.
* `tools/dump_flutter_licenses.sh`: 94 packages, zero without
LICENSE (unchanged).
* `flutter analyze`: clean.
* `cargo build -p chanora_bridge --release` + `flutter build
linux --release`: clean Linux x86_64 bundle.
Documentation
-------------
* `docs/release/release-readiness-go-nogo-record.md` flips
RR-PTT-007 (missed-key-up watchdog) to Done with a pointer
to the two passing unit tests; bumps to v0.9.4. Live
per-platform traces (RR-PTT-001..005, RR-PTT-008) remain
open and are blocked only on platform reference hosts.
Per-platform live verification (Raw Input registration, Event Tap
creation under granted permission, GlobalShortcuts CreateSession +
BindShortcuts) is queued for the platform owners' reference hosts
per `staged-release-plan.md`.
Applies the gen2 desktop-PTT review summary
(`gen2/chanora-desktop-ptt-review-summary-v0.9.2.md`) to our doc set
with the owner rulings PTT-OPEN-001 through PTT-OPEN-006 resolved as
accepted decisions DEC-023 through DEC-028:
* DEC-023 Windows Global PTT P0 / MVP
* DEC-024 macOS Global PTT P0 / MVP with permission UX
* DEC-025 Linux officially-tested env: GNOME on Wayland only
* DEC-026 Mouse side buttons supported (Win + macOS; Linux portal)
* DEC-027 PTT diagnostics: capability + availability only, no
raw key codes ever
* DEC-028 Missed-key-up watchdog: P0
Requirements (SysRS / SRS) and architecture (SysDes / SAD / SDD)
gain the desktop-PTT ID set the gen2 summary describes:
SysRS-296..302 -> SysDes-142..148
-> SRS-195..203
-> SAD-071..079
-> SDD-081..092
ID totals advance from 295 / 141 / 194 / 70 / 80 to 302 / 148 / 203
/ 79 / 92. The strict layered sourcing rule (`SRS -> SysDes` only,
`SAD -> SRS` only, `SDD -> SAD` only) is preserved; the
`tools/validate_docs.py` validator reports zero undefined refs and
zero direct-layer-rule violations.
New document:
* `docs/architecture/desktop-ptt-architecture.md` — capability
ladder (L0Focused, L1GlobalShortcut, L2GlobalHoldToTalk,
L3GlobalWithMouseButtons, L4DeviceAware reserved), Windows /
macOS / Linux strategies, privacy rule, audio-gate rule,
missed-key-up watchdog, release-readiness evidence requirement,
traceability summary.
Doc addenda (Baseline Candidate 0.9.3):
* `privacy/privacy-policy.md` — no raw key history, capability-
dependent Global PTT, UI reflects actual runtime capability
* `security/threat-model.md` — THREAT-PTT-001..006
* `security/diagnostic-redaction-audit-report.md` —
REDACT-PTT-001..006 banned field list enforced by `PttSanitizer`
* `release/platform-release-policy.md` — per-platform evidence
fields, no over-claim on untested Linux compositors
* `release/release-readiness-go-nogo-record.md` — RR-PTT-001..008
release-readiness items
* `verification/swe4-unit-verification-plan.md` —
SWE4-UV-035..039
* `verification/swe5-software-integration-verification-plan.md` —
SWE5-IV-015
* `verification/swe6-software-verification-plan.md` — SWE6-SV-017
* `verification/sys4-system-integration-verification-plan.md` —
SYS4-SIV-016
* `governance/traceability-matrix.md` — full PTT trace rows +
verification map
* `governance/decision-impact-assessment.md` — DEC-023..028
impact matrix
* `governance/product-decision-register.md` v0.9.9 entry
recording DEC-023..028 in the decision table and the status
table at §7
* `governance/document-index.md` — adds
`desktop-ptt-architecture.md` to the controlled set
* `architecture/proof-of-concept-plan.md` —
PoC-PTT-001..005 platform items
* `references/external-references.md` — Windows Raw Input,
macOS event-tap, Linux GlobalShortcuts portal references
* Both validation reports
(`baseline-candidate-validation-report.md`,
`repo-format-validation-report.md`) bumped to v0.9.3 with the
new ID totals (302 / 148 / 203 / 79 / 92).
README §"Desktop Push-to-Talk" added between Architecture Overview
and Repository Layout: capability levels, per-platform strategy,
privacy posture, missed-key-up watchdog.
Tooling:
* `tools/validate_docs.py` copied from the gen2 zip into the
repo tree (was previously available only inside the zip).
Reports zero undefined refs, zero direct-layer-rule violations,
English-only CJK check passes. The 35 "old package-style
filename" hits are pre-existing and identical to the gen2
baseline (they live in `path-migration-map.md` and config-ID
headers of governance docs and are intentional per the path
migration policy).
* `.gitignore` adds `/gen2/` so the externally-provided review
package does not enter the repo.
No code changes in this commit; B (the implementation split into
`transmit_active` / `capture_active`, `PttCapabilityLevel`
reporting, `PttSanitizer` diagnostics rule, and the UI capability
badge) follows in a separate commit.
Closes engineering deliverables 1–3 from the open-work table in
`docs/governance/legal-review-readiness.md` so the DEC-012 legal
review can actually run. With this commit, the only remaining
engineering item blocking sign-off is signed Windows / macOS / iOS
build artefacts, deferrable per the DEC-002 staged release plan.
Tooling
-------
* `about.toml` + `about.hbs` + `about-md.hbs` configure cargo-about
with the DEC-020 license posture and the five-target matrix
(Linux, Android, Windows, macOS, iOS). One per-crate clarification
for `allo-isolate` (`flutter_rust_bridge` transitive that ships
Apache-2.0 via `license-file` rather than an SPDX `license`
field). `cargo about generate` runs with zero warnings.
* `deny.toml` mirrors the cargo-about allow-list and adds minimal
bans / sources / advisories config. `cargo deny check` reports
`advisories ok, bans ok, licenses ok, sources ok` for the
workspace; multiple-versions of `windows_x86_64_msvc` produce
advisory `warn` (no fail) because three windows-targets versions
reach the graph via `jni`, `cpal`, and `keyring` respectively.
* `tools/dump_flutter_licenses.sh` + `tools/dump_flutter_licenses.dart`
walk `apps/chanora_flutter/pubspec.lock`, resolve each dependency
to its local pub-cache directory, read the LICENSE file, and emit
`docs/security/flutter-license-inventory.md`. SDK-sourced
packages (`flutter`, `flutter_localizations`, `flutter_test`,
`flutter_web_plugins`, `sky_engine`) resolve to the Flutter
framework BSD-3-Clause LICENSE under `$FLUTTER_ROOT` (or
`$HOME/sdks/flutter`).
Artefacts
---------
* `docs/security/license-inventory.md` — 364 transitive Rust
crates with full license texts. Apache-2.0 (276), MIT (55),
Unicode-3.0 (19), BSD-3-Clause (7), ISC (7). Zero copyleft.
* `docs/security/license-inventory.html` — same data rendered as
styled HTML for reviewer convenience.
* `docs/security/flutter-license-inventory.md` — 94 Dart / Flutter
packages with their LICENSE texts. Zero packages without a
resolvable LICENSE in this RC.
CI
--
* New `supply-chain` job runs `cargo deny check --workspace
--all-features` via `EmbarkStudios/cargo-deny-action@v2`. Fails
the build on any GPL / LGPL / AGPL / commercial-source license
surfacing transitively.
* New `license-inventory` job installs `cargo-about --features cli`
and regenerates `docs/security/license-inventory.md`; diffs
against the committed copy and fails on drift. Forces
contributors who touch the Cargo.lock to refresh the inventory.
* New `flutter-license-inventory` job runs
`tools/dump_flutter_licenses.sh` against the just-resolved pub
cache; same diff-on-drift semantics.
Governance
----------
* `docs/governance/legal-review-readiness.md` §5 cross-links the
three new artefacts in a "Reviewer artefacts" subsection.
* The open-work table at the bottom of the doc is rewritten as a
status grid: items 1–3 now read **Done**; item 4 (signed iOS /
macOS builds) remains the only open engineering blocker, with a
pointer back to `staged-release-plan.md`.
Verification
------------
* `CHANORA_DISABLE_KEYRING=1 cargo test --workspace`: all 49 unit
+ integration tests green (unchanged from v1.0.0-rc.1).
* `cargo deny check`: advisories ok, bans ok, licenses ok,
sources ok.
* `cargo about generate --output-file …`: zero warnings.
* `tools/dump_flutter_licenses.sh`: 94 packages, 0 without LICENSE.
* `flutter analyze`: clean.
No code changes touch the runtime; this is governance-tooling only.
Closes the v0.4 dual-file weakness in identity-at-rest and turns the
release into an MVP public release candidate. The remaining work
before `v1.0.0` is DEC-012 legal sign-off — see
`docs/governance/legal-review-readiness.md` — and the staged
platform promotions in `docs/governance/staged-release-plan.md`.
No decision rows in `product-decision-register.md` change; the
register's change-history advances to 0.9.8.
`chanora_storage`
-----------------
* New public `Crypto` trait + `IdentityFileStore::crypto()` give
callers an encrypt / decrypt pair anchored on the per-install
32-byte DEK without exposing the key material.
* `IdentityFileStore` keyring-first DEK retrieval (Linux Secret
Service via D-Bus, macOS Keychain, Windows Credential Manager,
iOS Keychain via the `keyring` crate). Pre-existing
`identity.dek` files are opportunistically migrated into the
keyring on first run; the on-disk DEK copy is removed once the
keyring acknowledges. `CHANORA_DISABLE_KEYRING=1` forces the
file-fallback path for tests and headless / CI hosts where a
real keyring call would prompt the user or block on a missing
D-Bus session.
* `BookmarkRepository::with_crypto(dir, crypto)` encrypts the
server password into a new `password_blob` BLOB column under
the same per-install DEK. Schema v2 migration is idempotent —
legacy v0.4 rows with a plain `password TEXT` are read
transparently and lifted into `password_blob` on the next
`update()`. `BookmarkRepository::new` (no crypto) is preserved
for tests and as a documented fallback when the DEK is
unreachable.
* Storage tests rise from 8 to 10: encrypted bookmark password
round-trip + legacy-plaintext-bookmark upgrade.
`chanora_core`
--------------
* `ChanoraSession::init_storage(dir)` wires the bookmark
repository with crypto by default. On any crypto-derivation
failure it falls back to the plain-password repository and
logs the gap — better than hard-failing init.
* `supervisor_loop` now tracks a 64-bit `snapshot_signature` over
channels (id + parent + order + name) and clients (id + channel
+ name) instead of the old `(channel_count, client_count)`
tuple. Any in-channel client move, channel rename, or reorder
now fires `SessionEvent::SnapshotChanged`. The signature sorts
by id before hashing so it's stable under input-vector
reordering.
* Two new unit tests cover the signature behaviour; new
`tests/mvp_storage.rs` integration test drives
`ChanoraSession::init_storage` end-to-end and verifies the
bookmark `password_blob` does not contain the plaintext.
* Re-export `ChannelId` + `ClientId` from `chanora_protocol` so
downstream callers and tests can construct DTOs directly.
Flutter
-------
* New About dialog (info icon in the AppBar) surfaces DEC-018
(public name "Chanora"), DEC-019 (non-affiliation statement),
and DEC-020 (Apache-2.0 OR MIT dual license). New ARB keys in
`app_en.arb` and `app_zh.arb`: `aboutAction`, `aboutVersion`,
`aboutNonAffiliation`, `aboutLicenseHeading`, `aboutLicenseBody`,
`aboutThirdPartyHeading`, `aboutThirdPartyBody`.
* `pubspec.yaml` version bumps to `1.0.0-rc.1+5`.
Governance
----------
* `docs/governance/legal-review-readiness.md` — DEC-012 handoff
package. Enumerates trademark / non-affiliation / license-text
/ third-party-attribution / `tsclientlib`-posture / crypto-
export / data-handling items the legal reviewer must confirm,
and lists the concrete engineering deliverables they block on
(`cargo about generate`, `cargo deny check licenses`,
Flutter `LicenseRegistry` dump).
* `docs/governance/staged-release-plan.md` — DEC-002 channel
schedule. Linux + Android sideload promote to GA on DEC-012
sign-off; Play Store / Windows / macOS / iOS gate on per-
platform signed-build availability. Rollback policy included.
* `product-decision-register.md` change-history advances to
0.9.8 with a single entry summarising v0.3, v0.4, and v1.0-rc.1
progress against DEC-001. No decision rows mutate.
Build + ops
-----------
* `NOTICE` refreshed for the MVP product-code dependency set:
adds `chacha20poly1305`, `rand`, `zeroize`, `base64`,
`keyring`, `connectivity_plus`, `path_provider`,
`freezed_annotation`; drops PoC-only entries.
* `CHANGELOG.md` restructured: explicit version sections for
v0.3.0-beta.1, v0.4.0-beta.2, v1.0.0-rc.1. Previous "Unreleased"
contents migrated into their respective milestone sections.
* `.github/workflows/ci.yml` exports `CHANORA_DISABLE_KEYRING=1`
for the cargo-test job — CI runners have no D-Bus session and
the keyring crate would otherwise block.
* `run-chanora.sh` reads `CHANORA_BUNDLE_FLAVOUR` (default
`release`) and self-copies the latest cdylib into the bundle's
`lib/` if missing.
Verification
------------
* `cargo test --workspace` with `CHANORA_DISABLE_KEYRING=1`: all
green (49 unit tests across the workspace; up from 36 at
v0.4.0-beta.2).
* `cargo test -p chanora_core --release -- --ignored alpha_smoke`
passes against the live `cn.teamspeak.app` (DNS → connect →
snapshot → disconnect in ~2.5 s).
* `flutter analyze`: clean.
* `cargo build -p chanora_bridge --release` + `flutter build
linux --release` produce a working Linux x86_64 bundle.
No Android live test in this commit per the user's note that the
physical device was removed; the Android arm64-v8a build path is
mechanically identical to v0.4.0-beta.2.