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.
199 KiB
Chanora SDD — Software Detailed Design
Document type: SDD / Software Detailed Design
Process alignment: ASPICE SWE.3 Software Detailed Design and Unit Construction
Version: 0.9.16
Status: Baseline Candidate
Language: English
Product: Chanora
Lifecycle position: SysRS -> SysDes -> SRS -> SAD -> SDD
Repo path: docs/architecture/sdd.md ---
1. Purpose
This document defines detailed software design units, APIs, data structures, and implementation-level design rules for Chanora.
The SDD derives from the SAD only.
2. Strict Traceability Rules
SDD shall link directly to SAD only.
SDD shall not use direct software-requirement source IDs.
SDD shall not use direct system-design source IDs.
SDD shall not use direct system-requirement source IDs.
3. Software Unit Decomposition
| Software Unit | Responsibility |
|---|---|
ChanoraApp |
Root application widget, initialization, theme mode, localization initialization |
ChanoraThemeFactory |
Builds ThemeData, ColorScheme, TextTheme, component themes |
ChanoraSemanticColors |
ThemeExtension for connection, voice, latency, diagnostic, and error states |
ChanoraSpacing |
ThemeExtension for spacing tokens |
ChanoraShape |
ThemeExtension for shape tokens |
ChanoraMotion |
ThemeExtension for motion tokens and reduced-motion behavior |
ChanoraAdaptiveShell |
Window class detection and shell layout selection |
CompactShell |
Single-column shell |
MediumShell |
Navigation rail / medium-width shell |
ExpandedShell |
Persistent pane shell |
ChanoraVoiceBar |
Persistent voice control component |
ChanoraChannelTree |
Hierarchical channel and client browser |
ChanoraClientTile |
Client row component |
ChanoraStatusBadge |
Accessible status indicator |
ChanoraAudioMeter |
Input level display |
LocalizationService |
Product-owned string localization and fallback |
ContentTextAdapter |
Server-provided text pass-through and display preparation |
UnicodeTextBoundary |
Boundary string validation and conversion helper |
PlatformInsetsService |
Safe area, keyboard, cutout, and desktop inset data |
BackIntentService |
Android/system back intent abstraction |
HapticsService |
Platform-aware haptic feedback wrapper |
BridgeFacade |
Flutter-to-Rust command and event facade |
CoreEventMapper |
Core-event-to-view-model mapping |
DiagnosticsTextPresenter |
Localized diagnostic descriptions and language-neutral event keys |
ArchitectureViewDocs |
Maintains context, container, component, runtime, deployment, and ADR architecture documentation |
DeploymentConfig |
Platform packaging and release configuration |
ArchitectureTraceChecker |
Checks direct-layer traceability rules for documentation |
4. SWE.3 Detailed Design Completeness Criteria
The detailed design shall be sufficient to support implementation and unit verification. For each software unit, the SDD shall identify where applicable:
| Detail | Required information |
|---|---|
| Unit responsibility | Purpose and boundaries of the unit |
| Inputs | Name, type, allowed range, default value, optionality |
| Outputs | Name, type, allowed range, default value, error behavior |
| Interfaces | Internal and external software interfaces |
| Dynamic behavior | State changes, sequence behavior, asynchronous behavior |
| Error behavior | Recoverable errors, fatal errors, fallback behavior |
| Resource behavior | Buffering, time-sensitive operations, memory expectations |
| Unit verification hooks | Test seams and expected verification approach |
5. Software Unit Interface Detail Catalog
| Unit | Interface | Direction | Name | Type | Unit | Resolution | Range | Default | Error / fallback |
|---|---|---|---|---|---|---|---|---|---|
ChanoraThemeFactory |
Theme creation | Input | seedColor | Color value | n/a | n/a | valid color | Chanora default seed | fallback to default theme |
ChanoraThemeFactory |
Theme creation | Input | brightness | enum | n/a | n/a | light/dark | system setting | fallback to light |
ChanoraThemeFactory |
Theme creation | Output | themeData | ThemeData | n/a | n/a | valid ThemeData | n/a | construction error treated as app initialization failure |
ChanoraAdaptiveShell |
Window classification | Input | widthDp | double | dp | 1dp | >=0 | current window width | compact fallback |
ChanoraAdaptiveShell |
Window classification | Output | windowClass | enum | n/a | n/a | compact/medium/expanded | compact | compact fallback |
LocalizationService |
String lookup | Input | key | string | n/a | n/a | non-empty | n/a | fallback key display or English fallback |
LocalizationService |
String lookup | Input | locale | locale ID | n/a | n/a | supported locale | English | English fallback |
LocalizationService |
String lookup | Output | localizedText | UTF-8 string | n/a | n/a | valid Unicode | English fallback | missing-key diagnostic |
UnicodeTextBoundary |
Text validation | Input | rawText | external string/bytes | n/a | n/a | platform/protocol dependent | n/a | boundary conversion error |
UnicodeTextBoundary |
Text validation | Output | contentText | UTF-8 string | n/a | n/a | valid Unicode | replacement/failure policy | conversion diagnostic |
BridgeFacade |
Command bridge | Input | commandDto | DTO | n/a | n/a | valid command schema | n/a | bridge error DTO |
BridgeFacade |
Command bridge | Output | commandResult | DTO/result | n/a | n/a | success/error | n/a | typed error |
CoreEventMapper |
Event mapping | Input | coreEvent | event DTO | n/a | n/a | known event type | n/a | unknown event diagnostic |
CoreEventMapper |
Event mapping | Output | viewModelDelta | UI model delta | n/a | n/a | valid UI state change | n/a | safe no-op + diagnostic |
DiagnosticsTextPresenter |
Diagnostic presentation | Input | diagnosticKey | stable key | n/a | n/a | known/unknown key | n/a | generic localized fallback |
DiagnosticsTextPresenter |
Diagnostic presentation | Output | diagnosticText | localized string | n/a | n/a | valid Unicode | English fallback | fallback diagnostic text |
ChanoraAudioMeter |
Level display | Input | level | double | normalized | 0.01 | 0.0-1.0 | 0.0 | clamp to valid range |
ChanoraAudioMeter |
Level display | Output | semanticValue | localized string | n/a | n/a | valid string | "No input" | localized fallback |
BackIntentService |
Back handling | Input | platformBackEvent | platform event | n/a | n/a | platform-defined | n/a | ignored if unsupported |
BackIntentService |
Back handling | Output | backIntent | enum | n/a | n/a | pop/close/ignore/exitCandidate | ignore | safe no-op |
6. Detailed Design Evaluation Checklist
| Evaluation area | Question |
|---|---|
| Interoperability | Are interfaces compatible across units and layers? |
| Interaction | Are runtime interactions and asynchronous flows defined? |
| Criticality | Are critical voice, connection, security, and diagnostics behaviors detailed? |
| Technical complexity | Are complex units decomposed and testable? |
| Risks | Are platform, audio, protocol, encoding, and localization risks addressed? |
| Testability | Can unit tests, widget tests, integration tests, and platform tests be derived from the design? |
| Traceability | Does every detailed item link directly to SAD only? |
| Consistency | Are SAD, SDD, unit interfaces, and verification expectations consistent? |
7. Detailed Design Items
SDD-001: ChanoraApp shall initialize theme, localization, platform services, and routing before rendering feature content.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraApp - Source SAD: SAD-001, SAD-031
- Verification method: Unit Test, Integration Test
Initialization order: platform bindings -> localization resources -> theme model -> app router -> adaptive shell.
SDD-002: ChanoraThemeFactory shall create Material 3 ThemeData for light and dark modes.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraThemeFactory - Source SAD: SAD-001, SAD-002, SAD-043
- Verification method: Unit Test
The factory returns ThemeData instances with Material 3 enabled and populated ColorScheme and TextTheme values.
SDD-003: ChanoraThemeFactory shall support seed-color replacement without feature-screen changes.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraThemeFactory - Source SAD: SAD-001, SAD-002, SAD-044
- Verification method: Unit Test
Seed color is injected into theme construction and not referenced in feature modules.
SDD-004: ChanoraSemanticColors shall define connection state colors independently from base ColorScheme.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraSemanticColors - Source SAD: SAD-004, SAD-007
- Verification method: Unit Test
Connection tokens include connected, connecting, reconnecting, disconnected, and error roles.
SDD-005: ChanoraSemanticColors shall define voice state colors independently from base ColorScheme.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraSemanticColors - Source SAD: SAD-005, SAD-007
- Verification method: Unit Test
Voice tokens include speaking, muted, deafened, push-to-talk-active, and unavailable roles.
SDD-006: ChanoraSemanticColors shall define latency, packet-loss, diagnostics, and error semantic roles.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraSemanticColors - Source SAD: SAD-006, SAD-007
- Verification method: Unit Test
Token names remain semantic and shall not embed raw color names in API names.
SDD-007: ChanoraSpacing shall define reusable spacing values for compact, medium, and expanded layouts.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraSpacing - Source SAD: SAD-002, SAD-003
- Verification method: Unit Test
Spacing APIs expose xs, sm, md, lg, xl, and xxl values.
SDD-008: ChanoraShape shall define reusable corner and shape values for cards, buttons, panels, badges, and sheets.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraShape - Source SAD: SAD-002, SAD-003
- Verification method: Unit Test
Shape APIs expose none, small, medium, large, extraLarge, and full values.
SDD-009: ChanoraMotion shall define standard durations and reduced-motion behavior.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraMotion - Source SAD: SAD-009
- Verification method: Unit Test
The unit maps reduced-motion settings to no-motion or simplified-motion behavior.
SDD-010: Feature widgets shall read tokens through Flutter theme accessors only.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
Feature Widgets - Source SAD: SAD-003, SAD-007
- Verification method: Static Inspection
Feature code may call Theme.of(context) and extension accessors, but shall not instantiate semantic tokens.
SDD-011: ChanoraAdaptiveShell shall classify window width into compact, medium, or expanded.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraAdaptiveShell - Source SAD: SAD-008
- Verification method: Unit Test
Classification thresholds are centralized in the shell configuration.
SDD-012: ChanoraAdaptiveShell shall dispatch to CompactShell, MediumShell, or ExpandedShell.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraAdaptiveShell - Source SAD: SAD-008, SAD-009, SAD-010, SAD-011
- Verification method: Widget Test
Layout selection shall be deterministic for a given window class.
SDD-013: CompactShell shall render single-column content and mobile navigation.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
CompactShell - Source SAD: SAD-009
- Verification method: Widget Test
Compact layout shall avoid side-by-side pane requirements.
SDD-014: MediumShell shall render side navigation or navigation rail behavior.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
MediumShell - Source SAD: SAD-010
- Verification method: Widget Test
Medium layout shall preserve main content and primary navigation.
SDD-015: ExpandedShell shall render persistent pane regions and persistent voice controls.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ExpandedShell - Source SAD: SAD-011
- Verification method: Widget Test
Expanded layout shall support a side pane, main pane, and persistent VoiceBar slot.
SDD-016: ConnectionStatusPresenter shall provide non-color-only connection state presentation data.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ConnectionStatusPresenter - Source SAD: SAD-012, SAD-014
- Verification method: Unit Test
The presenter returns label, icon, severity token, and accessibility description.
SDD-017: ChanoraVoiceBar shall expose mute, deaf, push-to-talk, input meter, current channel, and latency properties.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraVoiceBar - Source SAD: SAD-013
- Verification method: Widget Test
VoiceBar properties are supplied from view models and shall not call Rust Core directly.
SDD-018: ChanoraVoiceBar shall remain renderable as a persistent shell slot.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraVoiceBar - Source SAD: SAD-007, SAD-013
- Verification method: Widget Test
The component shall not depend on a specific route.
SDD-019: ChanoraChannelTree shall support hierarchical channel nodes and client rows.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraChannelTree - Source SAD: SAD-003, SAD-006
- Verification method: Widget Test
The component consumes ChannelTreeVm data and supports expansion state.
SDD-020: ChanoraChannelTree shall support selected state, expansion state, join intent, and context-menu intent.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraChannelTree - Source SAD: SAD-006, SAD-015
- Verification method: Widget Test
UI events are emitted through callbacks and view-model intents.
SDD-021: ChanoraClientTile shall display client status with text, icon, and semantic state cues.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraClientTile - Source SAD: SAD-003, SAD-014
- Verification method: Widget Test, Accessibility Test
Client state shall not rely on color alone.
SDD-022: ChanoraStatusBadge shall expose visible text or icon and accessibility semantics.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraStatusBadge - Source SAD: SAD-012, SAD-014
- Verification method: Widget Test, Accessibility Test
Badge states include normal, warning, error, speaking, muted, and disconnected.
SDD-023: ChanoraAudioMeter shall display audio input level and support reduced-motion fallback.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ChanoraAudioMeter - Source SAD: SAD-009, SAD-013
- Verification method: Widget Test
Reduced-motion mode shall use simplified or static visual behavior.
SDD-024: All icon-only buttons shall provide localized semantic labels.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
Design System Components - Source SAD: SAD-014, SAD-020
- Verification method: Accessibility Test
Icon-only control constructors require label keys or semantic labels.
SDD-025: Focusable components shall define focus order and visible focus style.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
FocusPolicy - Source SAD: SAD-015
- Verification method: Widget Test
Focus traversal shall be testable with keyboard navigation.
SDD-026: Adaptive shell panes shall support increased text scale without losing critical actions.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
Adaptive Shell Components - Source SAD: SAD-016
- Verification method: Accessibility Test
Critical controls shall wrap, scroll, or remain reachable.
SDD-027: PlatformInsetsService shall provide safe area, keyboard inset, display cutout, and desktop inset data.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
PlatformInsetsService - Source SAD: SAD-017
- Verification method: Platform Test
Insets are consumed by shell layout rather than individual feature screens.
SDD-028: BackIntentService shall normalize Android/system back behavior into route-level intents.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
BackIntentService - Source SAD: SAD-018
- Verification method: Platform Test, Widget Test
Back handling shall be testable without direct platform branching in feature screens.
Implementation requirements:
-
Registration paths (Android, minSdk per SDD-073):
- On API 33+ (
Build.VERSION.SDK_INT >= TIRAMISU), the hostMainActivityshall register anOnBackInvokedCallbackagainstonBackInvokedDispatcherat priorityPRIORITY_DEFAULT. The callback shall delegate toBackIntentService.dispatch()and return without calling the system fallback. The callback shall be unregistered inonDestroy. - On API 28–32 (pre-33 fallback), the host
MainActivityshall add anOnBackPressedCallback(enabled = true) toonBackPressedDispatcher. The callback shall delegate toBackIntentService.dispatch(). Whendispatch()returnsBackIntent.exitCandidate, the callback shall setisEnabled = falseand re-invokeonBackPressed()exactly once so the OS finishes the activity. The callback shall be removed ononDestroy. - The predictive-back gesture animation provided by API 34+ is permitted;
BackIntentServiceshall not depend on it and shall remain functional when predictive back is disabled by the OEM.
- On API 33+ (
-
Route-pop semantics:
BackIntentService.dispatch()shall resolve the back intent in the following deterministic order and emit exactly oneBackIntentper platform back event: a. If the in-call PTT key is currently pressed (transmit_active = true viaAudioTransmitGate.load()per SDD-089) the service shall returnBackIntent.ignore. The platform back event shall be consumed without UI effect to prevent accidental disconnection during transmit. b. Else if a modal or dialog route is open on the Navigator (Navigator.canPop(context)true AND topmost routeisDialog == trueorfullscreenDialog == true), the service shall returnBackIntent.closeand pop only the modal/dialog. c. Else if the Navigator can pop a non-root route, the service shall returnBackIntent.popand pop one route. d. Else (at root route, no modal, no PTT), the service shall returnBackIntent.exitCandidateand defer to the platform fallback (system back / activity finish) per the registration rules above. -
Interface output enum:
BackIntent ::= { pop, close, ignore, exitCandidate }(matches §5 interface catalog). -
Threading:
dispatch()runs on the platform main thread; reads ofAudioTransmitGateshall use the atomic load path (no blocking await on the bridge isolate). -
Diagnostics: each dispatch shall emit a structured event with the resolved intent and the route depth; the event shall not include route arguments, user content, or PTT key identifiers (per SAD-077 / SDD-090 sanitiser rules).
-
Non-Android platforms:
BackIntentServiceshall expose the same Dart interface but the registration path shall no-op; feature screens shall remain unchanged across platforms (single back-intent abstraction).
SDD-029: HapticsService shall expose platform-aware haptic feedback operations.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
HapticsService - Source SAD: SAD-019
- Verification method: Platform Test
Haptics calls are optional and shall no-op where unsupported.
SDD-030: KeyboardAvoidancePolicy shall prevent critical controls from being covered by virtual keyboards.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
KeyboardAvoidancePolicy - Source SAD: SAD-017, SAD-019
- Verification method: Platform Test
Inputs and primary actions remain reachable when the virtual keyboard appears.
SDD-031: LocalizationService shall load product string resources for the active locale.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
LocalizationService - Source SAD: SAD-020
- Verification method: Unit Test
The unit returns localized strings by key.
SDD-032: LocalizationService shall fall back to the baseline English locale when a key is missing.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
LocalizationService - Source SAD: SAD-020
- Verification method: Unit Test
Fallback behavior is deterministic and logged for diagnostics.
SDD-033: LocalizationService shall allow new locale resources without changes to protocol, audio, state, or storage modules.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
LocalizationService - Source SAD: SAD-020
- Verification method: Inspection
Locale resource registration is separated from non-UI software modules.
SDD-034: ContentTextAdapter shall pass through server-provided names and messages without translation.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ContentTextAdapter - Source SAD: SAD-021
- Verification method: Integration Test
Server content is treated as user/server content rather than product copy.
SDD-035: UnicodeTextBoundary shall validate cross-layer text DTOs as UTF-8 compatible strings.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
UnicodeTextBoundary - Source SAD: SAD-022, SAD-023
- Verification method: Unit Test, Integration Test
Invalid external encodings are handled at boundaries.
SDD-036: Protocol-boundary conversion shall be implemented only in protocol adapter code.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
Protocol Adapter Text Boundary - Source SAD: SAD-023
- Verification method: Inspection
No general Flutter feature code may perform protocol encoding conversion.
SDD-037: Platform-boundary conversion shall be implemented only in platform adapter code.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
Platform Adapter Text Boundary - Source SAD: SAD-023
- Verification method: Inspection
Platform-specific string conversion remains explicit and localized.
SDD-038: Diagnostics export shall preserve Unicode after redaction.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
DiagnosticsTextPresenter - Source SAD: SAD-024
- Verification method: Test, Audit
Redaction removes secrets, not arbitrary multilingual content.
SDD-039: Bidi text rendering shall use Flutter text widgets and directionality behavior compatible with platform support.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
Flutter Text Presentation - Source SAD: SAD-025
- Verification method: Localization Test
Text rendering allows LTR and RTL content where supported.
SDD-040: Locale-aware formatting shall be centralized in localization utilities.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
LocalizationFormatting - Source SAD: SAD-026
- Verification method: Unit Test
Dates, times, numbers, and timestamps are formatted by locale-aware helpers.
SDD-041: Diagnostics event keys shall remain stable and language-neutral.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
DiagnosticsTextPresenter - Source SAD: SAD-027
- Verification method: Unit Test
User-facing descriptions are separate from machine-readable event keys.
SDD-042: UI/UX supporting documents shall be versioned with the software architecture baseline.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
Documentation Set - Source SAD: SAD-028
- Verification method: Review
Guidelines, token docs, component docs, adaptive docs, and platform docs are maintained as supporting baselines.
SDD-043: SAD source validation shall reject direct lower-layer source fields.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
Traceability Checker - Source SAD: SAD-029
- Verification method: Inspection
Automated or manual review checks SAD source fields.
SDD-044: SDD source validation shall reject direct non-SAD source fields.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
Traceability Checker - Source SAD: SAD-030, SAD-045
- Verification method: Inspection
Automated or manual review checks SDD source fields.
SDD-045: Theme/localization/platform initialization shall be tested before first feature render.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
App Initialization Tests - Source SAD: SAD-031
- Verification method: Integration Test
The app shell is tested for initialization order.
SDD-046: Bridge facade DTOs shall expose only stable UI-safe data structures.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
BridgeFacade - Source SAD: SAD-040, SAD-041
- Verification method: Integration Test
Flutter must not depend on Rust internal types.
SDD-047: Core event mapping shall convert Rust Core events into feature view models.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
CoreEventMapper - Source SAD: SAD-041
- Verification method: Unit Test
Event mapping is deterministic and testable.
SDD-048: Audio state view models shall separate UI state from real-time audio processing.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
VoiceViewModel - Source SAD: SAD-034, SAD-039
- Verification method: Unit Test
Voice UI receives status values without performing DSP.
SDD-049: Storage access shall be isolated from feature widgets behind repository or service APIs.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
StorageFacade - Source SAD: SAD-035
- Verification method: Inspection
Feature widgets shall not call database APIs directly.
SDD-050: Diagnostics UI shall obtain localized display text from diagnostics presentation services.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
DiagnosticsTextPresenter - Source SAD: SAD-027, SAD-036, SAD-050
- Verification method: Widget Test
Diagnostics UI consumes display models.
SDD-051: ArchitectureViewDocs shall maintain context, container, component, runtime, deployment, data, security, diagnostics, UI, audio, protocol, and i18n views.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
ArchitectureViewDocs - Source SAD: SAD-046, SAD-048, SAD-049, SAD-050
- Verification method: Review
Architecture views are updated when architecture-affecting changes occur.
SDD-052: ArchitectureViewDocs shall maintain an ADR index for accepted, deprecated, and superseded architectural decisions.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
ArchitectureViewDocs - Source SAD: SAD-047
- Verification method: Review
ADR entries include decision, rationale, status, and impact.
SDD-053: DeploymentConfig shall define platform-specific packaging and release configuration units.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
DeploymentConfig - Source SAD: SAD-049
- Verification method: Inspection
Deployment configuration separates platform package metadata from feature code.
SDD-054: DiagnosticsArchitecture shall centralize logging, event keys, localized descriptions, redaction, and export presentation.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
DiagnosticsArchitecture - Source SAD: SAD-050
- Verification method: Unit Test, Audit
Diagnostics behavior is implemented through centralized services.
SDD-055: ArchitectureTraceChecker shall check that SDD items reference only SAD IDs.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
ArchitectureTraceChecker - Source SAD: SAD-030, SAD-045
- Verification method: Inspection
A documentation check fails when non-SAD source IDs appear in SDD item source fields.
SDD-056: ArchitectureTraceChecker shall check that SAD items reference only SRS IDs.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
ArchitectureTraceChecker - Source SAD: SAD-029
- Verification method: Inspection
A documentation check fails when non-SRS source IDs appear in SAD item source fields.
SDD-057: ArchitectureViewDocs shall define reviewable architecture views for context, containers, components, runtime behavior, deployment, data, security, diagnostics, UI, audio, protocol, and i18n.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
ArchitectureViewDocs - Source SAD: SAD-046, SAD-051
- Verification method: Review
Each architecture view is maintained as part of the SAD baseline and referenced by the affected detailed design units.
SDD-058: ArchitectureViewDocs shall define an ADR record structure with ID, title, status, decision, rationale, consequences, affected units, and change history.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
ArchitectureViewDocs - Source SAD: SAD-047
- Verification method: Review
ADR records support architecture communication and change impact analysis.
SDD-059: InterfaceDetailCatalog shall refine SAD architectural interfaces into unit-level interface entries.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
InterfaceDetailCatalog - Source SAD: SAD-052, SAD-057
- Verification method: Inspection
Each entry includes name, type, unit, resolution, range, default value, and error/fallback behavior where applicable.
SDD-060: InterfaceDetailCatalog shall define command, event, localization, Unicode, diagnostics, adaptive layout, audio meter, and platform-intent interfaces.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
InterfaceDetailCatalog - Source SAD: SAD-052, SAD-056, SAD-057
- Verification method: Inspection
The catalog supports integration and unit verification planning.
SDD-061: PerformanceDesignNotes shall record UI responsiveness, audio latency, memory growth, reconnect, localization, and diagnostics resource considerations.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
PerformanceDesignNotes - Source SAD: SAD-053
- Verification method: Review, Analysis
Performance notes identify design mechanisms and verification hooks.
SDD-062: AssumptionRegister shall record software design assumptions and dependencies that affect feasibility.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
AssumptionRegister - Source SAD: SAD-054
- Verification method: Review
Assumptions are reviewed during change impact analysis.
SDD-063: ArchitectureGlossary shall provide definitions for architecture and detailed design terms.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P2 / Production
- Software unit:
ArchitectureGlossary - Source SAD: SAD-055
- Verification method: Review
The glossary reduces ambiguity across software architecture, detailed design, and verification.
SDD-064: Each software unit detailed design shall include responsibility, inputs, outputs, interfaces, dynamic behavior, error behavior, resource behavior, and verification hooks where applicable.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
SoftwareUnitDesignTemplate - Source SAD: SAD-058, SAD-059
- Verification method: Review, Inspection
The template defines the minimum expected detail for unit construction and unit verification.
SDD-065: UnitConstructionGuideline shall state that software units are constructed according to the SDD rather than reverse-documented from code.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
UnitConstructionGuideline - Source SAD: SAD-059
- Verification method: Review
The unit construction guideline preserves design-to-code traceability.
SDD-066: SoftwareUnitTraceChecker shall verify that SDD items link directly to SAD IDs only.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
SoftwareUnitTraceChecker - Source SAD: SAD-030, SAD-045, SAD-060
- Verification method: Inspection
The checker supports traceability rule enforcement.
SDD-067: SoftwareUnitTraceChecker shall support impact analysis from changed SAD items to affected SDD items.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
SoftwareUnitTraceChecker - Source SAD: SAD-060
- Verification method: Inspection
Impact analysis output identifies affected detailed design units.
SDD-068: DetailedDesignReviewRecord shall capture review evidence for SDD completeness, testability, and consistency with SAD.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
DetailedDesignReviewRecord - Source SAD: SAD-059, SAD-060
- Verification method: Review
Review records support baseline approval and communication.
SDD-069: UnitVerificationHandoff shall identify verification method candidates for each software unit.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
UnitVerificationHandoff - Source SAD: SAD-059
- Verification method: Review
The handoff supports unit verification planning.
SDD-070: SoftwareUnitRegistry shall maintain the list of software units, their owning component, source SAD items, and implementation status.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
SoftwareUnitRegistry - Source SAD: SAD-058, SAD-060
- Verification method: Inspection
The registry supports consistency between architecture, detailed design, implementation, and verification.
8. SAD-to-SDD Coverage Matrix
| SAD Range | SDD Coverage |
|---|---|
| SAD-001 through SAD-007 | SDD-001 through SDD-010 |
| SAD-008 through SAD-011 | SDD-011 through SDD-015 |
| SAD-012 through SAD-016 | SDD-016 through SDD-026 |
| SAD-017 through SAD-019 | SDD-027 through SDD-030 |
| SAD-020 through SAD-027 | SDD-031 through SDD-041 |
| SAD-028 through SAD-031 | SDD-042 through SDD-045 |
| SAD-032 through SAD-038 | SDD-046 through SDD-050 |
| SAD-039 through SAD-045 | SDD-046 through SDD-050, SDD-055, SDD-056 |
| SAD-046 through SAD-050 | SDD-051 through SDD-058, SDD-061 |
| SAD-051 through SAD-060 | SDD-057 through SDD-070 |
9. Change History
| Version | Date | Description |
|---|---|---|
| 0.1.0 | 2026-05-14 | Initial SDD baseline covering SAD-001 through SAD-045 and enforcing SAD-only direct traceability. |
| 0.2.0 | 2026-05-14 | Updated SDD coverage for expanded SAD v0.2 architecture views, ADRs, deployment architecture, diagnostics architecture, and traceability checker units. |
| 0.3.0 | 2026-05-14 | Added SWE.3 detailed-design completeness criteria, unit interface detail catalog, evaluation checklist, unit construction guideline, and additional SAD coverage. |
10. Platform Baseline and Product Decision Detailed Design Addendum
SDD-071: IOSBuildConfig shall define the iOS minimum runtime deployment target as iOS 13 unless an approved platform change raises it.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
IOSBuildConfig - Source SAD: SAD-061
- Verification method: Inspection, Platform Test
SDD-072: IOSReleaseGate shall verify that App Store Connect upload builds use Xcode 26 or later and the iOS 26 / iPadOS 26 SDK or later on or after 2026-04-28 unless Apple publishes a newer applicable upload requirement.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
IOSReleaseGate - Source SAD: SAD-062
- Verification method: Release Inspection
SDD-073: AndroidBuildConfig shall define Android API 28 (Android 9.0 Pie) as the minimum Android runtime baseline per DEC-004 and shall expose target SDK configuration for release inspection. This supersedes the prior API 24 recommendation and is reconciled with SRS-187 / SysRS-288 / SAD-063.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
AndroidBuildConfig - Source SAD: SAD-063
- Verification method: Platform Test, Release Inspection, Build Test
Implementation requirements (Gradle / apps/chanora_flutter/android/app/build.gradle.kts):
minSdk = 28. The value shall be sourced from a singlegradle.properties(or equivalent) constantchanora.android.minSdkso that release-inspection tooling can assert it without parsing Kotlin DSL.targetSdkpolicy:targetSdkshall be set to the Android API level required by Google Play on the upload date per SRS-188. The build shall not pintargetSdkbelow the Play-required floor. Release inspection shall verifytargetSdk >= <Play-required floor>at build time and fail the build otherwise.compileSdkshall be>= targetSdkand shall track the latest stable Android SDK supported by the configured Android Gradle Plugin.- NDK ABIs:
ndk.abiFiltersshall include exactlyarm64-v8a,armeabi-v7a, andx86_64. Other ABIs (e.g.,x86,mips) shall not be packaged. The AAB output (see SDD-107) shall rely on Play's per-ABI delivery rather than a fat APK. - Release signing config: release builds shall use a non-debug signing configuration.
AndroidBuildConfigshall not embed key material in the repository; signing credentials shall be supplied at CI time via environment variables (e.g.,CHANORA_ANDROID_KEYSTORE_PATH,CHANORA_ANDROID_KEYSTORE_PASSWORD,CHANORA_ANDROID_KEY_ALIAS,CHANORA_ANDROID_KEY_PASSWORD) or a CI secret manager. CI shall fail the release pipeline if any of these variables is unset, ifsigningConfigresolves todebug, or ifsigningConfig.storeFilepoints to a path matching**/debug.keystore. Local developer builds may skip the assertion when the Gradle propertychanora.release.signing.requiredis false. - R8 / ProGuard stance: release builds shall enable R8 (
isMinifyEnabled = true,isShrinkResources = true) with the default Android optimization rules plus a projectproguard-rules.prothat keeps:- JNI entry classes for
chanora_bridge(covered by SDD-105AndroidJniBootstrap), - Flutter and FRB generated bindings,
- any class referenced solely via reflection from native code.
Debug builds shall set
isMinifyEnabled = false.
- JNI entry classes for
- AAB output format: the release deliverable shall be the Android App Bundle (
.aab) produced bybundleReleaseper SRS-119. APKs shall be produced only for internal QA distribution. See SDD-107 (AndroidAaBuildPipeline) for the packaging-pipeline detail. - Manifest preconditions:
uses-sdkshall not overrideminSdkVersionbelow 28; tooling shall reject any module-level manifest that lowers it.
SDD-074: ConnectionSessionManager shall enforce a single active server connection in MVP.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
ConnectionSessionManager - Source SAD: SAD-064
- Verification method: Unit Test, System Test
SDD-075: AudioProcessingSettings shall default AEC, AGC, Noise Suppression, and High-Pass Filter to enabled where supported and stable.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
AudioProcessingSettings - Source SAD: SAD-065
- Verification method: Unit Test, Audio Test
SDD-076: AudioProcessingBackendSelector shall select platform-native audio processing first where available and stable and isolate any fallback backend behind the audio subsystem interface.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
AudioProcessingBackendSelector - Source SAD: SAD-066
- Verification method: Unit Test, Audio Test
SDD-077: LocalDatabaseRepository shall persist non-secret local state through SQLite or an equivalent embedded database abstraction.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
LocalDatabaseRepository - Source SAD: SAD-067
- Verification method: Unit Test, Storage Test
SDD-078: SecretStorageRepository shall persist secrets only through platform secure storage service interfaces.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
SecretStorageRepository - Source SAD: SAD-067
- Verification method: Unit Test, Security Audit
SDD-079: TypedBridgeFacade shall define generated or schema-controlled DTOs for Flutter/Rust commands, results, and events.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
TypedBridgeFacade - Source SAD: SAD-068
- Verification method: Integration Test, Inspection
SDD-080: DiagnosticsPrivacyGate shall disable automatic diagnostic upload, telemetry upload, and crash reporting in MVP release configuration unless an approved decision enables them.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
DiagnosticsPrivacyGate - Source SAD: SAD-069, SAD-070
- Verification method: Privacy Review, Security Audit
SDD-081: DesktopPttBackend shall be a Rust trait in chanora_audio::ptt exposing fn capability(&self) -> PttCapabilityLevel, fn backend_id(&self) -> &'static str, fn start(&mut self, gate: Arc<AudioTransmitGate>, binding: PttBinding) -> Result<(), PttError>, fn stop(&mut self), and fn rebind(&mut self, binding: PttBinding) -> Result<(), PttError>. Implementations shall be Send and stop on Drop.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
DesktopPttBackend - Source SAD: SAD-071
- Verification method: Unit Test, Inspection
SDD-082: PttCapabilityLevel shall be a plain Rust enum (L0Focused, L1GlobalShortcut, L2GlobalHoldToTalk, L3GlobalWithMouseButtons, L4DeviceAware) with a public as_str(&self) -> &'static str mapping for diagnostics. L4DeviceAware is reserved and shall not be produced by any MVP implementation.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
PttCapabilityLevel - Source SAD: SAD-071
- Verification method: Unit Test
SDD-083: WindowsRawInputBackend shall use the RegisterRawInputDevices Win32 API targeting HID_USAGE_PAGE_GENERIC keyboard and mouse usages, run a dedicated message-only window on its own OS thread, and forward only sanitised binding-match results to the AudioTransmitGate. Raw RAWINPUT payloads shall never cross the backend boundary.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
WindowsRawInputBackend - Source SAD: SAD-072
- Verification method: Platform Test (Windows), Unit Test
SDD-084: WindowsHookBackend shall be used only when Raw Input registration fails. It installs WH_KEYBOARD_LL and WH_MOUSE_LL hooks on its dedicated thread; the hook callbacks compare against the active binding and call AudioTransmitGate::set only, never logging key data. The hook is uninstalled on Drop.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
WindowsHookBackend - Source SAD: SAD-072
- Verification method: Platform Test (Windows), Unit Test
SDD-085: MacOSEventTapBackend shall create a CGEventTap filtered to keyboard and mouse-button events, attached to the main run loop with an explicit retain. The tap callback shall consult the active binding and call AudioTransmitGate::set only. The backend shall expose permission_state() -> Granted | Denied | Undetermined via IOHIDCheckAccess (or the macOS-version-appropriate equivalent) and shall return PttCapabilityLevel::L0Focused when permission is not Granted.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
MacOSEventTapBackend - Source SAD: SAD-073
- Verification method: Platform Test (macOS), User Acceptance Test
SDD-086: LinuxGnomeWaylandBackend shall call CreateSession, BindShortcuts, and listen for Activated / Deactivated signals on the freedesktop org.freedesktop.portal.GlobalShortcuts D-Bus interface. Binding capture shall use the portal's own configuration dialog; Chanora itself does not read raw key events. Missing D-Bus or missing portal interface resolves to PttCapabilityLevel::L0Focused.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
LinuxGnomeWaylandBackend - Source SAD: SAD-074
- Verification method: Platform Test (Linux, GNOME Wayland), Unit Test
SDD-087: FocusedPttBackend shall integrate with Flutter's keyboard / pointer event stream via the existing Listener widget path and the bridge set_ptt command. Reports PttCapabilityLevel::L0Focused and backend_id = "focused".
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
FocusedPttBackend - Source SAD: SAD-071, SAD-072, SAD-073, SAD-074
- Verification method: Unit Test, UI Review
SDD-088: PttController shall sit in chanora_core::ptt and own a Box<dyn DesktopPttBackend>, a clone of AudioTransmitGate, a watch::Sender<PttCapabilityLevel> for live publication to the bridge, and a tokio::sync::Mutex over the active PttBinding. Public surface: set_binding, current_capability, subscribe_capability.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
PttController - Source SAD: SAD-071, SAD-076
- Verification method: Unit Test, Integration Test
SDD-089: AudioTransmitGate shall be Arc<AudioTransmitGate> wrapping a tokio::sync::watch::Sender<bool> and an internal AtomicBool. set(v) updates both atomically; load() reads the atomic; subscribe() hands out a watch::Receiver for the missed-key-up watchdog task.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
AudioTransmitGate - Source SAD: SAD-075
- Verification method: Unit Test
SDD-090: PttSanitizer shall implement tracing_subscriber::Layer and decorate the existing RedactingLogLayer. Records whose field names match any of the following banned field names shall be dropped before on_event reaches the wrapped sink: key_code, scan_code, virtual_key, vk, keysym, keysym_string, key_sequence (7 fields required by SAD-077) and key_press_history, key_timing (2 defensive additions for future PTT diagnostic fields that might carry raw key data). The implementation shall be allocation-free on the success path (the typical "no banned field" case).
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
PttSanitizer - Source SAD: SAD-077
- Verification method: Unit Test, Privacy Review
SDD-091: PttCapabilityBadge shall be a Flutter widget rendered inside the Voice Bar, subscribed to the FRB stream of BridgeEvent::PttCapability. The widget shall map each enum value to a short user-facing label and shall render an info icon that opens a per-platform explanation sheet when the resolved capability is L0Focused.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
PttCapabilityBadge - Source SAD: SAD-078
- Verification method: UI Review, User Acceptance Test
SDD-092: MissedKeyUpWatchdog shall be a tokio task spawned by the audio engine that holds the watch::Receiver<bool> from AudioTransmitGate::subscribe(). On each transition from false to true the task records the timestamp; on each true -> false transition the task clears the timestamp. If the transition timeline shows true for longer than the configured timeout the task calls AudioTransmitGate::set(false) and emits a sanitised diagnostic record.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
MissedKeyUpWatchdog - Source SAD: SAD-079
- Verification method: Unit Test, Integration Test
SDD-093: PttInputClass shall be a Rust enum in chanora_audio::ptt_backends with variants None, Keyboard, and MouseSideButton. The enum is the only carrier of the user's bound input class across the audio + bridge + Flutter boundary; its as_str(&self) -> &'static str mapping returns the diagnostic-safe label ("" / "keyboard" / "mouse-side-button") consumed by PttBackendDescriptor::bound_input_class. The Windows backend ladder and the macOS event-tap backend shall observe the MouseSideButton variant on their binding-rebind path and translate it into the platform's native side-button identifier internally; the Linux GNOME-Wayland portal backend shall accept whatever input class the portal returns from BindShortcuts and apply the same classifier already documented in desktop-ptt-architecture.md §5.3. The Flutter binding-capture dialog shall capture mouse side-button presses through the PointerEvent.buttons bitmask (back = 0x08, forward = 0x10) and never forward the raw bitmask beyond the dialog — the bridge set_ptt_binding call carries only the enum variant.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
PttInputClass,WindowsRawInputBackend,WindowsHookBackend,MacOSEventTapBackend,LinuxGnomeWaylandBackend,_PttBindingCaptureDialog - Source SAD: SAD-080
- Verification method: Unit Test, Platform Test (Windows / macOS / Linux), UI Review
SDD-094: chanora_bridge::voice::voice_join(channel_id: ChannelId) -> Result<(), VoiceError> and chanora_bridge::voice::voice_leave() -> Result<(), VoiceError> shall be the only public-bridge voice lifecycle calls. Internally chanora_audio::engine::AudioEngine::ensure_running() shall be idempotent and shall open the cpal input and output streams on the first call of the session; chanora_audio::engine::AudioEngine::shutdown_if_idle() shall be called from voice_leave and shall close both streams when no voice channels remain. The Flutter widget tree shall subscribe to BridgeEvent::VoiceState { in_channel, transmit_mode, mute, release_tail_ms } through the existing FRB stream and shall render the Voice Bar from that stream. The pre-existing start_audio and stop_audio FRB symbols shall be deleted from crates/chanora_bridge/src/api.rs together with their generated bindings; the corresponding caller in apps/chanora_flutter/lib/main.dart shall be removed.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
chanora_bridge::voice,chanora_audio::engine::AudioEngine,BridgeEvent::VoiceState,apps/chanora_flutter/lib/main.dart - Source SAD: SAD-081
- Verification method: Integration Test, UI Review
SDD-095: chanora_audio::TransmitMode shall be a #[repr(u8)] Rust enum with variants Ptt = 0, Continuous = 1, VoiceActivity = 2. Persistence shall be provided by chanora_storage::IdentityStore::set_transmit_mode(&self, mode: TransmitMode) and chanora_storage::IdentityStore::get_transmit_mode(&self) -> TransmitMode, the latter returning TransmitMode::Ptt on first read for a fresh install. The bridge surface shall carry the enum across the FFI boundary via mirror codegen as BridgeTransmitMode. The Flutter settings page in apps/chanora_flutter/lib/widgets/voice_settings.dart shall render three radio rows; the VoiceActivity row shall be rendered with enabled: false and a "Coming soon" trailing label until DEC-030 is resolved and an implementation is allocated.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
chanora_audio::TransmitMode,chanora_storage::IdentityStore::{set_transmit_mode, get_transmit_mode},BridgeTransmitMode,apps/chanora_flutter/lib/widgets/voice_settings.dart - Source SAD: SAD-083
- Verification method: Unit Test, UI Review
SDD-096: chanora_audio::release_tail::ReleaseTailTimer shall be a tokio-task-owning struct exposing arm(&self, gate: AudioTransmitGate, tail_ms: u32), key_down(&self), key_up(&self), and cancel(&self). Internally the struct shall hold a tokio::sync::watch::Sender<bool> plus a JoinHandle<()> for the pending close. key_down shall cancel any pending close by aborting the JoinHandle (cancel_pending()) and shall set transmit_active = true via the gate; key_up shall abort the prior close handle via JoinHandle::abort() and shall spawn a new task that sleeps for tail_ms milliseconds and then sets transmit_active = false. Cancellation is performed by aborting the pending JoinHandle; the watch channel enables cooperative early exit — the spawned task races cancel_rx.changed() against the tail sleep so that it can exit without writing to the gate when the sender is replaced or dropped. The struct shall use no unsafe. No std::sync::Mutex — interior mutability shall use std::sync::RwLock for brief edge-transition writes (arm, key_down, key_up, cancel); concurrent reads are uncontended. The watch handle shall be shared via atomic refcounts. The configuration value release_tail_ms shall live in the identity store next to transmit_mode, with a default of 200 and validated range 0–500 inclusive on every write.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
chanora_audio::release_tail::ReleaseTailTimer,chanora_storage::IdentityStore(release_tail_ms) - Source SAD: SAD-082
- Verification method: Unit Test, Integration Test
SDD-097: apps/chanora_flutter/lib/widgets/voice_bar.dart shall replace the existing _AudioControls widget in apps/chanora_flutter/lib/main.dart as the rendering surface for voice state. Its input shall be the BridgeEvent::VoiceState stream; its children, in render order, shall be: the channel-name pill (visible only when in_channel is true); the mode badge (PTT: <key_label> / Continuous / Voice activity (coming soon)); the hard-mute toggle button; the release-tail and threshold disclosure; the level meter; and the PTT capability badge already specified in SDD-091. The "Start audio" button shall be removed from the widget tree. The settings dialog in apps/chanora_flutter/lib/widgets/voice_settings.dart shall render the TransmitMode radio group, the bind-key button, and a release-tail slider over the range 0–500 ms with step 25 ms; the dialog shall never expose a "Start audio" affordance.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
apps/chanora_flutter/lib/widgets/voice_bar.dart,apps/chanora_flutter/lib/widgets/voice_settings.dart,apps/chanora_flutter/lib/main.dart - Source SAD: SAD-081, SAD-083
- Verification method: UI Review, Widget Test
SDD-098: IOSAudioSessionConfig shall be a Swift unit that configures AVAudioSession with category .playAndRecord, mode .default, and options .defaultToSpeaker | .allowBluetoothHFP | .allowBluetoothA2DP. setCategory shall be called from didFinishLaunching in the app delegate; setActive shall be called from the UIApplication.didBecomeActiveNotification observer. The actual session state shall be read back after both calls and logged for verification.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
IOSAudioSessionConfig - Source SAD: SAD-061, SAD-064
- Verification method: Platform Test, Release Inspection
SDD-099: IOSPermissionRequester shall call AVAudioSession.sharedInstance().requestRecordPermission approximately 1 second after app launch. The permission result shall be logged. A denied permission shall surface as an audio engine failure at voice_join time with a user-safe message; the UI shall not crash or hang.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
IOSPermissionRequester - Source SAD: SAD-061
- Verification method: Platform Test, Demo
SDD-100: Swift shall observe AVAudioSession.routeChangeNotification. On route change where reason == .oldDeviceUnavailable or reason == .newDeviceAvailable, Swift shall call the Rust bridge method handleRouteChange(). The Rust side shall invoke the ios_voice_unit restart path, uninitialising then reinitialising per ios_voice_unit.rs comments §5. Audio shall continue after rebind.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
IOSRouteChangeHandler,ios_voice_unit - Source SAD: SAD-061, SAD-064
- Verification method: Platform Test TC-9.3
SDD-101: Swift shall observe AVAudioSession.interruptionNotification. On .began the audio shall pause via VPIO.stop(). On .ended with shouldResume == true the audio shall resume via VPIO.start(). A phone call shall yield the audio session; the end of the call shall resume audio without user action. The interruption state shall be communicated to Flutter via BridgeEvent.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
IOSInterruptionRecovery,BridgeEvent - Source SAD: SAD-061
- Verification method: Platform Test TC-9.1, TC-9.2
Design note (shouldResume == false): When iOS signals interruption end with
shouldResume == false, the VPIO unit remains paused and theBridgeEvent_InterruptionStatecarriesshouldResume: falseto Flutter. The Flutter UI surfaces a snackbar indicating the interruption. The user must manually rejoin the voice channel or the app must implement a "resume audio" action in a future release. This is a known limitation for P0.Design note (lifecycle interleaving): The current iOS audio lifecycle handlers (route change, interruption began/ended) are individually serialized by the session mutex and
_ios_voice_unitmutex, but do not track an explicit lifecycle state (e.g.,Running,Interrupted,Restarting). This means that a route change arriving during an active interruption will restart the VPIO unit even though the app should remain paused. A future release should introduce an explicit iOS audio lifecycle state model that gates restart/resume decisions accordingly. For P0, the risk is mitigated by iOS serializing these notifications on the main thread and the low probability of interleaving in practice.
SDD-102: Info.plist shall contain UIBackgroundModes = [audio]. The VPIO unit shall continue running when the app backgrounds. iOS shall show the red microphone indicator in the status bar while background audio is active. AVAudioSession shall remain active. On iOS lock screen, audio shall continue if the network remains available.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
Info.plist,VPIOUnit - Source SAD: SAD-061
- Verification method: Platform Test TC-8
SDD-103: iOS P0 shall use FocusedPttBackend only. There shall be no DesktopPttBackend ladder on iOS. PttCapabilityLevel shall always be L0Focused. The UI shall render an on-screen PTT button. A capability badge shall explain the iOS limitation. Users shall bind a key through the in-app dialog only when the app is in the foreground.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
FocusedPttBackend,PttCapabilityLevel,FocusedPttBindingDialog - Source SAD: SAD-061, SAD-071
- Verification method: Platform Test TC-3, TC-12
SDD-104: VoiceProcessingIO AudioUnit shall provide hardware AEC, AGC, and noise suppression. These shall always be engaged and shall not be user-disableable. The Flutter audio processing settings UI shall display these as hardware-enabled with switches disabled and a "Hardware-enabled" label.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
VoiceProcessingIO,AudioProcessingSettings(Flutter) - Source SAD: SAD-061, SAD-065
- Verification method: Platform Test, Audio Test
Android P0 Platform Detailed Design (SDD-105 through SDD-110)
SDD-105: AndroidJniBootstrap shall own the JNI initialisation surface for chanora_bridge on Android, covering native library load, JNI_OnLoad JavaVM* capture, and initChanoraContext(Context) invocation. This item back-fills the existing implementation in crates/chanora_bridge/src/android_init.rs and the Kotlin caller currently in MainActivity.kt; no implementation work is created by this item, only the design constraints under which the back-filled code shall stabilise.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
AndroidJniBootstrap(crates/chanora_bridge/src/android_init.rs,crates/chanora_bridge/build.rs,apps/chanora_flutter/android/app/src/main/kotlin/.../ChanoraApplication.kt) - Source SAD: SAD-081, SAD-086
- Verification method: Platform Test (Android), Inspection
- Back-fill: yes (existing code in
crates/chanora_bridge/src/android_init.rs,apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/ChanoraApplication.kt(System.loadLibrary), andapps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/MainActivity.kt(initChanoraContext invocation)) - Cross-trace: See SDD-118 item 6 (extended) for the runtime staging of
libc++_shared.sothat theSystem.loadLibrarypreload depends on; seecrates/chanora_bridge/build.rsfor the DT_NEEDED emission (cargo:rustc-link-lib=dylib=c++_shared) that makes the namespace-isolated Android dynamic linker resolve C++ runtime symbols correctly.
Implementation requirements:
- Loader location:
System.loadLibrary("chanora_bridge")shall be invoked from anandroid.app.Applicationsubclass (e.g.,ChanoraApplication.onCreate()), not fromMainActivity. Rationale:Application.onCreateruns before anyActivityis instantiated, guaranteeing the native library is available to early FRB calls, foreground-service callbacks (SDD-107), and any background-trigger paths. The manifest<application android:name=".ChanoraApplication">attribute shall be set. If a future refactor reintroducesActivity-only load, it shall be justified in an ADR. JNI_OnLoad(Rust side): on first load the function shall (a) captureJavaVM*into aOnceCell<JavaVM>for later use by audio-engine and foreground-service JNI calls, (b) initialise thendk_contextAndroidContextwith the capturedJavaVMand a nullContextplaceholder, and (c) returnJNI_VERSION_1_6. The function shall never panic; any error shall be logged viaandroid_logand shall returnJNI_ERR.initChanoraContext(context: Context)shall be called fromChanoraApplication.onCreate()exactly once, AFTERSystem.loadLibraryand BEFORE the first FRB call. The Kotlin side shall pass the applicationContext(not anActivitycontext) to avoid leaking activity references into the long-lived JNI singleton. The Rust function shall convert the local-refjobjectinto a global ref, store it in thendk_context::AndroidContext, and return on success; on failure it shall log and return without crashing.- Ordering guarantee: any caller of
chanora_bridge(FRB-generated bindings, foreground service via SDD-107, audio mode controller via SDD-108) shall be entitled to assume bothJNI_OnLoadandinitChanoraContexthave completed. The bridge shall expose a debug-only assertion that panics with a clear message if a bridge call observes an uninitialised context. - Threading:
initChanoraContextruns on the Android main thread; subsequent bridge calls may run on any thread. The capturedJavaVMis process-global and thread-safe; per-threadJNIEnvshall be acquired viaJavaVM::attach_current_threadand released on thread exit (deferred to standardjnicrate guards). - Error / panic surface: Rust panics inside JNI callbacks shall be caught at the FFI boundary (
catch_unwind) and converted to a logged error plus a returned error code or null; they shall never unwind into the JVM. A panic during bootstrap is treated as a fatal initialisation failure and shall be reported through the existing diagnostics layer. - Library name: the canonical library name shall be
chanora_bridge(resulting.soislibchanora_bridge.so). Any rename requires an ADR and a coordinated update to this SDD item.
SDD-106: AndroidPermissionRequester shall own the Android runtime acquisition of RECORD_AUDIO on behalf of the audio subsystem and shall expose the current permission state to the audio engine and to Flutter through the existing BridgeEvent stream. Listen-only operation (capture_active = false with the output stream open per SAD-081) is a first-class mode, not an error condition, and shall be selected automatically when the permission is not granted.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
AndroidPermissionRequester(Android platform permission adapter) - Source SAD: SAD-085
- Verification method: Platform Test (Android), Integration Test, UI Review
Implementation requirements:
- Request timing: the requester shall attempt to acquire
RECORD_AUDIOat or before the firstvoice_joinof the session. The trigger shall fire from thevoice_joinbridge handler before the audio engine opens the input stream, NOT at app launch. Rationale: per Android guidance, runtime permissions should be requested in-context. - Denial UX (first-time denial, not "do not ask again"): the audio engine shall clamp
capture_active = falseandTransmitModeSelector(per SAD-083 / SDD-095) shall clamptransmit_active = false. The output mix path defined in SAD-081 / SDD-094 shall remain open (listen-only). The Voice Bar shall surface a non-blocking affordance "Enable microphone" that re-invokes the requester. - Permanent denial path (
shouldShowRequestPermissionRationalereturns false AND permission is denied): the affordance shall switch to "Open settings" and shall launch anIntent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)deep-linked to the Chanora app entry. The requester shall NOT auto-retry beyond Android platform conventions; the user must explicitly re-grant. - Mid-session revocation: Android may revoke
RECORD_AUDIOwhile the app is running (system kills and restarts the process). On process resume the requester shall re-queryContextCompat.checkSelfPermissionbefore resuming capture; on revocation it shall behave identically to first-time denial — clampcapture_active = false, keep listen-only, surface the affordance. The active voice session shall not be torn down; only capture is suspended. - Bridge event surface: the requester shall publish a
BridgeEvent::PermissionState { permission: "RECORD_AUDIO", state: Granted | Denied | DeniedPermanent | Undetermined }whenever the resolved state changes. The Flutter UI shall subscribe to this stream alongsideBridgeEvent::VoiceState(SDD-094) and render the Voice Bar accordingly. - Interaction with
TransmitModeSelector(SAD-083 / SDD-095): the selector shall treatPermissionState != Grantedas a final clamp identical to hard-mute —transmit_active = falseregardless of PTT key state, transmit mode, or mute state. When the permission transitions toGranted, the selector shall resume normal evaluation on the next state tick; no re-bind of PTT is required. - Thread safety: the requester runs on the Android main thread for the request dialog and on the bridge isolate / audio thread for state queries. The cached permission state shall be held in an
AtomicU8mirroring the four-state enum. - Single-flight: concurrent
voice_joincalls (defensive — SRS-189 forbids it but the design must compose) shall coalesce into a single Android permission request; the second caller shall observe the result of the first. - Manifest declaration:
<uses-permission android:name="android.permission.RECORD_AUDIO" />(already required for capture) shall be present; see SDD-107 for the foreground-service permission set.
SDD-107: AndroidVoiceForegroundService shall own the Android foreground service lifecycle for active voice sessions. It shall be started by the audio engine on first voice_join and stopped on the last voice_leave or on engine shutdown_if_idle (per SDD-094), shall declare foreground service type microphone on API 30+, and shall coordinate with AndroidAudioModeController (SDD-108) and AndroidPermissionRequester (SDD-106) so capture and SCO routing survive UI backgrounding and process trimming.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
AndroidVoiceForegroundService(Android platform services container) - Source SAD: SAD-086
- Verification method: Platform Test (Android), Integration Test, Release Inspection
Implementation requirements:
- Service class:
ChanoraVoiceForegroundService extends android.app.Service, located in the Android app module underapps/chanora_flutter/android/app/src/main/kotlin/<pkg>/voice/. The class shall befinaland shall not export bindings beyond what is required by the bridge (onBindreturns null; the service is start-only). - Manifest entry:
The
<service android:name=".voice.ChanoraVoiceForegroundService" android:exported="false" android:foregroundServiceType="microphone" />foregroundServiceType="microphone"attribute is required by API 30+ (Android 11) and is read by the platform to gate background microphone access. On API 28–29 the attribute is ignored; capture is permitted without it. - Required manifest permissions:
android.permission.FOREGROUND_SERVICE(required on API 28+).android.permission.FOREGROUND_SERVICE_MICROPHONE(required on API 34+ for type=microphone).android.permission.POST_NOTIFICATIONS(required to display the notification on API 33+; runtime-requested per item 6 below).android.permission.RECORD_AUDIO(already declared per SDD-106).
- Notification channel: on first service start the service shall create a notification channel with:
id = "chanora.voice.session"name = "Voice session"(localised via product strings; not server content)importance = NotificationManager.IMPORTANCE_LOW(no sound; non-intrusive ongoing indicator)- description text: "Shown while a Chanora voice session is active." (localised)
The channel is created once via
NotificationManager.createNotificationChannel; repeated creates are no-ops per Android contract.
- Ongoing notification:
startForeground(notificationId = 0x4348414E /* "CHAN" */, notification)shall be called within 5 seconds ofService.onStartCommandto satisfy Android's foreground promotion deadline. The notification shall:- Use the channel from item 4.
- Carry a small icon (
ic_chanora_voice) and content title "Chanora — Voice session active". - Carry a content intent that re-opens the host
MainActivity(PendingIntent.FLAG_IMMUTABLEset). - NOT carry server-provided channel names, user names, or message content (per privacy posture; localised product strings only).
- Be marked
ongoing = trueandcategory = CATEGORY_CALLto encourage the system to keep it visible.
POST_NOTIFICATIONS(API 33+): the requester pattern in SDD-106 shall be reused. The permission shall be requested when the foreground service is first started in a session; denial shall NOT block the service (the service still runs without a visible notification; Android allows this but UX is degraded). The bridge shall publish aBridgeEvent::PermissionState { permission: "POST_NOTIFICATIONS", … }so the UI can prompt the user to grant it.- Lifecycle:
- Start: from the
voice_joinhandler (afterAndroidPermissionRequesterresolves RECORD_AUDIO state — listen-only IS allowed to start the service so the user can hear audio while backgrounded), callContextCompat.startForegroundService(context, intent). - Stop: from the
voice_leavehandler when no voice sessions remain, AND from the engineshutdown_if_idle()path (per SDD-094), callservice.stopForeground(STOP_FOREGROUND_REMOVE)followed byservice.stopSelf(). The stop shall be idempotent. - On process death: Android may kill the service. The service shall return
START_NOT_STICKYfromonStartCommandso it is not auto-restarted; the nextvoice_joinre-starts it explicitly. State is rebuilt from the audio engine, not from the service. - On user removing the notification: API 34+ allows dismissal of ongoing FGS notifications. Removal shall NOT stop the voice session (only the user explicitly leaving a channel via the UI does that). The service shall re-post the notification on next state tick if still active.
- Start: from the
- Coordination:
- With
AndroidAudioModeController(SDD-108): the service start happens BEFOREAudioManager.setMode(MODE_IN_COMMUNICATION); the mode controller engages after the service is foreground. On stop, the mode controller restores prior mode BEFORE the service stops. - With
AndroidPermissionRequester(SDD-106): the service may start in listen-only mode (RECORD_AUDIO not granted); the foreground-service-type=microphone declaration is still required so that capture can resume immediately when the user grants the permission, without restarting the service.
- With
- Thread: service lifecycle methods run on the main thread; the service shall not perform audio work itself — the Rust audio engine owns the streams, the service exists purely to keep the process foregrounded.
- Bridge call path:
voice_join/voice_leaveare bridge calls; their Rust handlers shall invoke a Kotlin static methodChanoraVoiceForegroundService.start(Context)/.stop(Context)via JNI (using the capturedJavaVMfrom SDD-105) to avoid coupling the service to a Dart MethodChannel hop.
SDD-108: AndroidAudioModeController shall own engagement and release of Android's in-call audio mode for the voice subsystem. It shall snapshot the prior AudioManager.getMode() on first engage and restore it on last release, shall be reference-counted across composed sessions (P0 = single session per SRS-189 but design must compose for P1), and shall be the only writer of the Android audio mode within the application.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
AndroidAudioModeController(Android platform audio adapter) - Source SAD: SAD-084
- Verification method: Platform Test (Android), Unit Test, Audio Test
- Back-fill: yes (back-fills the v0.9.8 product-decision-register entry for Android in-call audio mode)
Implementation requirements:
- Engagement API (Rust-facing):
AndroidAudioModeControlleris a Kotlin singleton with a JNI surface:fn engage() -> Result<(), AudioModeError>: increments the internal reference count; on the 0 → 1 transition it shall snapshot the current mode (prior_mode = audioManager.mode) and callaudioManager.setMode(MODE_IN_COMMUNICATION). On counts ≥ 1, it is a no-op success.fn release(): decrements the reference count; on the 1 → 0 transition it shall callaudioManager.setMode(prior_mode)and clearprior_mode. On counts ≥ 1 after decrement, it is a no-op. On underflow (release without engage) it shall log and clamp at 0 without crashing.fn is_engaged() -> bool: reads the reference count (debugging / diagnostics only).
- Reference-count semantics: the count is an
AtomicI32guarded by a KotlinMutexfor the snapshot/restore critical section. P0 only ever observes counts in{0, 1}per SRS-189, but the design must compose for future multi-session use. Any two-engages-one-release sequence shall keepprior_modefrom the FIRST engage; a release at count 1 restores that original mode. - Lifecycle binding:
engage()shall be called from the audio engine'sensure_running()path (per SDD-094) AFTERAndroidVoiceForegroundService.startsucceeds.release()shall be called fromshutdown_if_idle()BEFOREAndroidVoiceForegroundService.stop. The controller is bound to the voice-session lifecycle, NOT to any UI screen lifecycle. - JNI surface from Rust audio engine: the Rust audio engine shall hold a
OnceCell<GlobalRef>for the Kotlin singleton, populated duringAndroidJniBootstrap(SDD-105). The Rust-side helpersandroid_audio_mode_engage()/android_audio_mode_release()shall acquireJNIEnvviaJavaVM::attach_current_thread, invoke the static methods, and translate any thrownExceptionintoAudioModeError. - Error paths:
AudioManagerunavailable (context.getSystemService(AUDIO_SERVICE)returns null — exceedingly rare):engage()shall returnAudioModeError::ManagerUnavailable. The audio engine shall proceed in fallback path (media-mode capture) and shall emit a diagnostic; voice quality may degrade but the session shall not abort.setModethrowsSecurityExceptionorIllegalStateException: log, returnAudioModeError::SetModeFailed, leaveprior_mode = null, do NOT increment the reference count. Subsequent engages may retry.- Restore failure on
release(): log; do not retry; do not throw across the JNI boundary.
- Exclusivity: no other code path in the Android app — Flutter, bridge surface, feature screens — shall call
AudioManager.setModedirectly. A CI lint rule shall flag any directsetModecall outside this controller. - Thread: all
audioManager.setModecalls run on the calling JNI thread; AndroidAudioManageris thread-safe forsetMode. - Diagnostics: each engage/release transition shall emit a structured event
{ event: "android.audio_mode.transition", prior: <int>, new: <int>, refcount: <int> }. No user content; safe under the SAD-077 / SDD-090 sanitiser.
SDD-109: AndroidAaBuildPipeline shall define the Android App Bundle (.aab) packaging pipeline used for release builds, covering bundletool configuration, ABI splits, signing assertions in CI, and version-code / version-name derivation.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P1 / Beta
- Software unit:
AndroidAaBuildPipeline(CI scripts + Gradle release configuration) - Source SAD: SAD-037, SAD-063
- Verification method: Build Test, Release Inspection
Implementation requirements:
- Output artefact:
bundleReleaseGradle task producesapp-release.aabatapps/chanora_flutter/build/app/outputs/bundle/release/app-release.aab. This artefact is the only Play Console upload candidate. - Bundle config (
bundle { … }inbuild.gradle.kts):language { enableSplit = true }— per-language asset delivery.density { enableSplit = true }— per-density resource delivery.abi { enableSplit = true }— per-ABI native-lib delivery; combined with the SDD-073ndk.abiFiltersset this yields three native splits (arm64-v8a,armeabi-v7a,x86_64).
- bundletool integration: CI shall invoke
bundletool build-apks --bundle=app-release.aab --mode=universalonce per pipeline to produce a universal APK for smoke testing on a known device; this universal APK is not shipped to users. - Signing assertions (CI step
assert-android-release-signing):- Fail if
signingConfig.storeFileis unset or resolves to a debug keystore (matches SDD-073 item 5). - Fail if
jarsigner -verify -verbose -certs app-release.aabreports the certificate fingerprint does not match the expected production cert pinned in the CI secret store. - Fail if
bundletool validate --bundle=app-release.aabreports any error.
- Fail if
- Version code derivation:
versionCodeshall be derived from the build number, computed as(MAJOR * 10_000_000) + (MINOR * 100_000) + (PATCH * 1_000) + BUILD_COUNTER, where MAJOR/MINOR/PATCH come from the workspaceversionfield and BUILD_COUNTER is the CI-monotonic counter. The derivation shall be implemented in a Gradle helper so local builds can reproduce the value given the inputs.versionCodeis a 32-bit signed int; the scheme yields headroom through MAJOR=99. - Version name derivation:
versionName = "${MAJOR}.${MINOR}.${PATCH}"for release builds;"${MAJOR}.${MINOR}.${PATCH}-rc${BUILD_COUNTER}"for release-candidate builds;"${MAJOR}.${MINOR}.${PATCH}-dev"for local builds. - ABI split verification: release inspection shall extract the AAB manifest and assert exactly the three abiFilters from SDD-073 appear in the
<config>element, and no other ABI directories are present inBUNDLE-METADATA/native.pb. - Reproducibility: the release pipeline shall record the toolchain identifiers (AGP version, Kotlin version, NDK version, Flutter version, Rust toolchain) into a
build-manifest.jsonattached to the release artefact.
SDD-110: AndroidPttCapability shall pin Android P0 to FocusedPttBackend only. There shall be no DesktopPttBackend ladder on Android, no system-wide global key hook, and no global accessibility-service-based PTT. PttCapabilityLevel shall always be L0Focused on Android, and the capability badge (per SDD-091) shall render the Android-specific explanation text.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
FocusedPttBackend,PttCapabilityLevel,PttCapabilityBadge - Source SAD: SAD-071 (PTT-backend isolation behind trait + factory-selected implementation + published capability level — the architectural source of the "Android pins to
FocusedPttBackend/L0Focused" decision), SAD-076 (BridgeEvent::PttCapability { level, backend_id }— the capability-event surface feeding the Android capability badge per SDD-091) - Verification method: Platform Test (Android), UI Review
Implementation requirements:
- Backend selection: on Android,
PttController(per SDD-088) shall instantiateFocusedPttBackendonly. The Windows / macOS / Linux backends (SDD-083 through SDD-086) shall not be compiled into the Android target; build configuration shall gate them behind#[cfg(not(target_os = "android"))]. - Capability report:
current_capability()shall returnPttCapabilityLevel::L0Focusedunconditionally on Android.backend_idshall return"focused". - UI rendering:
PttCapabilityBadge(SDD-091) on Android shall display the short label "PTT (in-app)" and, when the user taps the info icon, shall present an explanation sheet with the localised text: "Push-to-talk on Android works while Chanora is in the foreground. To keep transmitting while another app is on screen, use the on-screen PTT button in the notification or switch to Continuous mode." (Product strings; not server content.) - On-screen PTT control: the Voice Bar shall expose a touchscreen PTT button when transmit mode is
Ptt, sized for thumb reach. The button shall drive theAudioTransmitGate(per SDD-089) via the bridgeset_ptt(true|false)call. Release-tail (SDD-096) applies identically. - No accessibility-service request: Chanora shall NOT declare or request
BIND_ACCESSIBILITY_SERVICEon Android. Any future P1+ proposal to enable global PTT via the accessibility framework shall require an ADR and explicit user-consent UX, and shall NOT default-on. - No global media-button capture for PTT: media-button intents (
MediaSessionCompat) are reserved for transport controls; they shall not be repurposed as a PTT input on Android in P0. - Capability badge cross-platform consistency: the badge widget itself is shared per SDD-091; only the per-platform explanation text differs. Android explanation text is owned by
LocalizationService(SDD-031) under the keyptt.capability.android.l0.
Android Voice Audio Backend Detailed Design (SDD-111 through SDD-116)
SDD-111: AndroidVoiceAudioBackend shall be the Android-side implementation of a new platform-abstracted voice-audio capture+playback element. The cross-platform abstraction shall be expressed as a Rust trait — provisionally MobileVoiceAudioBackend — that owns the full lifetime of a paired input/output low-latency voice stream pair (open, start, stop, close, error/disconnect signalling). The Android implementation shall live in a new module within crates/chanora_audio (module name android_voice_unit; the exact filename is an implementation detail), and shall be selected by #[cfg(target_os = "android")] from the audio engine. The iOS module ios_voice_unit (per SDD-104) shall be the trait's iOS implementation; back-filling the existing ios_voice_unit.rs to conform to MobileVoiceAudioBackend is out of scope of this SDD and is deferred to a separate follow-up unit (SDD-117 candidate). Desktop platforms (Windows / macOS / Linux) do not implement MobileVoiceAudioBackend; cpal continues to own capture and playback on desktop per the existing audio engine design.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
MobileVoiceAudioBackend(trait, inchanora_audio),android_voice_unit(module, inchanora_audio),AudioEngine(existing; selects backend bycfg) - Source SAD: SAD-077 (PTT/voice audio component), SAD-081 (audio engine lifecycle)
- Verification method: Unit Test (Rust), Platform Test (Android), Build Verification (cross-platform
cargo buildfor android / ios / desktop targets)
Implementation requirements:
- Trait surface (Rust):
MobileVoiceAudioBackendshall expose, at minimum:open(config: MobileVoiceStreamConfig) -> Result<Self, BackendError>(whereMobileVoiceStreamConfigis the structure defined by SDD-112),start(&mut self) -> Result<(), BackendError>,stop(&mut self) -> Result<(), BackendError>,close(self) -> Result<(), BackendError>,session_id(&self) -> Option<i32>(for SDD-113 hardware-effect binding),achieved_performance_mode(&self) -> AchievedPerformanceMode,achieved_sharing_mode(&self) -> AchievedSharingMode, and an error/disconnect callback surfaced through a bounded channel or watch handle that the audio engine polls. The trait shall be object-safe-or-not at implementation discretion; the engine shall instantiate it as a concrete generic, not viadyn. - Android crate selection (primary): the Android implementation shall be built on
oboe-rs(version pin:0.6.xfamily;0.6.1is the working pin at SDD authoring time). Rationale:oboe-rsis a thin Rust wrapper around Google's Oboe C++ library, which is the platform-recommended path for low-latency voice audio on Android (selects AAudio on API 27+ and falls back to OpenSL ES below that, though Chanora'sminSdk = 28per SDD-073 means the OpenSL ES path is not in scope). - Android crate fallback (contingency, NOT to be selected unless required): if
oboe-sys's C++ stdlib pull (libc++_shared.so) conflicts with the existingcargo-ndk+flutter_rust_bridgebuild chain in ways that cannot be resolved by standardlibc++_sharedpackaging, the contingency shall be a directndk::audio(AAudio) binding. The contingency shall NOT be exercised based on developer preference; it shall be exercised only on a documented build-system failure reported by codex-builder. The traitMobileVoiceAudioBackendis unchanged by this contingency — only the implementation behind it differs. - Engine selection:
AudioEngineshall, on Android, instantiateandroid_voice_unit::AndroidVoiceUnit(theMobileVoiceAudioBackendimpl) in place of the existingcpalcapture+playback pair. Thecpalcode paths shall remain compiled in for desktop targets but shall not be reachable on Android. - Mobile-vs-desktop selection sites: the audio engine shall have exactly one
cfgdecision site (a typed alias such astype PlatformVoice = <android_voice_unit::AndroidVoiceUnit | ios_voice_unit::IosVoiceUnit | DesktopCpalPair>). The engine's session lifecycle code (per SAD-081) shall be written against the trait, not against any concrete backend. - Error surface:
BackendErrorshall be achanora_audio-local error enum carrying at least:OpenFailed { reason: String },StartFailed { reason: String },ErrorDisconnected,InvalidConfig { reason: String }. The engine shall mapErrorDisconnectedto its existing retry/reopen path per SDD-094ensure_running; the backend itself shall NOT loop on retries (single-responsibility: the engine owns retries). - Tracing / diagnostics: each lifecycle transition (
open,start,stop,close,ErrorDisconnected) shall emit atracingevent under thechanora_audiotarget with the achieved performance mode, achieved sharing mode, and (where available)session_id. These fields feed SRS-210 verification analysis.
Notes:
- SAD anchor caveat: SAD-077 (the PTT/voice audio component) and SAD-081 (the audio engine lifecycle) are the closest existing SAD anchors. There is no dedicated SAD item that names a platform-abstracted "mobile voice audio backend" element by itself; this SDD is sourced from those two anchors. A SAD revision that adds a dedicated allocation item for the mobile-voice-audio-backend trait should be considered as a follow-up so that
Source SADfor SDD-111 can be tightened beyond the SAD-077 / SAD-081 pairing.
SDD-112: AndroidVoiceStreamConfig shall be the immutable, validated configuration passed by the audio engine into android_voice_unit::AndroidVoiceUnit::open() (per SDD-111). It shall encode the AAudio / Oboe stream-construction parameters that SRS-210, SRS-211, SRS-213, and SRS-214 obligate the client to request, and shall expose the corresponding "achieved" values after the stream is opened so that the engine and diagnostics can record what the platform actually granted.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
MobileVoiceStreamConfig(struct),AchievedPerformanceMode(enum),AchievedSharingMode(enum),AchievedInputPreset(enum),AndroidVoiceUnit::open(consumer) - Source SAD: SAD-077, SAD-081
- Verification method: Unit Test (Rust, config-validation paths), Platform Test (Android, achieved-value recording)
Implementation requirements:
- Sample rate: the configuration shall pin the requested sample rate to
48_000Hz, matching the existingchanora_audioSAMPLE_RATEengine constant. The Android implementation shall request 48 kHz on both input and output streams; if the platform refuses 48 kHz and offers a different native rate, the implementation shall accept the native rate and the backend shall surface it viaachieved_sample_rate(). Resampling, if required, shall be performed by the existing cross-platform audio engine path, not inside the backend. - Channel layout: both input and output streams shall be opened as mono (1 channel), consistent with the existing Opus 20 ms framing in the cross-platform engine. Stereo is out of scope for P0 voice.
- Frame size / burst: the implementation shall NOT hard-code a frame size. After stream open, the implementation shall query the stream's
framesPerBurst(OboeAudioStream::getFramesPerBurst()/ AAudioAAudioStream_getFramesPerBurst) and shall set the audio callback's processing block size to a multiple of that burst that also satisfies the engine's 20 ms framing (the engine's existing Opus-frame buffering shall absorb the mismatch). - Performance mode: the requested performance mode shall be
LowLatency(AAUDIO_PERFORMANCE_MODE_LOW_LATENCY) on both input and output. After open, the implementation shall recordgetPerformanceMode()and expose it asAchievedPerformanceMode::{LowLatency, None, PowerSaving}. Per SRS-210, the achieved mode determines which mouth-to-ear latency bound (≤150 ms vs ≤250 ms) applies; the backend itself does not enforce the bound but exposes the data SRS-210 verification requires. - Output usage / content type (SRS-213): the output stream shall be opened with usage =
VoiceCommunication(AAUDIO_USAGE_VOICE_COMMUNICATION) and content type =Speech(AAUDIO_CONTENT_TYPE_SPEECH). These shall NOT be configurable from the engine for P0; they are fixed by SRS-213. - Input preset (SRS-211): the input stream shall be opened with input preset =
VoiceCommunication(AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION). If the open call fails specifically due to preset rejection, the implementation shall retry once withVoicePerformance(AAUDIO_INPUT_PRESET_VOICE_PERFORMANCE). If that also fails for preset reasons, the implementation shall retry withGeneric(AAUDIO_INPUT_PRESET_GENERIC) and log awarn-level diagnostic. The achieved preset shall be recorded viaAchievedInputPresetand exposed to diagnostics. Failures unrelated to preset (e.g., device busy) shall bubble up asBackendError::OpenFailedwithout preset retries. - Sharing mode (SRS-214): both input and output streams shall request
Exclusive(AAUDIO_SHARING_MODE_EXCLUSIVE) on a best-effort basis. If the platform returnsShared(or refuses Exclusive open with a sharing-mode-specific error), the implementation shall retry once withSharedand record the achieved sharing mode viaAchievedSharingMode::{Exclusive, Shared}. The fall-back to Shared shall NOT be reported to the user as a failure (per SRS-214) and shall not prevent voice session establishment. - Error / disconnect callback: the implementation shall register an Oboe
AudioStreamErrorCallback(or AAudioAAudioStream_setErrorCallbackequivalent on the contingency path). OnErrorDisconnected(e.g., headset unplugged, USB audio interface removed, route invalidated), the callback shall NOT attempt to reopen the stream from inside the callback (Oboe documents this as forbidden). It shall instead enqueue aBackendError::ErrorDisconnectedonto the engine-visible channel; the audio engine's state machine (per SDD-094 / SAD-081) shall own the reopen retry. - Validation:
MobileVoiceStreamConfig::new(...)shall returnErr(BackendError::InvalidConfig { reason })for: sample rate outside{8_000, 16_000, 24_000, 32_000, 44_100, 48_000}, channel count not equal to 1, or any field deliberately outside the documented enums above. The default constructor used by the audio engine shall always produce a valid config; the validator exists for defence-in-depth and for unit testing. - Diagnostics record: after a successful pair of opens, the backend shall emit a single
tracingevent with all achieved values (sample rate, frames-per-burst, performance mode, sharing mode, input preset) so that SRS-210 / SRS-211 / SRS-213 / SRS-214 verification can be performed from logs.
SDD-113: AndroidHardwareAudioEffects shall be the element responsible for engaging the Android platform hardware voice-audio effects — AcousticEchoCanceler, NoiseSuppressor, AutomaticGainControl — on the active microphone capture session, and for triggering the cross-platform software fallback when one or more effects are unavailable on the device. It shall close the prior silent no-op gap explicitly called out in SRS-212.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
AndroidHardwareAudioEffects(Rust+JNI shim), Java/Kotlinandroid.media.audiofx.AcousticEchoCanceler/NoiseSuppressor/AutomaticGainControlconsumers,AudioEngine(consumer) - Source SAD: SAD-077, SAD-084 (composes with
AndroidAudioModeControllerper SDD-108) - Verification method: Platform Test (Android, device-matrix from SDD-116), Unit Test (Rust, fallback-path branch coverage)
Implementation requirements:
- Session id acquisition: after
android_voice_unit::AndroidVoiceUnit::open()(per SDD-111) opens the input stream, the implementation shall calloboe::AudioStream::getSessionId()(or the AAudio equivalentAAudioStream_getSessionIdon the contingency path) to obtain the platform audio session id of the input stream. The input stream MUST be opened with a session id that allows effect attachment (OboesetSessionId(SessionId::Allocate)or AAudioAAUDIO_SESSION_ID_ALLOCATE); SDD-112 item 6 is amended by this SDD to also require session-id allocation on the input stream. - Effect attachment: with a valid session id, the implementation shall, via JNI (using the captured
JavaVMfrom SDD-105), construct each of the three Java effect classes —AcousticEchoCanceler,NoiseSuppressor,AutomaticGainControl— using the staticcreate(int audioSession)factory, and shall callsetEnabled(true)on each successfully constructed effect. The JavaGlobalRefs shall be retained for the lifetime of the input stream and shall be released viarelease()on stream close. - Availability probing: before construction, the implementation shall call the static
isAvailable()method on each effect class. Effects reported as unavailable shall NOT be constructed; the implementation shall proceed to fallback handling for that specific effect. - Construction-failure handling: if
isAvailable()returns true butcreate()throws or returns null, the implementation shall log awarn-level diagnostic that includes the effect name and the exception class (sanitised — no PII), and shall fall back for that effect. - Software fallback: where one or more hardware effects are unavailable or fail to construct, the implementation shall signal the cross-platform engine to engage the existing software AEC / NS / AGC path that the engine already exposes via
AudioEngineConfig.effects.aec/.noise_suppression. The engine's Android#[cfg]branch atcrates/chanora_audio/src/engine.rs:316-323shall no longer emit the prior "engagement depends on device AEC/NS support under MODE_IN_COMMUNICATION" log line as a no-op rationale; the rationale is superseded by this SDD, and the branch shall actually engage either hardware (via this SDD) or software (via the existing engine path). The codex-builder shall remove the no-op rationale comment as part of SDD-111..SDD-115 implementation. - Composition with SDD-108: hardware-effect engagement requires
AudioManager.setMode(MODE_IN_COMMUNICATION)to be active for the platform voice-effect routing to engage correctly. The lifecycle order (per SDD-115) shall therefore be: SDD-108 mode engage → SDD-111 stream open → SDD-113 effect attach → start. Effect attach on a stream opened beforeMODE_IN_COMMUNICATIONis engaged is not specified by Android and shall be treated as undefined behaviour to be avoided by sequencing, not by retry. See SDD-115 step ordering 4–6 for the canonical implementation sequence (mode engage → streams open → effect attach); this SDD unit (SDD-113 item 6) and SDD-115 are the joint authority on the ordering, and any future amendment to the ordering shall update both units in the same revision. - Diagnostics: the implementation shall emit a
tracingevent with three boolean fields —aec_engaged_hardware,ns_engaged_hardware,agc_engaged_hardware— and three corresponding*_engaged_softwarebooleans for the fallback path. SRS-212 verification reads these fields from sanitised logs / diagnostics. - Failure isolation: failure to engage any single effect shall NOT fail the voice session. Effect engagement is best-effort with software fallback; the only fatal failure path is stream open itself.
SDD-114: AndroidVoiceManifestRequirements shall enumerate, at the SDD layer, the Android manifest declarations required for the voice audio backend (SDD-111..SDD-113) and the foreground-service hosting (SDD-107). This unit is a back-fill of declarations already landed by Wave 2B-1; it records the design contract so that future manifest edits do not silently regress the voice subsystem.
- Status: Draft (back-fill — implementation precedes documentation; see notes)
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
apps/chanora_flutter/android/app/src/main/AndroidManifest.xml - Source SAD: SAD-085 (permission adapter), SAD-086 (foreground service)
- Verification method: Manifest Inspection (CI), Platform Test (Android)
Implementation requirements:
- Required
<uses-permission>declarations (canonical set for voice audio + foreground service):android.permission.INTERNET— required by the network stack (already declared at the workspace baseline).android.permission.RECORD_AUDIO— required for microphone capture (per SDD-106).android.permission.FOREGROUND_SERVICE— required to start any foreground service.android.permission.FOREGROUND_SERVICE_MICROPHONE— required by API 34+ forforegroundServiceType="microphone"(per SDD-107).android.permission.POST_NOTIFICATIONS— required for the ongoing foreground-service notification on API 33+; this permission is declared in the manifest and is requested at runtime per SDD-106's runtime-request pattern (SDD-107 item 6).android.permission.MODIFY_AUDIO_SETTINGS— required forAudioManager.setMode(MODE_IN_COMMUNICATION)per SDD-108.android.permission.BLUETOOTH_CONNECT— required on API 31+ for Bluetooth SCO routing; declared withandroid:usesPermissionFlags="neverForLocation"if required by future Play Store policy review.
- Required
<service>declaration: the Android manifest shall declare exactly one<service>element for the voice foreground service, with attributes:android:name="app.chanora.chanora_flutter.AndroidVoiceForegroundService"android:exported="false"android:foregroundServiceType="microphone"The fully-qualified class name shall match the Kotlin class introduced by SDD-107; if the application package id changes, the manifest entry and the SDD-107 JNI call site shall be updated together.
- SDK-gated declarations: SDK-version-conditional declarations (
<uses-permission android:name="…" android:maxSdkVersion="…" />/tools:targetApi) are NOT required for the canonical set in item 1 under the SDD-073minSdk = 28baseline. The set above is the minimum set required across the SDD-073 SDK range and shall be present unconditionally. - CI manifest assertion: a CI manifest-inspection step shall fail the build if any of the permissions or the
<service>entry in item 1 / item 2 is missing, or ifforegroundServiceTypeon theAndroidVoiceForegroundServiceentry is anything other thanmicrophone(or a strict superset thereof, e.g.,microphone|connectedDeviceif a future feature legitimately requires it; the CI rule shall use a contains-check, not equality). - No additional permissions: the manifest shall NOT declare
BIND_ACCESSIBILITY_SERVICE,SYSTEM_ALERT_WINDOW, or any media-button-related capture permissions for the voice subsystem (consistent with SDD-110 item 5 / item 6).
Notes:
- Documentation-after-code situation: the manifest entries described in items 1 and 2 were implemented in Wave 2B-1 (the Android voice foreground service landing wave) before this SDD unit was authored. This SDD unit back-fills the design contract; no new manifest work is created by this unit. Future regressions against item 1 / item 2 shall be treated as SDD-114 violations and shall be caught by the CI assertion in item 4.
SDD-115: AndroidVoiceLifecycleSequencing shall fix the sequencing of voice-session lifecycle operations across the elements introduced in SDD-107 (foreground service), SDD-108 (audio mode controller), SDD-111 (voice audio backend), and SDD-113 (hardware audio effects). It shall also specify the responses to Android-level audio events that can interrupt or reshape an active voice session.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
AudioEngine(lifecycle orchestrator),AndroidVoiceForegroundService(SDD-107),AndroidAudioModeController(SDD-108),AndroidVoiceUnit(SDD-111),AndroidHardwareAudioEffects(SDD-113), JavaAudioManager.OnAudioFocusChangeListener - Source SAD: SAD-081 (engine lifecycle), SAD-084 (mode controller), SAD-086 (foreground service)
- Verification method: Integration Test (Rust + Android), Platform Test (Android, device-matrix from SDD-116)
Implementation requirements:
-
Voice-join sequencing (forward order, on first
voice_join):- Dart calls the bridge
voice_join(channel_id)per SDD-094. - The Rust bridge handler invokes the JNI helper for SDD-107 to start
AndroidVoiceForegroundService(Kotlin static method invocation via the capturedJavaVM). Service.onStartCommandcallsstartForeground(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE)within Android's 5-second deadline (SDD-107).- Rust calls SDD-108 to engage
AudioManager.setMode(MODE_IN_COMMUNICATION)(snapshot prior mode, write new mode). Mode engagement precedes stream open so that AAudio's stream construction observes the in-call audio mode and routes to the voice-communication path from the outset. - Rust opens the AAudio input and output streams via SDD-111 (
AndroidVoiceUnit::open(MobileVoiceStreamConfig)), with the input stream opened withSessionId::Allocate(SDD-113 item 1 amends SDD-112). - Rust binds SDD-113 hardware effects against the input stream's session id (with software-AEC fallback on per-effect failure).
- Rust calls
AndroidVoiceUnit::start()on both streams. Active voice session.
Mode-first ordering note: the AudioManager mode is engaged BEFORE AAudio streams open so that AAudio's stream configuration sees the in-call audio mode at construction time, per SDD-113 item 6. Earlier drafts of this step list had stream open before mode engage (the order was swapped in SDD v0.9.14 — see change history); that ordering was unsafe because some devices route streams to the wrong destination if the mode is set after stream construction, and AEC/NS/AGC effect attachment on a stream constructed under the prior (non-voice) mode is not specified by Android.
- Dart calls the bridge
-
Voice-leave sequencing (reverse order, on last
voice_leaveorshutdown_if_idle):- Rust calls
AndroidVoiceUnit::stop()on both streams. - Rust releases SDD-113 hardware effects (
release()on eachGlobalRef). - Rust calls
AndroidVoiceUnit::close()on both streams. - Rust calls SDD-108 to restore the prior audio mode. Mode restore follows stream close (mirror of the forward order's mode-first rule): the streams are torn down under
MODE_IN_COMMUNICATIONbefore the mode is reverted, so no live stream observes a mid-flight mode change. - Rust invokes the JNI helper for SDD-107 to call
Service.stopForegroundandService.stopSelf. The reverse order is mandatory; releasing effects after mode restore, or closing streams after mode restore, is undefined and shall be avoided.
- Rust calls
-
Justification for sequencing: step 4 (mode engage) before step 5 (stream open) before step 6 (effect attach) is required because Android platform voice-effect routing only engages correctly under
MODE_IN_COMMUNICATION(SDD-113 item 6), and AAudio stream construction binds routing decisions based on the audio mode observed at open time. Steps 2/3 (service start before stream open) is required to ensure the foreground-service-type=microphone hosting is in place before any background-eligible capture occurs (SRS-215). -
Route-change callback handling: the Oboe
AudioStreamDataCallbackdoes not surface route changes directly; the implementation shall additionally observeandroid.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY(BroadcastReceiver) and, where available on API 30+,AudioDeviceCallbackfor device add/remove. On route change, the implementation shall:- Log the new route at
infolevel (sanitised — device type only, no serial numbers). - Re-evaluate exclusive-vs-shared mode on the next reopen (no in-place sharing-mode change; an actual change requires close + open).
- Not tear down the session for transient routes (e.g., USB headset hot-plug during call) — the engine's
ErrorDisconnectedretry path (SDD-094) handles stream invalidation if it occurs.
- Log the new route at
-
Audio focus loss handling: the implementation shall request audio focus via
AudioManager.requestAudioFocus(AudioFocusRequest.Builder(USAGE_VOICE_COMMUNICATION).setAcceptsDelayedFocusGain(false).build())at voice-join (step 4, alongside the SDD-108 mode engage) and shall register anOnAudioFocusChangeListener. Listener responses:AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: continue capture and playback at full gain (voice call is privileged content; ducking on the voice stream is not appropriate). No state change.AUDIOFOCUS_LOSS_TRANSIENT: pause capture (settransmit_active = falsevia the existingTransmitModeSelectorclamp) and pause playback. On subsequentAUDIOFOCUS_GAIN, resume both.AUDIOFOCUS_LOSS(permanent): tear down the voice session —AndroidVoiceUnit::stop()andclose(), release effects, restore mode, stop service. EmitBridgeEvent::VoiceState { in_channel: false, ... }so the Flutter UI surfaces "left channel — interrupted by another app." Manual rejoin is required (no auto-rejoin).
-
Bluetooth SCO connection-state-change handling (P0 stance): the implementation shall register a
BluetoothHeadsetconnection-state-changeBroadcastReceiverand shall NOT crash on connect/disconnect events. P0 acceptance is "no crash on connection-state-change during an active voice session." Full SCO route correctness — i.e., guaranteeing that capture and playback routes follow the headset across connect/disconnect during a session — is explicitly deferred to P1 and shall be tracked as a follow-up. The platformMODE_IN_COMMUNICATIONengagement (SDD-108) typically yields correct SCO routing on most devices; P0 relies on that platform behaviour. -
Concurrency assumption: all lifecycle transitions in items 1 and 2 are serialised on the audio engine's single lifecycle task (per SDD-094). The JNI helpers for SDD-107 and SDD-108 attach to the current thread on demand (per SDD-108 item 4). The Oboe callbacks (data callback, error callback) run on Oboe's audio thread; they shall NOT call back into the engine lifecycle directly — they shall enqueue events onto a bounded channel that the engine lifecycle task drains.
SDD-116: AndroidVoiceVerificationMatrix shall specify the minimum Android device matrix that the SDD-111..SDD-115 implementation must be exercised against before the implementation can be declared verified for the purposes of closing risk RISK-AUDIO-MOBILE-001 and the audio-quality gate G-A5. It shall enumerate the per-device observations that must be recorded so that SRS-210 (latency tier), SRS-211 (input preset), SRS-212 (hardware effects), SRS-213 (output usage), SRS-214 (sharing mode), and SRS-215 (foreground service longevity) verification cases can be evaluated from collected evidence.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit: Verification artefact contract for SWE.4 / SWE.5 / SWE.6 Android voice cases
- Source SAD: SAD-077, SAD-081, SAD-084, SAD-085, SAD-086
- Verification method: Aggregation — this SDD is itself a contract for verification engineers; the per-case verification is owned by SWE4-UV / SWE5-IV / SWE6-SV ranges cross-linked below.
Implementation requirements:
- Minimum API-level matrix: implementation evidence shall be collected on at least one device at each of the following Android API levels:
- API 28 (Android 9) — the SDD-073 / DEC-004 minimum baseline. Validates that AAudio low-latency mode and
foregroundServiceType="microphone"(which becomes mandatory only on API 30+) behave correctly when only the older permission-and-service model is enforced. - API 31 (Android 12) — Bluetooth permission changes (
BLUETOOTH_CONNECTruntime permission); validates SDD-114 item 1 Bluetooth-permission declaration. - API 34 (Android 14) —
FOREGROUND_SERVICE_MICROPHONEpermission andforegroundServiceType="microphone"enforcement; validates SDD-107 and SDD-114 together.
- API 28 (Android 9) — the SDD-073 / DEC-004 minimum baseline. Validates that AAudio low-latency mode and
- Minimum vendor diversity: the matrix shall include at least one Google Pixel device AND at least one non-Pixel device, with Samsung Galaxy preferred for the non-Pixel slot. Rationale: Samsung's pre-One UI 5 AAudio HAL has historical quirks (specifically, performance-mode downgrades and shared-mode coercions that Pixel devices do not exhibit) that have caused SRS-210-class regressions in comparable Android voice apps.
- Per-device observations recorded (each device, per voice-session run):
getPerformanceMode()return value for the input stream and the output stream (SDD-112 item 4 / SRS-210).getSharingMode()return value for the input stream and the output stream (SDD-112 item 7 / SRS-214).getInputPreset()(or the closest equivalent observable) and the preset attempt sequence (SDD-112 item 6 / SRS-211).AcousticEchoCanceler.isAvailable(),NoiseSuppressor.isAvailable(),AutomaticGainControl.isAvailable()return values, and whether each effect'screate()succeeded andsetEnabled(true)did not throw (SDD-113 / SRS-212).- Measured glass-to-glass (mouth-to-ear) latency in milliseconds, using a procedure to be documented by verification-engineer (procedure is out of scope of this SDD; SDD-116 only contracts that the measurement is recorded). SRS-210 is satisfied if the measurement is ≤150 ms on devices where
getPerformanceMode()returnedLowLatency, or ≤250 ms otherwise. - Foreground-service longevity: the voice session shall run continuously for ≥10 minutes with the device screen off and shall not be terminated by Android background-execution restrictions during that window (SRS-215, SWE6-SV-018 precedent).
- Evidence handling: per-device observations shall be captured by the existing sanitised audio-diagnostics path (per SDD-095 conventions) plus a verification-engineer-owned latency-measurement attachment. Device serial numbers and Bluetooth peer MAC addresses are NOT permitted in the evidence per SDD-077 / PII conventions.
- Closure rule for gate G-A5 / RISK-AUDIO-MOBILE-001: the gate may be closed when, for every device in the matrix (items 1 and 2), every observation in item 3 is recorded AND every SRS bound (SRS-210..SRS-215) is met on that device. A single device failing the SRS-210 latency bound shall block gate closure for the gate's affected platform tier; a device failing only the SRS-214 sharing-mode bound (i.e., achieving Shared rather than Exclusive) shall NOT block closure (graceful fallback is explicitly permitted by SRS-214).
- Verification ID cross-link: this SDD is the design-layer counterpart to the following verification ranges (cross-link only; verification IDs are owned by the verification documents):
- SWE.4 unit verification: SWE4-UV-040..046 (Android audio backend unit cases).
- SWE.5 integration verification: SWE5-IV-016..020 (Android audio engine + foreground service integration cases).
- SWE.6 software qualification: SWE6-SV-018..024 (Android voice session longevity, latency, effect engagement, sharing-mode fallback).
- SYS.4 system integration: cross-link to SYS4-SIV cases for Android voice end-to-end where applicable.
Chanora Bridge Build Automation Detailed Design (SDD-118, SDD-119)
ID allocation note: SDD-117 is reserved (per the SDD-111 trait-back-fill note) for a future ios_voice_unit adapter onto MobileVoiceAudioBackend and is intentionally left unauthored at this revision. The next two free IDs are therefore SDD-118 and SDD-119; this revision allocates both for the chanora_bridge Rust cdylib build-automation pair.
SDD-118: AndroidBridgeBuildAutomation shall specify how the chanora_bridge Rust cdylib is produced and staged for the Android build automatically, without manual cargo-ndk invocations and without environment mutation. This unit complements SDD-073 (Android build config), SDD-109 (AAB packaging pipeline), and SDD-105 (JNI bootstrap — the same libchanora_bridge.so is the one that System.loadLibrary resolves at process start). The Android pattern intentionally diverges from the always---release iOS / macOS automation pattern (SDD-119) on the iteration-time profile axis only; the release-build safety property (release Android build always uses release Rust) is preserved.
- Status: Draft
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit: Gradle task set in
apps/chanora_flutter/android/app/build.gradle.ktsdrivingcargo-ndkagainstcrates/chanora_bridge, and the resulting per-ABI.sostaging underapps/chanora_flutter/android/app/src/main/jniLibs/ - Source SAD: SAD-063 (Android runtime / build-configuration isolation)
- Cross-trace SDD: SDD-073 (Android build config — minSdk, ABI filters), SDD-105 (JNI bootstrap — consumer of the produced
.so), SDD-109 (AAB pipeline — downstream packaging consumer of the staged jniLibs) - Verification method: Build-system Test (Gradle task wiring, up-to-date semantics), Platform Test (Android — verify per-ABI
.sopresent in produced AAB / APK and loadable per SDD-105), Unit Test (preflight failure-mode messages)
Implementation requirements:
-
Gradle task registration: a Gradle task or task set shall be registered in
apps/chanora_flutter/android/app/build.gradle.ktsthat invokescargo-ndk(a single invocation; see item 13) to build thechanora_bridgecdylib for the configured ABI set. The task shall be a Kotlin DSL registration on the Android app module, not a standalone plugin and not an external script. -
Minimum API source of truth: the API level passed to
cargo ndk --platform $minSdkshall be sourced from thechanora.android.minSdkGradle property mandated by SDD-073 item 1, defaulting to28per DEC-004, overridable via-Pchanora.android.minSdk=…on the command line or viagradle.properties. The task shall NOT hard-code a numeric API level and shall NOT introduce a second source of truth. -
ABI set: the ABI set shall be
arm64-v8a,armeabi-v7a,x86_64. The list shall be derived at task-configuration time from the existingandroid.defaultConfig.ndk.abiFiltersdeclaration owned by SDD-073 item 4 so that the build-automation list and the AGP packaging list are guaranteed in sync. Nox86(32-bit) ABI shall be built — Android x86 32-bit is explicitly out of scope per the stakeholder decision recorded by this SDD. "Universal" on Android means an APK/AAB containing all three per-ABI.sofiles; Android does NOT lipo (this is the fundamental mechanism divergence from SDD-119 macOS). -
Profile mapping (divergence from SDD-119): the cargo profile shall be selected from the active Gradle build variant — Gradle
debugvariant invokescargo build(no--release), Gradlereleasevariant invokescargo build --release. This diverges from the SDD-119 iOS / macOS always---releasepattern. Rationale: Android developer-iteration UX (a clean debug build ofchanora_bridgein release profile adds a multi-minute wall-clock cost per iteration that iOS / macOS users do not pay because Xcode's debug build is similarly already release-bridged). Safety property preserved: a Gradlereleasebuild is observably guaranteed to ship a--releaseRust cdylib (item 10 release-inspection assertion enforces this). -
Environment variables on the
cargo-ndkinvocation: the task shall set, on the child-process environment only (no parent-shell mutation),LIBOPUS_STATIC=1,LIBOPUS_NO_PKG=1, andCMAKE_POLICY_VERSION_MINIMUM=3.5. Rationale: the Android NDK sysroot ships no libopus, so theaudiopus_syscrate must build libopus from source statically (mirrors SDD-119 macOS env).ANDROID_NDK_HOMEshall be propagated from the Flutter / AGP-resolved NDK location (flutter.ndkVersion/android.ndkVersionresolution) if and only if not already present in the process environment; the task shall NOT override an externally providedANDROID_NDK_HOME. -
Output staging (per-ABI plain copy, no symlinks, no fat binary): after a successful
cargo-ndkbuild, each per-ABI artefact shall be copied to its Android-ABI-named jniLibs directory using the following Rust-triple ↔ Android-ABI mapping:target/aarch64-linux-android/<profile>/libchanora_bridge.so→apps/chanora_flutter/android/app/src/main/jniLibs/arm64-v8a/libchanora_bridge.sotarget/armv7-linux-androideabi/<profile>/libchanora_bridge.so→apps/chanora_flutter/android/app/src/main/jniLibs/armeabi-v7a/libchanora_bridge.sotarget/x86_64-linux-android/<profile>/libchanora_bridge.so→apps/chanora_flutter/android/app/src/main/jniLibs/x86_64/libchanora_bridge.sowhere<profile>isdebugorreleaseper item 4. Each.sois independent; there is no fat-binary equivalent. The copy shall overwrite any prior staged.soat the destination.
Item 6 (extended): in addition to staging
libchanora_bridge.so, the build automation shall co-stagelibc++_shared.sofrom the active NDK sysroot into the same per-ABIjniLibs/<abi>/directory. The source path pattern shall be$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/<sysroot_triple>/libc++_shared.so, with the following sysroot-triple ↔ Android-ABI mapping (note this mapping differs from the cargo Rust-triple mapping in item 6 — specifically,armeabi-v7amaps toarm-linux-androideabifor the NDK sysroot path, NOTarmv7-linux-androideabi):arm64-v8a→ sysroot tripleaarch64-linux-androidarmeabi-v7a→ sysroot triplearm-linux-androideabi(NOTarmv7-linux-androideabi)x86_64→ sysroot triplex86_64-linux-android
Rationale: SDD-105's
System.loadLibrary("c++_shared")preload (performed byChanoraApplication.onCreate()beforeSystem.loadLibrary("chanora_bridge")) combined with thecrates/chanora_bridge/build.rsdirectivecargo:rustc-link-lib=dylib=c++_sharedtogether ensure thatDT_NEEDED libc++_shared.sois baked intolibchanora_bridge.so; the runtime.somust therefore be co-staged intojniLibs/<abi>/so Android's per-library namespace linker (API 24+) can resolve__cxa_pure_virtualand other C++ runtime symbols at load time. Without co-staging,System.loadLibrary("chanora_bridge")fails with an unresolved-symbol error on devices where the system libc++ is not visible to the app's linker namespace. Cross-trace: SDD-105 (load order —c++_sharedmust be loaded BEFOREchanora_bridge), SDD-111 (oboe-rs C++ dependency that introduces the C++ runtime requirement), SDD-118 item 10 (release-inspection assertion shall includelibc++_shared.soas well aslibchanora_bridge.so). -
Task-graph wiring: the build task(s) shall execute before AGP's jniLib merge step, wired via
tasks.matching { it.name.startsWith("merge") && it.name.endsWith("JniLibFolders") }.configureEach { dependsOn(<the cargo-ndk task>) }. The task shall NOT be wired viapreBuild.dependsOn(...)(preBuild fires too early in the AGP graph and runs even for non-build configurations such as IDE sync, which would force unnecessary cargo invocations). -
Gradle up-to-date semantics: the task shall declare, for Gradle incremental-build correctness:
inputs.dir("$rootProject/crates")— Rust source treeinputs.file("$rootProject/Cargo.toml")— workspace manifestinputs.file("$rootProject/Cargo.lock")— dependency-version pinoutputs.files(<the three per-ABI jniLibs.sodestination paths from item 6>)Cargo's own incremental cache layers on top; both Gradle and cargo must independently agree that nothing has changed for the task to skip cleanly. (Path roots in this item are illustrative; the implementation shall resolve them against the actual workspace layout rather than hard-coding the literal$rootProjectstring.)
-
Preflight (no auto-install): at task-configuration or task-action time, the task shall check that
cargois present, thatcargo-ndkis present, and that the three rustup targetsaarch64-linux-android,armv7-linux-androideabi,x86_64-linux-androidare installed. On any missing prerequisite the task shall fail with an actionable error message that includes the exact remediation commands — at minimumcargo install cargo-ndkandrustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android. The task shall NOT auto-install any of these tools and shall NOT mutate the user's PATH,~/.cargo/, or~/.rustup/state. -
Release-inspection assertion: immediately before the AGP
bundleReleaseorbundleDebugtask runs (wired via the samemergeXxxJniLibFoldersgraph node as item 7, or by a siblingdoFirston the bundle task), each expected.sodestination from item 6 shall be asserted to exist and to have a size greater than 1 KiB. If any.sois missing or trivially small, the build shall fail with a clear message naming the missing ABI and the expected path. Rationale: catches silent cargo-ndk task skips or empty-link-output regressions before they ship into an AAB. -
Idempotency: if all three staged
.sofiles are newer than every declared input file (item 8) AND cargo's own incremental cache confirms no Rust input changed, both thecargo-ndkinvocation and the per-ABI copy step shall be skipped. The interaction model is layered — Gradle'sinputs/outputsgating decides whether the task runs at all; if the task does run, cargo's own incremental decides whether it actually relinks. Both must agree on "no change" for the full no-op path. -
Caching across clean builds: the cargo
target/directory lives at the repository root (outsideapps/chanora_flutter/android/) and is OUTSIDE the Gradlecleanscope../gradlew cleanMUST NOT remove the cargotarget/cache. The task shall NOT registertarget/(or any subpath of it) as a Gradle build output, since doing so would make Gradle'scleandelete it. The implementation shall document this constraint inline in the build script for future maintainers. -
Parallelism stance: the baseline implementation shall use a single
cargo-ndkinvocation with-t arm64-v8a -t armeabi-v7a -t x86_64(cargo-ndk iterates ABIs sequentially internally), matching the single-shot model used by the SDD-119 iOS / macOS automation. The implementation shall NOT split per-ABI into parallel Gradle tasks at this revision. Revisit only if dev-cycle wall-clock cost becomes observably painful; a future SDD revision may relax this stance. -
Cross-platform contract scope: SDD-118 is Android-only. iOS and macOS bridge-build automation is owned by SDD-119. The two units share no files and no tasks; their only shared contract is the principle that the
chanora_bridgeRust cdylib is auto-built and embedded by the platform's native build system without manual developer steps. The mechanisms (Gradle Exec task + cargo-ndk + jniLibs copy here, vs. CocoaPods podspec + cargo +lipo+ framework-layout shell there) reflect platform conventions and are intentionally independent.
Notes:
- The Android automation is hand-rolled in the same spirit as the iOS / macOS podspecs (SDD-119): no cargokit, no rinf, no custom xtask wrapper at this revision. A future SDD revision may unify all three platforms under one cross-platform Rust-cdylib build tool; the explicit decision at this revision is to match the existing iOS / macOS pattern so that a single mental model covers all three platforms.
- The Android profile-mapping divergence from the iOS / macOS always-
--releasepattern (item 4) is deliberate and is the only material design divergence between SDD-118 and SDD-119. It is documented here as a known asymmetry rather than as a latent inconsistency.
SDD-119: iOSAndMacOSBridgeBuildAutomation shall record, as a documentation-after-code back-fill, the existing CocoaPods-podspec-driven automation that builds the chanora_bridge Rust cdylib for iOS and macOS and embeds the result as an @rpath-based .framework consumed by the Flutter Runner via vendored_frameworks. The implementation already exists in code under apps/chanora_flutter/ios/chanora_bridge.podspec and apps/chanora_flutter/macos/chanora_bridge.podspec; this SDD unit anchors the design contract that has lived in code without an SDD / SAD anchor since landing.
- Status: Draft (back-fill — implementation already exists)
- Type: Software Detailed Design Item
- Stage: P0 / MVP
- Software unit:
apps/chanora_flutter/ios/chanora_bridge.podspec(iOS — single archaarch64-apple-ios, flat framework),apps/chanora_flutter/macos/chanora_bridge.podspec(macOS — dual archaarch64-apple-darwin+x86_64-apple-darwinjoined vialipo,Versions/Aframework layout), plus their Podfile entries (iOS Podfile line 41; macOS Podfile line 34). - Source SAD: SAD-061 (iOS runtime), SAD-062 (App Store / packaging). No dedicated macOS-runtime SAD anchor exists; the macOS half of this unit is sourced by parallelism from SAD-061 / SAD-062 pending a SAD revision that adds a macOS-runtime allocation item (flagged in §11 coverage matrix).
- Cross-trace SDD: SDD-104 (
ios_voice_unit/ VPIO — consumer of the produced iOS framework), and any iOS audio-related SDD downstream of SDD-104 by transitive use of the same.framework. - Verification method: Build-system Test (CocoaPods integration on a clean derived-data state), Platform Test (iOS device build, macOS desktop build, both with
chanora_bridgesymbols resolving at runtime).
Implementation requirements (back-fill of existing podspec automation):
-
iOS pattern —
apps/chanora_flutter/ios/chanora_bridge.podspec(file ranges per the researcher's report):- Hand-rolled podspec wired into the iOS app via the Podfile (iOS Podfile line 41).
- Dual-hook automation:
prepare_command(podspec lines 51–97) runs atpod installtime;script_phase :before_compile(podspec lines 114–147) runs at every Xcode build. Both hooks call intocargo build --release --target aarch64-apple-ios -p chanora_bridge. - Single-architecture build:
aarch64-apple-iosonly (real arm64 devices). The simulator slice (aarch64-apple-ios-sim) is NOT actually built by the script despite a comment in the podspec referring to it (see item 4 doc-drift note). - Always
--releaseprofile (the SDD-118 Android variant-driven profile mapping is NOT used here). - Environment on the cargo invocation:
IPHONEOS_DEPLOYMENT_TARGET=13.0,CMAKE_POLICY_VERSION_MINIMUM=3.5,CMAKE_OSX_DEPLOYMENT_TARGET=13.0(the last is set because the cargo cmake bridge keys off the OSX variant when building from a macOS host even for an iOS target). - Output staging:
libchanora_bridge.dylibis copied into a flat (non-versioned).frameworkbundle. A hand-writtenInfo.plistis emitted into the framework.install_name_tool -id @rpath/chanora_bridge.framework/chanora_bridgerewrites theLC_ID_DYLIBso the framework is@rpath-relocatable. - CocoaPods
vendored_frameworksdirective auto-embeds the produced.frameworkinto the Runner.app target via the standard CocoaPods embed-frameworks build phase; no additional Xcode-project edits are required. - Caching: cargo's own incremental cache in
target/plus a timestamp guard inside thescript_phaseskips rebuild when sources are unchanged.
-
macOS pattern —
apps/chanora_flutter/macos/chanora_bridge.podspec(file ranges per the researcher's report):- Parallel hand-rolled podspec wired into the macOS app via the Podfile (macOS Podfile line 34).
- Same dual-hook structure (
prepare_command+script_phase :before_compile). - Dual-architecture build:
aarch64-apple-darwinANDx86_64-apple-darwin, thenlipo -createjoined into a single universal binary. This is the "universal" mechanism — a single fat binary at the dylib layer. (This is the fundamental mechanism divergence from SDD-118 Android, where there is nolipo.) - Always
--releaseprofile. - Environment on the cargo invocation:
MACOSX_DEPLOYMENT_TARGET=10.15,LIBOPUS_STATIC=1,LIBOPUS_NO_PKG=1,CMAKE_POLICY_VERSION_MINIMUM=3.5. Rationale forLIBOPUS_STATIC=1/LIBOPUS_NO_PKG=1: avoid relying on a Homebrew-installedlibopusandpkg-configon the build host (developer-machine variance); also matches the SDD-118 Android stance for the same reason (NDK sysroot has no libopus). - Output staging: the universal
libchanora_bridge.dylibis placed inside a macOS-styleVersions/Aframework layout (chanora_bridge.framework/Versions/A/chanora_bridgeplus the standardCurrent/ top-level symlinks).install_name_tool -id @rpath/chanora_bridge.framework/Versions/A/chanora_bridgesets the install name to the versioned framework path. - CocoaPods
vendored_frameworksintegration is identical to iOS in mechanism. - Caching: cargo incremental + per-platform timestamp guard inside the
script_phase, identical to iOS.
-
Deployment-target source-of-truth — RESOLVED for macOS (v0.9.17 amendment): the macOS bridge deployment-target floor (
10.15) is now sourced from a single Ruby constantMACOS_BRIDGE_DEPLOYMENT_TARGETdeclared inapps/chanora_flutter/macos/macos_deployment_target.rbandrequire_relative-loaded by bothapps/chanora_flutter/macos/Podfile(consumed as the argument toplatform :osx, ...) andapps/chanora_flutter/macos/chanora_bridge.podspec(consumed bys.platform :osx, ..., by the fourMACOSX_DEPLOYMENT_TARGET=...env exports inside theprepare_commandandscript_phaseheredocs via Ruby#{...}interpolation, and by the two embedded frameworkInfo.plist<key>MinimumOSVersion</key>entries that are emitted inside the same heredocs). This realizes SAD-087(a)'s "single macOS build-configuration location" mandate as Option B (Ruby constant, scoped narrowly to the chanora_bridge Rust-cdylib baseline), selected over Option A (.xcconfig) because the bridge baseline is consumed by the podspec'sprepare_commandBEFORE any xcconfig is applied — a Ruby-evaluation-time constant is the only mechanism that reaches every consumer site without indirection — and over Option C (versioned text file) on the grounds that the bridge consumers are all already Ruby files, so a Ruby constant adds zero new file-format burden. The pbxprojMACOSX_DEPLOYMENT_TARGET = 10.15occurrences atRunner.xcodeproj/project.pbxprojlines 565, 666, 717 are explicitly OUT OF SCOPE for this SoT: those three lines belong to the PBXProject "Runner" build-configuration list (i.e. the project-default config that Flutter'sflutter createtemplate emits) and are independently overridden to11.0at the Runner PBXNativeTarget level (project.pbxproj lines 756, 782, 614) — they are the Runner app's project-default floor, not the bridge cdylib's floor; the matching10.15value is coincidental and the two values can legitimately diverge in the future (e.g., raising the bridge to11.0while leaving the Runner app's project-default at10.15, or vice versa). To bump the bridge floor, edit ONLYmacos_deployment_target.rb; do not introduce any other literal occurrence of the floor string underapps/chanora_flutter/macos/. The iOS half of this follow-up (IPHONEOS_DEPLOYMENT_TARGET=13.0hard-coded in the iOS podspec) is intentionally NOT addressed by this amendment — the iOS podspec is a separate Ruby file with no Podfile-shared baseline (the iOS Podfile uses a Flutter-template-generatedplatform :ios, ...line that already lives in its own configuration scope) and a parallel SoT consolidation for iOS is recorded as a follow-up SDD revision. -
iOS simulator slice — RESOLVED (decision: Option α, drop the misleading comment): P0 scope intentionally excludes
aarch64-apple-ios-sim; the iOS podspec comment atapps/chanora_flutter/ios/chanora_bridge.podspec:9-10referencing simulator support is to be removed in a follow-up commit so the in-file docstring matches the actual cargo invocation (which builds onlyaarch64-apple-ios). Rationale for choosing Option α over adding the sim slice: P0 minimalism — simulator support is a P1 concern; expanding the iOS build matrix at P0 expands scope without serving a P0 verification need. Implication for downstream: building the Runner for an iOS simulator currently relies on Xcode's own handling of an arm64-device-only framework on Apple-silicon hosts; Intel-host iOS simulator builds are not supported by this automation as written. A future SDD revision (P1) may add the simulator slice; until then, the podspec comment shall be dropped to eliminate the doc-drift defect. -
Caching mechanism (unified across iOS / macOS): cargo's incremental cache in
target/provides Rust-source-level caching; a per-platform timestamp guard inside eachscript_phaseprovides the outer CocoaPods-script-level skip. As with SDD-118 item 12, thetarget/directory lives at the repository root and is OUTSIDE both Xcode's derived-data cleanup andpod deintegrate's footprint; neither should remove cargo's cache. -
Preconditions and failure modes:
cargois expected at$HOME/.cargo/bin/cargo(the rustup default). The podspec adds$HOME/.cargo/binto PATH for the script's child process; it does NOT mutate the developer's shell profile.- The rustup targets
aarch64-apple-ios,aarch64-apple-darwin, andx86_64-apple-darwinMUST be installed (rustup target add ...). Missing targets surface as cargo errors during the prepare_command / script_phase. - A CMake 4.x toolchain MUST be present on the build host because
CMAKE_POLICY_VERSION_MINIMUM=3.5is the workaround for CMake-4-introduced policy strictness in theaudiopus_sys/ libopus build chain; on a CMake 3.x host the env var is a no-op and the build still succeeds, but the variable's documented purpose only applies to CMake 4.x. - The same
LIBOPUS_STATIC/LIBOPUS_NO_PKGrationale as SDD-118 item 5 applies on macOS (and is set in the macOS podspec env); on iOS, the audiopus_sys build path for the iOS target also statically links libopus from source by default.
-
Why hand-rolled and not Cargokit: per the podspec's own header comment, this automation is "written by hand … to avoid the Cargokit vendoring footprint we previously dropped." Maintenance posture: the podspec is project-maintained; there is no upstream-tracked build helper to follow. Bit-rot risk is currently low (the surface area is small — cargo invocation + lipo + install_name_tool + framework layout) but will rise if Apple changes framework-embedding conventions or if
audiopus_syschanges its build-script interface. The risk is accepted at this revision. -
Relationship to SDD-118 (Android counterpart): SDD-118 and SDD-119 implement the same principle — the
chanora_bridgeRust cdylib is auto-built and embedded by the platform's native build system without manual developer steps — through different mechanisms. SDD-118 uses Gradle +cargo-ndk+ per-ABI plain-copy intojniLibs/; SDD-119 uses CocoaPods podspec +cargo(+lipoon macOS) + framework-layout shell. The pair is intentionally not unified at this revision (open follow-up: a future SDD may collapse them under one cross-platform tool). The Android profile-mapping divergence (SDD-118 item 4) and the macOS multi-arch / lipo divergence (item 2 above) are the two material design differences between the units; both are documented as deliberate.
Notes:
- This is a back-fill SDD. No new implementation work is created by SDD-119; the code already exists and ships. A builder agent does NOT need to act on SDD-119. The doc-drift defect in item 4 is RESOLVED (v0.9.13). The macOS deployment-target single-source-of-truth follow-up in item 3 is RESOLVED for macOS (v0.9.17); the iOS half remains an open follow-up for a future SDD revision.
- SAD-side anchoring of the build-automation pattern at the architectural layer is NOT performed by this SDD unit; a software-architect dispatch is required if the orchestrator wants a SAD-layer build-automation item.
SDD-120 — Realtime audio benchmark harness and advisory CI infrastructure
- Source SAD: SAD-088 (criterion-based bench harness allocated to
chanora_audiobenches/directory, extending SAD-034 audio-subsystem allocation), SAD-089 (baseline-JSON on-disk path pinned tocrates/chanora_audio/benches/baselines/x86_64-unknown-linux-gnu.json), SAD-090 (advisory CI workflow.github/workflows/bench-advisory.yml— PR-triggered, non-blocking, comment-posting reader of SAD-089), SAD-091 (baseline-update CI workflow.github/workflows/bench-baseline-update.yml—workflow_dispatch-only, sole writer of SAD-089). - Allocated to: Audio Subsystem (
crates/chanora_audio/benches/for criterion bench targets;crates/chanora_audio/examples/for the post-processor binaries — see §2 rationale) + Build/Release/Operations (.github/workflows/bench-advisory.yml,.github/workflows/bench-baseline-update.yml). - Software units (new):
crates/chanora_audio/benches/realtime_capture.rs,crates/chanora_audio/benches/opus_codec.rs,crates/chanora_audio/benches/resampler.rs,crates/chanora_audio/benches/common.rs(shared synthetic-input helper module),crates/chanora_audio/examples/emit_baseline.rs(post-bench JSON aggregator binary),crates/chanora_audio/examples/compare_baseline.rs(PR-vs-baseline comparator binary),crates/chanora_audio/Cargo.toml(dev-dependency additions and[[bench]]entries),.github/workflows/bench-advisory.yml,.github/workflows/bench-baseline-update.yml,crates/chanora_audio/benches/baselines/x86_64-unknown-linux-gnu.json(seeded SAD-089 baseline file). - Cross-trace: SDD-094 (the
chanora_audio::engine::AudioEnginecapture path that the §3 bench seam exercises is the same audio engine whoseensure_running/shutdown_if_idlelifecycle is specified by SDD-094; the harness measures the steady-state callback that runs whileensure_runninghas opened the cpal input stream, so the bench seam is part of the engine's verification surface). No cross-trace to SDD-076..SDD-080 / SDD-118: the bench targets the realtime audio path and not the PTT capability surface; the Android-bridge build automation is unrelated to the Linux-host bench workflow.
§1 — Crate selection and version pin.
- Add
criterion = "0.5"tocrates/chanora_audio/Cargo.toml[dev-dependencies]. The0.5series is the current stable major and is compatible with the workspacerust-version = "1.95"pin. Default features are kept;html_reportsis left at its default (criterion only emits HTML when invoked with--save-baseline/--load-baselineagainst the local user, and CI consumes the per-benchestimates.jsonfiles directly fromtarget/criterion/, so an HTML feature toggle is not load-bearing for the workflow). Rationale: criterion is the de-facto statistical benchmark harness for Rust; it emits a stable JSON estimates file undertarget/criterion/<bench>/<group>/estimates.jsonthat the §5 post-processor parses. - Add
dhat = "0.3"tocrates/chanora_audio/Cargo.toml[dev-dependencies].dhatis used insidebench_capture_alloc_countto install a#[global_allocator] static ALLOC: dhat::Alloc = dhat::Alloc;profiler at bench-binary scope and to comparedhat::HeapStats::get().total_blocks(or the equivalent v0.3 API name) before and after the measurement window. Rationale:dhatis the de-facto heap profiler for Rust, has zero-cost when disabled, and is the lowest-friction way to obtain a steady-state heap-allocation count for the SRS-219 clause-a zero-allocation invariant (which carriestolerance = 0). - Add three
[[bench]]entries tocrates/chanora_audio/Cargo.toml, one per bench file enumerated in §2. Each entry shall setharness = falseonly if the bench file callscriterion::criterion_main!directly (criterion'smain!macro replaces the cargo test harness); the §3 signatures usecriterion_main!soharness = falseis required. - Add two
[[example]]entries pointing atexamples/emit_baseline.rsandexamples/compare_baseline.rsso thatcargo run --example emit_baselineandcargo run --example compare_baselineresolve correctly. The §6 / §7 workflow steps invoke these tools via the--exampleflag (not--bin). Cargo auto-discovers files underexamples/as example targets, so explicit[[example]]blocks inCargo.tomlare optional; thepath = "examples/<name>.rs"field is only required if the file lives outside the conventional location.
§2 — Bench file layout.
crates/chanora_audio/benches/realtime_capture.rs— declaresbench_capture_alloc_countandbench_capture_callback_wall_clock. Usescriterion_group!(realtime_capture, bench_capture_alloc_count, bench_capture_callback_wall_clock);andcriterion_main!(realtime_capture);.crates/chanora_audio/benches/opus_codec.rs— declaresbench_opus_encode_latencyandbench_opus_decode_latency. Usescriterion_group!(opus_codec, bench_opus_encode_latency, bench_opus_decode_latency);andcriterion_main!(opus_codec);.crates/chanora_audio/benches/resampler.rs— declaresbench_resampler_throughput. Usescriterion_group!(resampler, bench_resampler_throughput);andcriterion_main!(resampler);.crates/chanora_audio/benches/common.rs— shared synthetic-input helpers per §4. Included from each bench file withmod common;(criterion bench files compile as standalone binaries, socommon.rsis included by relative module declaration in eachrealtime_capture.rs/opus_codec.rs/resampler.rs; do NOT add it as a fourth[[bench]]entry).crates/chanora_audio/examples/emit_baseline.rsandcrates/chanora_audio/examples/compare_baseline.rs— the two post-processor tools enumerated in §1 item 4 and detailed in §5 / §8. Placement rationale: emit_baseline and compare_baseline are placed underexamples/rather thanbenches/orsrc/bin/. Cargo's dependency resolver only routes[dev-dependencies]to[[test]],[[bench]], and[[example]]targets; binaries undersrc/bin/would forceserde_jsonand other CI-only crates into the production[dependencies]block, contaminating the release build. Theexamples/placement keeps these tools strictly dev-only — they are excluded fromcargo build --releaseand fromflutter build apk/aab/ipa --release. Co-locating them underbenches/would also work mechanically for dev-dep routing but conflates the criterion bench-target surface with the auxiliary post-processor surface;examples/keeps the two concerns syntactically distinct while preserving the same dev-only isolation property.
§3 — Bench function signatures and warmup phase.
fn bench_capture_alloc_count(c: &mut Criterion)— the bench installs adhat::Profiler::builder().testing().build()once at the start of the function, runs the §4synthetic_capture_buffer(960, 1)throughCaptureState::ingest(&buf)100 times as a warm-up (this primes any first-call allocations from the cpal capture-state machine — ring-buffer growth, resampler-state init, opus-encoder-handle init via the existing engine code path), then samplesdhat::HeapStats::get()to capture thetotal_blockscountB_warm, runs 1000 additionalingestcalls insidec.bench_function("capture_alloc_count", |b| b.iter(|| ingest_one())), samplesdhat::HeapStats::get()again to obtainB_final, and the criterion-reported metric value shall be(B_final - B_warm)(the post-warmup allocation count). The function additionallyassert_eq!(B_final - B_warm, 0)so a regression also fails the bench process at the local-developer surface; the CI advisory comparator incompare_baseline.rscarries the SRS-219 clause-atolerance = 0zero-allocation rule independently. Bench seam: the function exercises theCaptureState::ingest<T>(&mut self, buf: &[T])signature located atcrates/chanora_audio/src/engine.rs:1207per the §“State” reading; the bench obtains aCaptureStateinstance by calling the engine's existing test-mode constructor (the same pathtests/ptt_privacy.rsuses today). No new public API surface is created onCaptureState.fn bench_capture_callback_wall_clock(c: &mut Criterion)— uses criterion's defaultc.bench_function("capture_callback_wall_clock", |b| b.iter(|| capture_state.ingest(black_box(&buf))));over the samesynthetic_capture_buffer(960, 1)input. Criterion's internal warmup (default 3 s, configurable but not overridden here) precedes the measurement window; the harness emitsmean,median, and the 95 % and 99 % bootstrap-confidence-interval percentiles intotarget/criterion/capture_callback_wall_clock/.../estimates.json. The §5 post-processor extracts p50 (median), p95, and p99 from that file and writes the three values into the SRS-217 record'svaluetriplet.fn bench_opus_encode_latency(c: &mut Criterion)— constructs anaudiopus::coder::Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip)once outside the measurement loop (per criterion best practice; encoder construction is dominated by libopus state allocation and is not part of the per-frame latency), pre-builds asynthetic_opus_frame()PCM buffer (exactlyFRAME_SAMPLES = 960samples per SAD-088 / SysDes-156), and measuresencoder.encode_float(&pcm, &mut opus_out)per iteration. The bench reports p50/p95/p99 as in item 2.fn bench_opus_decode_latency(c: &mut Criterion)— same pattern as item 3 but withaudiopus::coder::Decoder::decode_float(&opus_bytes, &mut pcm_out, false). The SDD explicitly pins the bench target to theaudiopusdirect decoder call rather thantsclientlib::AudioHandler::fill_buffer, becauseAudioHandler::fill_bufferperforms jitter-buffer dequeue + Opus decode + PCM mix in a single call and the wall-clock measurement would conflate three distinct concerns. The canonical SRS-217 metricopus_decode_latencytherefore refers to the codec-level decode of a single 20 ms / 960-sample frame, not the full AudioHandler call. (Future revision may add a separateaudio_handler_fill_buffer_latencymetric covering the composite call; that metric is out of scope at this SDD revision.)fn bench_resampler_throughput(c: &mut Criterion)— opens onecriterion::BenchmarkGroupnamedresampler_throughputand registers three sub-benches:c.bench_function("44100_to_48000", ...),c.bench_function("16000_to_48000", ...), andc.bench_function("48000_passthrough", ...). Each sub-bench feeds a 1-second synthetic-sine input (length is 44100 / 16000 / 48000 frames respectively) through the existing engine's resampler entry point and reportssamples_per_second = input_len / measured_duration_secs. The bench usesgroup.throughput(Throughput::Elements(input_len as u64))so criterion's report exposes the throughput estimate directly inestimates.json.
§4 — Synthetic input generation.
crates/chanora_audio/benches/common.rsexposes two functions, both deterministic (no RNG, no time-of-day input):pub fn synthetic_capture_buffer(frames: usize, channels: usize) -> Vec<f32>andpub fn synthetic_opus_frame() -> Vec<f32>(the latter is the special casesynthetic_capture_buffer(960, 1)).- Sample generation: a 440 Hz sine at amplitude 0.5 over the 48 kHz playback rate (matching the audio engine's mixer rate per SAD-034). For frame index
nthe sample value is0.5 * (2.0 * PI * 440.0 * (n as f32) / 48_000.0).sin(). For multi-channel buffers the same scalar value is replicated across all channels at the same frame index (interleaved layout, matching cpal'sStreamdata layout). No randomness, no fade-in, no DC offset — the same input produces the same bytes across runs and the same bytes across hosts. - Determinism is load-bearing for §5 baseline stability: if the synthetic input drifts between runs the criterion
estimates.jsonwould also drift and the comparator would emit false-positive 🔴 markers. The §5 emit-baseline binary and the §6 comparator both assume byte-stable input.
§5 — JSON post-processing.
- After each bench file runs, criterion writes
target/criterion/<bench-name>/<group-name>/estimates.jsonper benchmark group. The §5 binarycrates/chanora_audio/examples/emit_baseline.rsreads the five expected estimates files (one per §3 bench function, with the resampler bench contributing three sub-bench estimate files that are aggregated into the single SRS-217resampler_throughputrecord by averaging the three throughput estimates into a per-mode triplet, NOT a single scalar — the SRS-217 schema carries a per-mode record). Invocation pattern:cargo run --example emit_baseline(NOTcargo run --bin emit_baseline); see §2 item 5 rationale for theexamples/placement. - The binary parses each
estimates.json(a stable criterion-emitted JSON shape:mean,median,std_dev,confidence_interval) and projects each metric into an SRS-217 record with fields{ metric: <name>, value: <f64 or triplet>, unit: <string>, tolerance_pct: <f64 from SRS-219>, host: "x86_64-unknown-linux-gnu", timestamp_utc: <RFC3339> }. The full output is a single JSON document with one top-level arraymetrics. - The binary writes the document to
./current.jsonin the workspace root (not the SAD-089 path — that path is the comparator's input on advisory runs and the writer's output target on baseline-update runs; §6 step 7 reads SAD-089 from git, §7 step 5 writes SAD-089 by copy). - Rationale for the Rust-binary approach over a
tools/bench-emit.shscript: same toolchain as the bench files (no separatejq/python/bashdependency on the runner), sameserde_jsonalready pulled by the workspace as a[dev-dependencies]entry (routed to theexamples/targets per §2 item 5), and the example can share types withcompare_baseline.rsvia a small shared module underexamples/(both examples use the same SRS-217 struct definitions).
§6 — .github/workflows/bench-advisory.yml structure (SAD-090 realization).
- Triggers:
pull_request: { types: [opened, synchronize, reopened], branches: [main] }andpush: { branches: [main] }. (The default branch ismainper the existing.github/workflows/ci.ymlconvention; if the repository default branch ever renames, both YAML files in this SDD unit must update in lockstep.) - Job name:
bench-advisory.runs-on: ubuntu-latest.permissions: { pull-requests: write, contents: read }—pull-requests: writeis required by step 9 (PR-comment posting);contents: readis the minimum foractions/checkout. - Step 1:
actions/checkout@v4withfetch-depth: 0. Full history is required because step 6 resolves the PR merge-base viagit merge-base origin/main HEADand step 7 reads the baseline file from that merge-base viagit show. - Step 2: install Rust toolchain via
dtolnay/rust-toolchain@stable(matching the existing.github/workflows/ci.ymlpattern). The workspacerust-version = "1.95"floor is satisfied by@stableat the time of authoring; if a future workspace pin moves above the public stable channel, both this file andci.ymlmust update. - Step 3:
Swatinem/rust-cache@v2(matching existing.github/workflows/ci.yml). Reuses the bench target directory across runs. - Step 4: install the same system audio deps
ci.ymlinstalls (libasound2-dev libpulse-dev pkg-config libopus-dev). Required because thechanora_audiocrate linkslibopusand the cpal Linux backend pulls ALSA headers at build time. - Step 5:
cargo bench --bench realtime_capture --bench opus_codec --bench resampler. (cargo benchwithout--bencheswould build doctests; the explicit--bench <name>list bypasses that and matches the three[[bench]]entries from §1 item 3.) No-- --output-format jsonflag is required because §5 readstarget/criterion/.../estimates.jsonfiles directly; criterion writes those unconditionally. - Step 6:
cargo run --example emit_baseline→ writes./current.jsonto the workspace root. (Invocation flag is--example, not--bin; see §2 item 5 rationale.) - Step 7:
MERGE_BASE=$(git merge-base origin/main HEAD)andgit show $MERGE_BASE:crates/chanora_audio/benches/baselines/x86_64-unknown-linux-gnu.json > baseline.json. If the merge-base predates the introduction of SAD-089 (i.e. the file did not exist at that commit),git showexits non-zero; the step shall handle the missing-baseline case by writing an emptybaseline.jsonplaceholder and emitting a single advisory commentBaseline not yet established at merge-base; first run on default branch will establish.instead of running the comparator. This is the only branch where the workflow short-circuits before the comparator step. (Thepush: branches: [main]trigger never hits this short-circuit after the first baseline lands.) - Step 8:
cargo run --example compare_baseline -- --current ./current.json --baseline ./baseline.json --output ./report.md. The example applies the SRS-219 per-metric tolerance table and the §8 marker logic, then writes a markdown table to./report.md. The example shall always exit 0 (per SRS-218 clause 4); regression detection is rendered as a 🔴 marker inside the comment, not as a non-zero exit code. - Step 9: post the markdown as a PR comment via
actions/github-script@v7, gated onif: github.event_name == 'pull_request'(thepushtrigger has no PR to comment on; forpushruns the workflow still writes./report.mdas a workflow artifact viaactions/upload-artifact@v4so the default-branch trend is inspectable from the Actions tab). Comment body isbody: require('fs').readFileSync('report.md', 'utf8'). The script shall also de-duplicate prior comments by the samegithub-actions[bot]identifier carrying a sentinel marker line (e.g.<!-- chanora-bench-advisory -->) at the head of the body — on each run the existing comment is updated in place rather than appended, so a long-lived PR does not accumulate one comment per push. The de-duplication query usesoctokit.rest.issues.listCommentsfiltered onuser.login === 'github-actions[bot]'and body-startsWith the sentinel. - Step 10:
if: always()and a finalexit 0— no step in the job setscontinue-on-error: falseagainst a regression-detecting predicate, so the SRS-218 clause 4 non-blocking property is realized by absence of a failing step rather than by an explicit override. - Status-check name registered with the branch-protection ruleset (if any):
bench-advisory. The SDD does not mandate adding this to the required-checks list; SRS-218 clause 4 explicitly prohibits making it required.
§7 — .github/workflows/bench-baseline-update.yml structure (SAD-091 realization).
- Trigger:
workflow_dispatch: {}only. Nopull_request, nopush, noschedule, no tag triggers. SAD-091 is invariant on this property. - Job name:
bench-baseline-update.runs-on: ubuntu-latest(byte-comparable to SAD-090 per SAD-091).permissions: { contents: write, pull-requests: write }— write on both because step 6 opens a PR viapeter-evans/create-pull-request@v6. - Step 1:
actions/checkout@v4with default ref (nofetch-depth: 0needed; the workflow does not resolve merge-bases). - Step 2: install Rust toolchain (same shape as §6 step 2).
- Step 3: install audio system deps (same shape as §6 step 4).
- Step 4:
cargo bench --bench realtime_capture --bench opus_codec --bench resampler(same shape as §6 step 5). - Step 5:
cargo run --example emit_baseline(same shape as §6 step 6; invocation flag is--example, not--bin). - Step 6:
cp ./current.json crates/chanora_audio/benches/baselines/x86_64-unknown-linux-gnu.json. - Step 7:
peter-evans/create-pull-request@v6withtitle: "chore(bench): update baselines on $(date -u +%Y-%m-%d)",branch: bench/baseline-update-$(date -u +%Y%m%d),commit-message: "chore(bench): refresh x86_64-unknown-linux-gnu baseline",body: <generated diff summary>, andadd-paths: crates/chanora_audio/benches/baselines/x86_64-unknown-linux-gnu.json. Rationale for opening a PR rather than direct-pushing tomain: baseline updates should be reviewable in case of accidental triggering (a contributor clicks the dispatch button by mistake), runner pollution (an unrelated process on the GitHub-hosted runner skewed the measurement), or a legitimate regression that should not silently move the floor. The PR is then merged by a reviewer using the normal branch-protection flow; this preserves the SAD-091 invariant that this workflow is the sole writer of the SAD-089 file (the PR-merge commit is also written by this workflow's PR, not by a human directly editing the JSON).
§8 — Yellow-marker realization (simpler-form selection per SAD-090).
- Per SAD-090's yellow-marker pin, the SDD selects the simpler operationalization: the committed SAD-089 baseline JSON IS treated as the most recent default-branch run, and is the comparator for both the green/red tolerance evaluation AND the yellow trending evaluation. No separate run-history artifact is maintained.
compare_baseline.rslogic — for each metricmin the SRS-217 record set:- Let
c = current.metrics[m].value,b = baseline.metrics[m].value,t = SRS-219 tolerance for m. - If
mis a zero-tolerance metric (SRS-219 clause a —capture_alloc_count): emit 🔴 ifc != 0, 🟢 ifc == 0. Yellow does not apply. - Otherwise (
t > 0): computedelta_pct = (c - b) / bwhenb > 0, ordelta_pct = 0.0whenb == 0(a degenerate baseline). Then:- 🔴 if
delta_pct > t(regression beyond tolerance). - 🟡 if
0.5 * t < delta_pct ≤ t(within tolerance but trending up; the SAD-090 yellow-marker semantics). - 🟢 otherwise (
delta_pct ≤ 0.5 * t, including all improvements wheredelta_pct < 0).
- 🔴 if
- Let
- The markdown report's table carries columns:
metric | unit | baseline | current | delta_pct | tolerance | marker. Marker column uses the literal emoji characters 🟢 / 🟡 / 🔴. A header line cites the merge-base SHA from §6 step 7 so the reviewer can confirm which baseline the comparison used.
§9 — Cross-traces (informational, not derivation).
- SDD-094 (cross-platform audio engine
voice_join/voice_leavelifecycle): the §3 bench seam targetsCaptureState::ingest, which is part of the engine's running-state surface exposed byAudioEngine::ensure_running. The harness measures the steady-state callback path that runs while the engine is in theensure_running == truestate; this places the bench inside SDD-094's verification surface. The bench does NOT construct a fullAudioEngine(no cpal stream is opened — the bench drivesCaptureState::ingestdirectly with synthetic input), so the §3 bench seam is isolated from cpal device enumeration and from JNI / oboe-rs concerns. - SDD-118 (Android
chanora_bridgebuild automation): explicitly NOT cross-traced. The bench harness runs only on theubuntu-latesthost with thex86_64-unknora-linux-gnutarget per SysDes-157; Android cross-compilation is not exercised by either workflow file in this SDD unit. - SDD-076..SDD-080 (PTT capability surface): explicitly NOT cross-traced. The realtime callback being benched is downstream of these — a PTT key event ultimately gates the audio engine's transmit path — but the bench specifically targets the audio capture / codec / resampler path, not the PTT event-routing path. Adding PTT events would conflate the measurement.
§10 — Out of scope (deferrals preserved through Wave 1.5).
- No production telemetry export — the bench is a CI-only artifact; no runtime metric is exported from a shipping
chanora_audiobuild. Theexamples/placement of the post-processor binaries (per §2 item 5 rationale) is the mechanism that enforces this release-artifact isolation:[dev-dependencies](includingserde_json,criterion,dhat) are excluded fromcargo build --releaseby Cargo design, so neither the criterion bench targets nor theemit_baseline/compare_baselineexamples — and none of their transitive dev-only deps — can be reached by a release build of the production cdylib or the Flutter--releaseartifacts. - No hard CI gate — SRS-218 clause 4 is invariant; the advisory workflow never fails a check on regression.
- No multi-host benchmarking — SysDes-157 pins the host to
x86_64-unknown-linux-gnu; the SAD-089 filename is target-triple-keyed to permit future multi-host expansion without directory restructuring, but adding a second host (e.g.aarch64-apple-darwinfor macOS measurement) is out of scope at this revision. - No IDE integration — no VS Code task, no IntelliJ run configuration, no flutter-tool wrapper is authored.
- No benchmark of the Dart-side
flutter_rust_bridgeround-trip — deferred (researcher Tier B4). Measuring the FRB serialization overhead would require a separatepackage:benchmark_harnesssetup on the Flutter side and a synthetic Dart harness; the Rust-side bench in this SDD unit does not exercise that path.
§11 — Verification matrix (forward-allocation, not authoritative).
- The new SDD unit traces to suggested SWE.4 verification IDs
SWE4-UV-058throughSWE4-UV-062covering the five §3 bench functions (bench_capture_alloc_count,bench_capture_callback_wall_clock,bench_opus_encode_latency,bench_opus_decode_latency,bench_resampler_throughput). These IDs are SUGGESTED forward allocations only; verification-engineer authors the actual SWE.4 entries in a follow-up dispatch per the SAD-090 verification-method field (CI Dry Run + Inspection). The SDD does not author SWE.4 entries. - Verification method per SAD-088 / SAD-089 / SAD-090 / SAD-091: Inspection of the bench-file structure and YAML structure against this SDD unit; Test (manual local
cargo bench --bench …invocation) for the §3 bench functions; CI Dry Run (manual dispatch ofbench-baseline-update.yml) for the §7 baseline-update path; PR-comment-posting validation via the §6 step 9 sentinel-marker round-trip on a synthetic PR.
Notes:
- This is a forward-looking SDD unit. The bench files, the Cargo.toml additions, the two workflow YAML files, the
emit_baseline.rs/compare_baseline.rsbinaries, and the initial seed ofcrates/chanora_audio/benches/baselines/x86_64-unknown-linux-gnu.jsonare NEW implementation work for a builder agent. The seed baseline file is generated by manually dispatchingbench-baseline-update.ymlonce after the builder lands the rest of the unit; that first dispatch is the bootstrap that the §6 step 9 short-circuit branch will then no longer take on subsequent PR runs. - The
tsclientlib::AudioHandler::fill_bufferdecode-side bench was explicitly NOT selected as the canonicalopus_decode_latencytarget (see §3 item 4 rationale). A future SDD revision may addaudio_handler_fill_buffer_latencyas a separate composite-call metric without modifying the canonical Opus codec metric.
11. Updated SAD-to-SDD Coverage Matrix
| SAD Range | SDD Coverage |
|---|---|
| SAD-001 through SAD-060 | Covered by inherited SDD baseline SDD-001 through SDD-070 |
| SAD-061 through SAD-070 | Covered by SDD-071 through SDD-080 |
| SAD-061 through SAD-070 | Covered by SDD-098 through SDD-104 (iOS platform additions) |
| SAD-063, SAD-081, SAD-084, SAD-085, SAD-086 | Covered by SDD-105 through SDD-109 (Android P0 additions; SDD-110 re-anchored to SAD-071 / SAD-076 in v0.9.11 because the PTT-capability pin is a PTT-architecture concept, not a foreground-service-allocation concept) |
| SAD-071, SAD-076 | Covered by SDD-110 (Android pins to FocusedPttBackend / L0Focused; capability badge text feed via BridgeEvent::PttCapability). Cross-trace: SDD-088 (PttController), SDD-091 (PttCapabilityBadge), SDD-031 (LocalizationService). |
| SAD-077, SAD-081, SAD-084, SAD-085, SAD-086 | Covered by SDD-111 through SDD-116 (Android voice audio backend — SRS-210..SRS-215). No dedicated SAD anchor exists for the platform-abstracted mobile-voice-audio-backend trait itself; SDD-111 is sourced from SAD-077 / SAD-081 pending a SAD revision that adds a dedicated allocation item. |
| SAD-063 | Covered by SDD-118 (Android chanora_bridge cdylib build automation — Gradle + cargo-ndk + per-ABI jniLibs staging). Cross-trace: SDD-073, SDD-105, SDD-109. |
| SAD-061, SAD-062, SAD-087 | Covered by SDD-119 (iOS / macOS chanora_bridge cdylib build automation — CocoaPods podspec + cargo + lipo + framework-layout shell, back-fill of existing code). Source SAD allocation: iOS half anchored to SAD-061 (iOS runtime) and SAD-062 (App Store / packaging); macOS half anchored to SAD-087 (macOS runtime baseline — deployment target isolation, universal-binary lipo packaging, .framework Versions/A layout, hand-rolled CocoaPods podspec automation), authored in SAD v0.9.7 to close the macOS-runtime anchor gap that this SDD unit had previously bridged by parallelism from SAD-061 / SAD-062. SDD-117 remains reserved-but-unauthored for the deferred ios_voice_unit trait back-fill noted by SDD-111. |
| SAD-088, SAD-089, SAD-090, SAD-091 | Covered by SDD-120 (realtime-audio benchmark harness and advisory CI infrastructure — criterion-based bench harness, dhat-backed heap-allocation-count metric, SAD-089 baseline JSON post-processor, SAD-090 advisory PR-comment workflow with merge-base baseline read, SAD-091 workflow_dispatch-only baseline-update workflow that opens a PR rather than direct-pushing; simpler-form yellow-marker realization treats the SAD-089 baseline as the comparator for both red/green tolerance evaluation and yellow trending detection). Cross-trace: SDD-094 (the bench seam exercises the same chanora_audio::engine capture path whose lifecycle is specified by SDD-094). Suggested SWE.4 forward allocation: SWE4-UV-058 through SWE4-UV-062 (verification-engineer follow-up; not authored by this SDD unit). |
Baseline Candidate 0.9.1 Update
| Version | Date | Description |
|---|---|---|
| 0.9.1 | 2026-05-14 | Updated baseline after product decision closure: Apple App Store SDK gate uses Xcode 26+ and iOS 26 / iPadOS 26 SDK+ since 2026-04-28, platform baselines and decision traceability propagated across the document set. |
Baseline Candidate 0.9.2 Update
| Version | Date | Description |
|---|---|---|
| 0.9.2 | 2026-05-14 | Corrected Apple App Store Connect upload gate effective date and preserved layer-specific traceability. |
Baseline Candidate 0.9.3 Update
| Version | Date | Description |
|---|---|---|
| 0.9.3 | 2026-05-15 | Added desktop PTT detailed design SDD-081 through SDD-092: DesktopPttBackend trait, PttCapabilityLevel enum, Windows Raw-Input + low-level-hook + Focused implementations, macOS event-tap + Focused implementations, Linux GNOME-Wayland portal + Focused implementations, PttController, AudioTransmitGate, PttSanitizer layer, PttCapabilityBadge widget, MissedKeyUpWatchdog task. Strict sourcing (SDD -> SAD only) preserved. |
Baseline Candidate 0.9.4 Update
| Version | Date | Description |
|---|---|---|
| 0.9.4 | 2026-05-15 | Added SDD-093 covering SAD-080. SDD-093 fixes the PttInputClass enum surface, the cross-backend rebind contract for MouseSideButton, and the Flutter side-button capture path against the PointerEvent.buttons bitmask. Strict layered sourcing preserved (SDD -> SAD only). |
Baseline Candidate 0.9.5 Update
| Version | Date | Description |
|---|---|---|
| 0.9.5 | 2026-05-15 | Added v1 audio + PTT lifecycle detailed design SDD-094 through SDD-097 sourced from SAD-081..083: bridge surface drops start_audio / stop_audio and exposes chanora_bridge::voice::voice_join / voice_leave with idempotent AudioEngine::ensure_running and shutdown_if_idle, chanora_audio::TransmitMode as #[repr(u8)] enum persisted via chanora_storage::IdentityStore::{set,get}_transmit_mode and mirrored across FRB as BridgeTransmitMode, chanora_audio::release_tail::ReleaseTailTimer as a tokio-task-owning adapter on the watch handle (no unsafe, no mutex; configurable 0–500 ms via release_tail_ms), and the new apps/chanora_flutter/lib/widgets/voice_bar.dart + voice_settings.dart surfaces replacing the legacy _AudioControls widget. Strict layered sourcing preserved (SDD -> SAD only). |
Baseline Candidate 0.9.6 Update
| Version | Date | Description |
|---|---|---|
| 0.9.6 | 2026-05-17 | Added iOS P0 platform SDD-098 through SDD-104 to close traceability gaps identified in the P0 audit: IOSAudioSessionConfig (AVAudioSession .playAndRecord configuration), IOSPermissionRequester (microphone permission flow), IOSRouteChangeHandler (route change observation and Rust bridge call), IOSInterruptionRecovery (audio interruption handling), Info.plist / VPIOUnit (background audio), FocusedPttBackend only (iOS no global PTT), and VoiceProcessingIO (hardware AEC/AGC/NS always engaged). Sourced from SAD-061, SAD-064, SAD-065, SAD-071. Strict layered sourcing preserved (SDD -> SAD only). |
Baseline Candidate 0.9.7 Update
| Version | Date | Description |
|---|---|---|
| 0.9.7 | 2026-05-17 | Added design notes to SDD-101: (1) shouldResume == false behavior for iOS audio interruption recovery — VPIO remains paused, BridgeEvent_InterruptionState carries shouldResume: false to Flutter, snackbar surfaces the interruption, manual rejoin required; (2) iOS audio lifecycle interleaving concern — handlers are individually serialized but lack explicit lifecycle state, route-change-during-interruption risk is mitigated by iOS main-thread serialization for P0. |
Baseline Candidate 0.9.9 Update
| Version | Date | Description |
|---|---|---|
| 0.9.9 | 2026-05-18 | Added Android voice audio backend detailed design SDD-111 through SDD-116 sourced from SRS-210..SRS-215 (Wave 2A SRS landing) and SAD-077 / SAD-081 / SAD-084 / SAD-085 / SAD-086. SDD-111 (AndroidVoiceAudioBackend — MobileVoiceAudioBackend Rust trait, android_voice_unit module, oboe-rs 0.6.x primary with direct ndk::audio contingency, desktop cpal unaffected; iOS trait back-fill deferred to candidate SDD-117). SDD-112 (AndroidVoiceStreamConfig — 48 kHz mono, framesPerBurst query, requested LowLatency performance mode with achieved-mode recording, output VoiceCommunication usage + Speech content type, input preset VoiceCommunication → VoicePerformance → Generic retry ladder, Exclusive → Shared sharing-mode fallback, error/disconnect callback enqueues to engine retry path). SDD-113 (AndroidHardwareAudioEffects — session-id-bound AcousticEchoCanceler / NoiseSuppressor / AutomaticGainControl engagement via JNI with per-effect software fallback through the existing engine path; closes the prior silent no-op at crates/chanora_audio/src/engine.rs:316-323). SDD-114 (AndroidVoiceManifestRequirements — back-fill of Wave 2B-1 manifest declarations: INTERNET, RECORD_AUDIO, FOREGROUND_SERVICE, FOREGROUND_SERVICE_MICROPHONE, POST_NOTIFICATIONS, MODIFY_AUDIO_SETTINGS, BLUETOOTH_CONNECT; <service android:name="…AndroidVoiceForegroundService" android:exported="false" android:foregroundServiceType="microphone"/>; CI manifest assertion). SDD-115 (AndroidVoiceLifecycleSequencing — forward order Dart→bridge→service start→stream open→mode engage→effect bind→start; reverse order on leave; route-change observation via ACTION_AUDIO_BECOMING_NOISY + AudioDeviceCallback; audio-focus loss handling per TRANSIENT_CAN_DUCK / TRANSIENT / LOSS semantics; Bluetooth SCO connection-state-change P0 stance "no crash" with full SCO route correctness deferred to P1; Oboe callback threading rules). SDD-116 (AndroidVoiceVerificationMatrix — API 28 / 31 / 34 minimum, Pixel + Samsung minimum, per-device observation contract for getPerformanceMode / getSharingMode / getInputPreset / effect availability + create success / glass-to-glass latency measurement / ≥10 min screen-off foreground-service longevity; gate G-A5 / RISK-AUDIO-MOBILE-001 closure rule; cross-link to SWE4-UV-040..046 / SWE5-IV-016..020 / SWE6-SV-018..024). Strict layered sourcing preserved (SDD → SAD); SRS traces recorded on each unit per the SRS→SAD→SDD chain. |
Baseline Candidate 0.9.8 Update
| Version | Date | Description |
|---|---|---|
| 0.9.8 | 2026-05-18 | Reconciled Android P0 detailed design after Wave 1.5 SysRS / SRS / SAD propagation. SDD-073 (AndroidBuildConfig) updated in place (ID preserved) to record minSdk = 28 per DEC-004 (superseding the prior API 24 baseline), targetSdk policy bound to SRS-188 Google Play floor, NDK ABI set pinned to arm64-v8a / armeabi-v7a / x86_64, release-signing CI assertion, R8/ProGuard stance, and AAB output per SRS-119 (delegating packaging pipeline detail to SDD-109). SDD-028 (BackIntentService) expanded from a one-line stub to an implementable specification covering API 33+ OnBackInvokedCallback registration, pre-33 OnBackPressedDispatcher fallback, deterministic route-pop ordering, and edge cases (root route → exit candidate, modal/dialog → close-only, active PTT → ignore). Added six new Android P0 SDD units: SDD-105 (AndroidJniBootstrap — back-fill for JNI_OnLoad / initChanoraContext / library-load location, sourced from SAD-081 / SAD-086); SDD-106 (AndroidPermissionRequester — RECORD_AUDIO runtime flow with listen-only fallback, settings deep-link for permanent denial, revocation handling, BridgeEvent surface, TransmitModeSelector clamp, sourced from SAD-085); SDD-107 (AndroidVoiceForegroundService — service class, manifest entry, notification channel id chanora.voice.session, foregroundServiceType="microphone" on API 30+, POST_NOTIFICATIONS request on API 33+, lifecycle bound to voice_join / voice_leave / shutdown_if_idle, START_NOT_STICKY on process death, ongoing-notification re-post on dismissal, sourced from SAD-086); SDD-108 (AndroidAudioModeController — setMode(MODE_IN_COMMUNICATION) with prior-mode snapshot/restore, ref-count semantics composable for P1, JNI surface from Rust audio engine, error paths including AudioManager-unavailable fallback, sourced from SAD-084 — back-fills the v0.9.8 product-decision-register entry); SDD-109 (AndroidAaBuildPipeline — bundletool config, ABI splits matching SDD-073, CI signing assertion, versionCode/versionName derivation, sourced from SAD-037 / SAD-063); SDD-110 (AndroidPttCapability — pins Android P0 to FocusedPttBackend only, no global key hook, no accessibility-service PTT, capability badge text under ptt.capability.android.l0, sourced from SAD-077). Updated §11 SAD-to-SDD coverage matrix to list the new Android P0 SDD range. Strict layered sourcing preserved (SDD -> SAD only). |
Baseline Candidate 0.9.15 Update
| Version | Date | Description |
|---|---|---|
| 0.9.15 | 2026-05-18 | Added realtime-audio benchmark-infrastructure detailed-design unit SDD-120 (RealtimeAudioBenchmarkHarnessAndAdvisoryCI) sourced from SAD-088 / SAD-089 / SAD-090 / SAD-091 (the Wave 1.5 benchmark-infrastructure software-architecture chain authored at SAD v0.9.9 from the Option B scope decision propagated through SysRS-307/308/309 → SysDes-156/157/158 → SRS-216..219). SDD-120 is a single cohesive 11-section unit specifying: §1 crate selection (criterion = "0.5", dhat = "0.3" dev-deps, three [[bench]] entries and two [[bin]] entries in crates/chanora_audio/Cargo.toml); §2 bench-file layout (three files realtime_capture.rs / opus_codec.rs / resampler.rs plus a shared common.rs synthetic-input module); §3 bench function signatures with explicit warmup (100-call pre-warm into CaptureState::ingest at crates/chanora_audio/src/engine.rs:1207 before 1000-call dhat measurement window; criterion default warmup for the wall-clock / encode / decode / throughput benches), with the canonical opus_decode_latency metric pinned to audiopus::coder::Decoder::decode_float directly rather than tsclientlib::AudioHandler::fill_buffer because the latter conflates Opus decode with jitter-buffer dequeue and PCM mix; §4 deterministic synthetic-input generation (440 Hz sine at amplitude 0.5, no RNG, byte-stable across runs); §5 JSON post-processing via a Rust binary crates/chanora_audio/benches/emit_baseline.rs that parses criterion's target/criterion/.../estimates.json files and projects them into the SRS-217 schema; §6 .github/workflows/bench-advisory.yml (PR + push triggers, dtolnay/rust-toolchain@stable + Swatinem/rust-cache@v2 matching the existing ci.yml pattern, audio system deps libasound2-dev libpulse-dev pkg-config libopus-dev, fetch-depth: 0 for merge-base resolution, git show $MERGE_BASE:…/x86_64-unknown-linux-gnu.json baseline read with a missing-baseline short-circuit, actions/github-script@v7 PR comment with sentinel-marker de-duplication and pull_request-only gating, actions/upload-artifact@v4 for push-trigger reports, always exit 0 per SRS-218 clause 4); §7 .github/workflows/bench-baseline-update.yml (workflow_dispatch only — sole writer of SAD-089 per SAD-091, peter-evans/create-pull-request@v6 opens a PR rather than direct-pushing so baseline updates are review-gated against accidental triggering or runner pollution); §8 yellow-marker realization selecting the SAD-090-permitted simpler form where the SAD-089 baseline JSON is the comparator for both red/green tolerance evaluation and yellow trending detection (no separate run-history artifact), with the comparator algorithm (compare_baseline.rs) pinned: 🔴 if delta_pct > t, 🟡 if 0.5*t < delta_pct ≤ t, 🟢 otherwise; zero-tolerance metrics (heap allocation, SRS-219 clause a) collapse to 🔴/🟢 only; §9 cross-traces (SDD-094 audio-engine lifecycle as the bench-seam container; explicit NOT-cross-traces to SDD-118 Android bridge build automation and SDD-076..080 PTT capability surface, both correctly out of scope); §10 deferrals preserved (no telemetry export, no hard CI gate, no multi-host bench, no IDE integration, no Dart-side flutter_rust_bridge bench — researcher Tier B4); §11 suggested-forward SWE.4 allocation SWE4-UV-058 through SWE4-UV-062 (NOT authored at this layer — verification-engineer follow-up). Updated §11 SAD-to-SDD coverage matrix with a new SAD-088..SAD-091 row mapping to SDD-120. Strict layered sourcing preserved (SDD -> SAD only); no SRS, SysDes, SysRS, or SAD edits performed. Recommended next agent: builder (Step 6 of the benchmark chain — implements the three bench files, the common.rs helper, the two binary post-processors, the Cargo.toml dev-dependency + [[bench]] + [[bin]] additions, and the two workflow YAML files; strictly per this SDD unit). |
Baseline Candidate 0.9.14 Update
| Version | Date | Description |
|---|---|---|
| 0.9.14 | 2026-05-18 | Documentation-only reconciliation of the Android voice-join lifecycle step ordering between SDD-113 item 6 and SDD-115 step list — no behavioural design content changed; the canonical ordering was already specified by SDD-113 item 6 and is already implemented by the engine code at crates/chanora_audio/src/engine.rs:322-410 (mode engage at lines 358-381 precedes AndroidVoiceUnit::open() at line 399). Trigger: Wave 3 reviewer finding on commit 15d7117, which flagged that SDD-115's step list had stream open as step 4 and mode engage as step 5, contradicting SDD-113 item 6's "mode engage → stream open → effect attach" sequence. (1) SDD-115 item 1 (voice-join sequencing) step list renumbered so that mode engage is step 4 (was step 5) and stream open is step 5 (was step 4); effect attach remains step 6; AndroidVoiceUnit::start() remains step 7. Mode-first-ordering rationale paragraph added inline after the step list documenting why mode engagement precedes AAudio stream construction (AAudio binds routing at open time; effect attach on a stream opened under a non-voice mode is unspecified). (2) SDD-115 item 2 (voice-leave sequencing) reverse-order step list updated to mirror the forward order: streams close (step 4, formerly mode restore) now precedes mode restore (step 5, formerly stream close — wait, reverse: the new order is stop → release effects → close streams → restore mode → stop service), and the "reverse order is mandatory" note extended to cover closing streams under MODE_IN_COMMUNICATION before reverting the mode. (3) SDD-115 item 3 justification text updated to reference the new step numbers (step 4 mode engage before step 5 stream open before step 6 effect attach). (4) SDD-115 item 5 audio-focus request cross-reference updated from "voice-join (step 5)" to "voice-join (step 4, alongside the SDD-108 mode engage)" to track the renumbering. (5) SDD-113 item 6 extended with an explicit cross-reference to SDD-115 step ordering 4–6 and a joint-authority clause requiring future ordering amendments to update both units in the same revision. Other SDD units inspected for implicit assertion of the old ordering: SDD-107 (item containing "service start happens BEFORE setMode" at line 1354 — consistent with both old and new orderings, no change needed), SDD-108 (no stream-vs-mode ordering assertion, no change needed), SDD-111 (no lifecycle ordering language, no change needed), SDD-112 (no lifecycle ordering language, no change needed). Strict layered sourcing preserved (SDD -> SAD only); no SRS or SAD edits required as both upstream layers were silent on the inter-element ordering and delegated it to SDD-113 / SDD-115. Wave 3 follow-up scope (folded into this revision): (a) SWE.4 ID allocation — docs/verification/swe4-unit-verification-plan.md §12 allocates SWE4-UV-053..055 for the three TransmitModeSelector permission-state clamp tests (crates/chanora_audio/src/transmit_selector.rs:343-435, commit 635d160 — previously cited as the "SWE4-UV-026 family" placeholder) and SWE4-UV-056..057 for the two DiagnosticExport::with_android_audio rendering tests in crates/chanora_diagnostics (commit aea50a1 / commit-8 amend). (b) SWE4-UV-040 x86 pass-criteria correction (CRITICAL) — the prior pass-criteria string ndk.abiFilters set equals {arm64-v8a, armeabi-v7a, x86_64, x86} contradicted SDD-073 item 4 and SDD-118 item 3 (which both explicitly exclude 32-bit x86); corrected to {arm64-v8a, armeabi-v7a, x86_64} with explicit x86 32-bit is out of scope note. (c) Cross-trace and Version-header corrections — android-p0-acceptance.md TC-2 trace retargeted SAD-077 → SAD-085 (permission-requester is SAD-085, not the diagnostics PttSanitizer at SAD-077); TC-4 trace retargeted SRS-208 → SRS-195/SRS-196 (mode pin is not the PTT-capability trace); TC-6 trace retargeted SAD-085 → SAD-084 (AudioModeController is SAD-084, not the permission requester); TC-13 SWE.5 cite retargeted SWE5-IV-021 → SWE5-IV-020. Doc-Version headers refreshed across SAD (0.9.7→0.9.8), SysDes (0.9.2→0.9.7), SRS (0.9.2→0.9.8), product-decision-register (0.9.7→0.9.8), traceability-matrix (0.9.2→0.9.9), swe4-unit-verification-plan (0.9.2→0.9.14) to reflect each document's actual content baseline. (d) Traceability-matrix §E row 8 added: governance cross-reference flagging that SDD-111 has no dedicated SAD anchor for the platform-abstracted MobileVoiceAudioBackend trait (engineering chain remains intact via SAD-077 / SAD-081; future SAD revision to add a dedicated allocation item). §C "Status pointer (v0.9.7+)" added noting that SDD-111..SDD-116 have since been authored and the PENDING_SDD markers above are retained for historical context only. No code changes; all corrections are documentation-only. Recommended next agent: traceability-auditor refresh once all Wave 3 amendments have landed. |
| Version | Date | Description |
|---|---|---|
| 0.9.13 | 2026-05-18 | Documentation-cleanup amendment pass on SDD-105, SDD-118, SDD-119 — no behavioural design content changed; corrections only. (1) SDD-105 Back-fill notes corrected: the loader (System.loadLibrary) lives in ChanoraApplication.kt, not MainActivity.kt — back-fill text now cites ChanoraApplication.kt for System.loadLibrary and MainActivity.kt for initChanoraContext invocation, resolving the contradiction between the Software-unit field and the Back-fill field. (2) SDD-105 software-unit list extended to include crates/chanora_bridge/build.rs (which emits cargo:rustc-link-lib=dylib=c++_shared so that DT_NEEDED libc++_shared.so is baked into libchanora_bridge.so). (3) SDD-118 item 6 extended (new sub-paragraph "Item 6 (extended)") to mandate co-staging libc++_shared.so from the NDK sysroot into jniLibs/<abi>/ alongside libchanora_bridge.so, with the explicit NDK sysroot source-path pattern $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/<sysroot_triple>/libc++_shared.so and the sysroot-triple mapping (arm64-v8a → aarch64-linux-android, armeabi-v7a → arm-linux-androideabi — NOT armv7-linux-androideabi, x86_64 → x86_64-linux-android); rationale traces to SDD-105 load-order preload, SDD-111 oboe-rs C++ dependency, and SDD-118 item 10 release-inspection assertion (which shall include libc++_shared.so). (4) SDD-105 Cross-trace field added linking to SDD-118 item 6 (extended) for the runtime co-staging and to crates/chanora_bridge/build.rs for the DT_NEEDED emission. (5) SDD-119 item 4 doc-drift TODO resolved with Option α (drop the iOS podspec simulator comment) for P0 minimalism; aarch64-apple-ios-sim is explicitly deferred to a future P1 SDD revision. Strict layered sourcing preserved (SDD -> SAD only). Items deferred from this pass: macOS / iOS deployment-target single-source-of-truth (SDD-119 item 3); optional SysRS-290 anchor work; abiFilters DEC entry (routed to traceability-auditor + governance, not SDD). |
Baseline Candidate 0.9.12 Update
| Version | Date | Description |
|---|---|---|
| 0.9.12 | 2026-05-18 | Surgical traceability correction on SDD-119 (iOSAndMacOSBridgeBuildAutomation) Source SAD field. Prior allocation sourced the macOS half by parallelism from SAD-061 (iOS runtime) / SAD-062 (App Store / packaging) because no dedicated macOS-runtime SAD anchor existed at SDD-119 authoring time. SAD v0.9.7 closed that gap by authoring SAD-087 (macOS runtime baseline — 10.15 deployment target isolation in apps/chanora_flutter/macos/chanora_bridge.podspec, universal-binary arm64 + x86_64 lipo packaging, .framework Versions/A layout, hand-rolled CocoaPods podspec automation), parallel to SAD-061 (iOS runtime) and SAD-063 (Android runtime). This revision retargets SDD-119's Source SAD allocation to: iOS half = SAD-061 + SAD-062; macOS half = SAD-087 as the primary anchor. §11 coverage matrix row updated from SAD-061, SAD-062 to SAD-061, SAD-062, SAD-087 with explicit iOS-vs-macOS attribution narrative. No behavioural content of SDD-119 was modified; only the Source SAD allocation and the coverage matrix. SDD-119 remains a back-fill unit creating no new implementation work. Strict layered sourcing preserved (SDD -> SAD only). |
Baseline Candidate 0.9.11 Update
| Version | Date | Description |
|---|---|---|
| 0.9.11 | 2026-05-18 | Surgical traceability correction on SDD-110 (AndroidPttCapability) Source SAD field. Prior value SAD-077 was incorrect (SAD-077 is PttSanitizer, a diagnostics-log-sink decorator — wrong scope). The traceability-auditor flagged that an earlier draft had also cited SAD-086 (Android voice foreground service — also wrong scope). Re-anchored to SAD-071 (PTT-backend trait isolation + factory selection + published capability level — the architectural concept SDD-110 specialises on Android by pinning to FocusedPttBackend and L0Focused) and SAD-076 (BridgeEvent::PttCapability { level, backend_id } — the capability-event surface feeding the Android-specific capability badge text per SDD-091). §11 coverage matrix updated: SDD-110 removed from the SAD-063/081/084/085/086 row (now SDD-105–SDD-109 only) and added under a new SAD-071 / SAD-076 row. No behavioural content of SDD-110 was modified; only the Source SAD field and the coverage matrix. Strict layered sourcing preserved (SDD -> SAD only). |
Baseline Candidate 0.9.10 Update
| Version | Date | Description |
|---|---|---|
| 0.9.10 | 2026-05-18 | Added chanora_bridge Rust cdylib build-automation detailed design SDD-118 and SDD-119 (SDD-117 left reserved-but-unauthored for the deferred ios_voice_unit trait back-fill noted by SDD-111). SDD-118 (AndroidBridgeBuildAutomation — Gradle task set in apps/chanora_flutter/android/app/build.gradle.kts driving a single cargo-ndk -t arm64-v8a -t armeabi-v7a -t x86_64 invocation; cargo ndk --platform $minSdk sourced from the chanora.android.minSdk Gradle property per SDD-073 default 28 / DEC-004; ABI set derived from android.defaultConfig.ndk.abiFilters so the two lists stay in sync; profile mapping Gradle-debug→cargo build / Gradle-release→cargo build --release — deliberate divergence from the iOS / macOS always---release pattern, with the release-build safety property preserved by the item 10 release-inspection assertion; env LIBOPUS_STATIC=1 LIBOPUS_NO_PKG=1 CMAKE_POLICY_VERSION_MINIMUM=3.5 plus ANDROID_NDK_HOME propagation; per-ABI plain copy to apps/chanora_flutter/android/app/src/main/jniLibs/<abi>/libchanora_bridge.so with the explicit Rust-triple ↔ Android-ABI mapping; task-graph wiring via tasks.matching { ... endsWith("JniLibFolders") }.configureEach { dependsOn(...) } rather than preBuild.dependsOn; Gradle inputs/outputs declarations for up-to-date semantics; preflight that fails loudly with cargo install cargo-ndk / rustup target add ... remediation rather than auto-installing; release-inspection assertion that each .so exists and is >1 KiB before bundleRelease / bundleDebug; target/ lives at repo root and is OUTSIDE gradle clean scope; sourced from SAD-063 with cross-trace to SDD-073 / SDD-105 / SDD-109). SDD-119 (iOSAndMacOSBridgeBuildAutomation — back-fill of the existing CocoaPods-podspec automation under apps/chanora_flutter/ios/chanora_bridge.podspec and apps/chanora_flutter/macos/chanora_bridge.podspec; dual-hook prepare_command + script_phase :before_compile; iOS single-arch aarch64-apple-ios flat framework with hand-written Info.plist and install_name_tool -id @rpath/chanora_bridge.framework/chanora_bridge; macOS dual-arch aarch64-apple-darwin + x86_64-apple-darwin joined by lipo -create in a Versions/A framework layout with install_name_tool -id @rpath/chanora_bridge.framework/Versions/A/chanora_bridge; always --release; iOS env IPHONEOS_DEPLOYMENT_TARGET=13.0 CMAKE_POLICY_VERSION_MINIMUM=3.5 CMAKE_OSX_DEPLOYMENT_TARGET=13.0, macOS env MACOSX_DEPLOYMENT_TARGET=10.15 LIBOPUS_STATIC=1 LIBOPUS_NO_PKG=1 CMAKE_POLICY_VERSION_MINIMUM=3.5; CocoaPods vendored_frameworks integration; cargo incremental + per-platform timestamp guard caching; sourced from SAD-061 / SAD-062 with macOS-runtime SAD anchor flagged as a follow-up; known doc-drift defect recorded: iOS podspec comment references aarch64-apple-ios-sim but the script does not actually build the simulator slice — recorded as a TODO follow-up, NOT fixed in this SDD task; deployment-target single-source-of-truth improvement recorded as open follow-up). Updated §11 SAD-to-SDD coverage matrix to list SDD-118 and SDD-119 with their respective SAD anchors. Strict layered sourcing preserved (SDD -> SAD only). |
Baseline Candidate 0.9.16 Update
| Version | Date | Description |
|---|---|---|
| 0.9.16 | 2026-05-18 | SDD-120 amendment: clarify the post-processor binary placement at crates/chanora_audio/examples/ rather than benches/ or src/bin/. Rationale: Cargo's dependency resolver only routes [dev-dependencies] to [[test]], [[bench]], and [[example]] targets; src/bin/ placement would have forced serde_json and other dev-only crates into production builds. Reflects commit 3a7750a discovery. No semantic change to SDD-120 — same harness, same metrics, same workflows, same out-of-scope deferrals; only the path conventions and invocation flags (--example not --bin) corrected. Edits scoped to SDD-120: (1) "Allocated to" line gains crates/chanora_audio/examples/ alongside benches/; (2) "Software units" list relocates emit_baseline.rs and compare_baseline.rs from benches/ to examples/; (3) §1 item 4 reworded from [[bin]] entries pointing into benches/ to [[example]] entries (auto-discovered under examples/) with cargo run --example invocation pattern; (4) §2 gains a new item 5 stating the examples/ rationale and the dev-only isolation property; (5) §5 item 1 path updated and invocation-flag note added; (6) §5 item 4 rationale clause updated to reference the [dev-dependencies]-to-examples routing; (7) §6 step 6 and step 8 invocation flags changed --bin → --example; (8) §7 step 5 invocation flag changed --bin → --example; (9) §10 item 1 release-artifact-isolation bullet extended with an explicit note that the examples/ placement is the Cargo-design mechanism enforcing the exclusion of [dev-dependencies] (including serde_json, criterion, dhat) from cargo build --release and from flutter build apk/aab/ipa --release. §11 verification matrix unchanged. Coverage matrix row for SAD-088..SAD-091 unchanged. The SDD-120 spec is otherwise byte-identical to v0.9.15. |
Baseline Candidate 0.9.17 Update
| Version | Date | Description |
|---|---|---|
| 0.9.17 | 2026-05-18 | SDD-119 amendment: macOS deployment-target single-source-of-truth (open follow-up flagged at SDD-119 item 3 since v0.9.10 and explicitly deferred at v0.9.13) RESOLVED for the macOS half. Selected Option B (Ruby constant) over Option A (.xcconfig) and Option C (versioned text file); rationale recorded inline at the rewritten item 3. New SoT file apps/chanora_flutter/macos/macos_deployment_target.rb declares a single frozen Ruby constant MACOS_BRIDGE_DEPLOYMENT_TARGET = '10.15'.freeze; apps/chanora_flutter/macos/Podfile and apps/chanora_flutter/macos/chanora_bridge.podspec require_relative the file and consume the constant at seven previously-literal sites (1 Podfile platform :osx, ...; 1 podspec s.platform :osx, ...; 4 podspec MACOSX_DEPLOYMENT_TARGET=... env exports inside the prepare_command and script_phase shell heredocs, reached via outer-Ruby-heredoc #{...} interpolation; 2 podspec embedded-framework Info.plist <key>MinimumOSVersion</key> entries, reached the same way). The pbxproj MACOSX_DEPLOYMENT_TARGET = 10.15 occurrences at Runner.xcodeproj/project.pbxproj lines 565 / 666 / 717 are explicitly OUT OF SCOPE for this SoT and were NOT touched: those three lines belong to the PBXProject "Runner" build-configuration list (project-default config emitted by Flutter's template) and are independently overridden to 11.0 at the Runner PBXNativeTarget level — they are the Runner app's project-default floor, not the bridge cdylib's floor, and the matching value is coincidental and intentionally allowed to diverge in the future. The iOS half of the SoT follow-up (IPHONEOS_DEPLOYMENT_TARGET=13.0 hard-coded in the iOS podspec) remains an open follow-up and is NOT addressed by this amendment — the iOS Podfile / podspec pair sits in a separate directory under a parallel Ruby scope and warrants its own future revision. SDD-119 item 3 fully rewritten in place (no longer flagged as open follow-up — now records the Option-B realization, the consumer-site enumeration, the option-rejection rationale, and the explicit OUT-OF-SCOPE note for the pbxproj project-default lines). SDD-119 "Notes" bullet at line 1703 updated to reflect that item 3 is RESOLVED for macOS while preserving the iOS-half open-follow-up status. Source SAD: SAD-087(a) (which already named the podspec as the "single macOS build-configuration location") — this amendment realizes that mandate by collapsing the seven intra-cluster literal occurrences to one. Files modified: apps/chanora_flutter/macos/macos_deployment_target.rb (new, ~22 lines including documentation header), apps/chanora_flutter/macos/Podfile (+1 line require_relative, 1 line edited), apps/chanora_flutter/macos/chanora_bridge.podspec (1 line require_relative insertion + 7 literal-to-interpolation edits), docs/architecture/sdd.md (this entry + item-3 rewrite + Notes-bullet edit). No behavioural design content changed beyond consolidating an already-implemented value to a single declaration site. The pre-staged apps/chanora_flutter/macos/Frameworks/chanora_bridge.framework/Versions/A/Resources/Info.plist artifact still contains the literal 10.15 value but is regenerated on every pod install / Xcode build by the podspec's prepare_command / script_phase from the new SoT, so it will refresh automatically on the next macOS build. Strict layered sourcing preserved (SDD -> SAD only). Ruby syntax of the modified files NOT executable-verified on this host (no ruby available in the build container); static review confirmed (a) the require_relative 'macos_deployment_target' filename is correct (the new file is named macos_deployment_target.rb and require_relative strips the .rb extension), (b) the constant is referenced after the require in every consumer, (c) the outer Ruby heredocs (<<-SCRIPT, unquoted delimiter) support #{...} interpolation, (d) the inner shell <<PLIST heredoc inside those Ruby heredocs is interpolated at outer-Ruby-heredoc evaluation time before the shell ever sees the string, so the embedded Info.plist MinimumOSVersion interpolations are well-formed. Recommended next step: orchestrator push, then on next macOS developer machine pod install to refresh the staged framework Info.plist. |