Commit Graph
10 Commits
Author SHA1 Message Date
Edison Jwa 1813bbaa0c fix(ci): keep benchmark advisory non-blocking 2026-06-02 20:01:58 +09:00
Edison Jwa da174806ac fix(ci): install SDL2 for Rust tests 2026-06-02 19:52:07 +09:00
Edison Jwa 966afd2b53 ci: fix Apple CoreML VAD checks 2026-06-02 19:33:02 +09:00
Edison Jwa fe6e07353e chore: restore product scaffold to rollback baseline 2026-05-29 14:02:04 +09:00
Edison Jwa 5c8c6df16b Make beta release artifacts reproducible on Linux and Android
Constraint: SRS-118 and SRS-119 require a Linux release package and an Android release AAB, and the current workspace also needs the sibling oboe-rs checkout for Cargo manifest loading.
Rejected: Keep release packaging as ad-hoc local knowledge | CI and contributors would still miss the required artifacts and hit the missing oboe-rs prerequisite.
Confidence: medium
Scope-risk: moderate
Directive: If the oboe-rs fork path changes or is vendored, update the helper scripts and workflow checkout steps together.
Tested: bash -n tools/build-linux-deb.sh tools/build-android-aab.sh; python3 YAML parse for .github/workflows/ci.yml, .github/workflows/bench-advisory.yml, .github/workflows/bench-baseline-update.yml; git diff --check
Not-tested: End-to-end flutter build linux --release; end-to-end flutter build appbundle --release; GitHub Actions runtime execution
2026-05-25 03:21:21 +09:00
Edison Jwa 6af4ecab0f feat(voice): add iOS VAD runtime support 2026-05-21 20:51:45 +09:00
EdisonJwa 7188a5a69d feat(perf,benchmark-infra): criterion bench harness + advisory CI workflows (SDD-120)
Implementation of SDD-120 §1-§8:

Bench harness (crates/chanora_audio/benches/):
- common.rs: deterministic synthetic audio (440 Hz sine, no RNG).
- realtime_capture.rs: bench_capture_alloc_count (dhat) +
  bench_capture_callback_wall_clock (criterion).
- opus_codec.rs: bench_opus_encode_latency + bench_opus_decode_latency
  (direct audiopus, not AudioHandler — SDD-120 §3 item 4).
- resampler.rs: bench_resampler_throughput across 44.1->48 /
  16->48 / 48->48 passthrough.

CI tooling (crates/chanora_audio/examples/):
- emit_baseline.rs: aggregates criterion estimates.json outputs
  into the SRS-217 baseline schema.
- compare_baseline.rs: applies SRS-219 tolerance, renders markdown
  table with 🟢/🟡/🔴 markers + yellow simpler-form realization per
  SDD-120 §8.

  Deviation from SDD-120 §2 / §5 / §7 placement: these tools live
  under examples/, not benches/ or src/bin/. Rationale: they must
  consume serde_json (a dev-only dep — production builds must not
  pull it). Cargo only resolves dev-dependencies for [[test]],
  [[bench]], and [[example]] targets; [[bin]] targets under
  src/bin/ see only regular [dependencies]. examples/ keeps the
  binaries out of the production dep tree while still giving them
  cargo run --example invocation. An SDD-120 amendment should
  reflect this.

Workflows (.github/workflows/):
- bench-advisory.yml: PR + push triggers; runs benches; posts a
  sticky PR comment via actions/github-script@v7; job status is
  always success (SRS-218 clause 4 — non-blocking).
- bench-baseline-update.yml: workflow_dispatch only; runs benches;
  opens PR via peter-evans/create-pull-request@v6 (sole writer of
  the SAD-089 baseline JSON).

Cargo.toml additions ([dev-dependencies] only — verified excluded
from --release builds): criterion 0.5, dhat 0.3, serde_json 1.

Source-code seam: minimal pub-but-#[doc(hidden)] bench_seam module
in chanora_audio (engine.rs + lib.rs re-export) so the criterion
bench harness can construct a CaptureState and drive
CaptureState::ingest without re-implementing the engine (SDD-120
§3). Non-iOS targets only — CaptureState itself is iOS-gated.

Initial baseline seed: crates/chanora_audio/benches/baselines/
x86_64-unknown-linux-gnu.json = {}. compare_baseline handles the
missing-baseline case gracefully and emits a 'no red markers'
report; the first manual dispatch of bench-baseline-update.yml
after merge establishes the real values.

Out of scope per SDD-120 §10: production telemetry export,
build-failing hard CI gate, multi-host benchmarking, IDE
integration, Dart-side bridge round-trip bench.

Verification:
- cargo check --workspace --all-targets: PASS.
- cargo bench --bench realtime_capture --no-run: PASS.
- cargo bench --bench opus_codec --no-run: PASS.
- cargo bench --bench resampler --no-run: PASS.
- cargo build --example emit_baseline --example compare_baseline
  -p chanora_audio: PASS.
- cargo test --workspace: 106 passed, 0 failed, 3 ignored — no
  regression from prior count.
2026-05-18 13:52:15 +08:00
EdisonJwa b932dc1405 feat(legal): land cargo-about + cargo-deny + Flutter license inventory
Closes engineering deliverables 1–3 from the open-work table in
`docs/governance/legal-review-readiness.md` so the DEC-012 legal
review can actually run. With this commit, the only remaining
engineering item blocking sign-off is signed Windows / macOS / iOS
build artefacts, deferrable per the DEC-002 staged release plan.

Tooling
-------

* `about.toml` + `about.hbs` + `about-md.hbs` configure cargo-about
  with the DEC-020 license posture and the five-target matrix
  (Linux, Android, Windows, macOS, iOS). One per-crate clarification
  for `allo-isolate` (`flutter_rust_bridge` transitive that ships
  Apache-2.0 via `license-file` rather than an SPDX `license`
  field). `cargo about generate` runs with zero warnings.
* `deny.toml` mirrors the cargo-about allow-list and adds minimal
  bans / sources / advisories config. `cargo deny check` reports
  `advisories ok, bans ok, licenses ok, sources ok` for the
  workspace; multiple-versions of `windows_x86_64_msvc` produce
  advisory `warn` (no fail) because three windows-targets versions
  reach the graph via `jni`, `cpal`, and `keyring` respectively.
* `tools/dump_flutter_licenses.sh` + `tools/dump_flutter_licenses.dart`
  walk `apps/chanora_flutter/pubspec.lock`, resolve each dependency
  to its local pub-cache directory, read the LICENSE file, and emit
  `docs/security/flutter-license-inventory.md`. SDK-sourced
  packages (`flutter`, `flutter_localizations`, `flutter_test`,
  `flutter_web_plugins`, `sky_engine`) resolve to the Flutter
  framework BSD-3-Clause LICENSE under `$FLUTTER_ROOT` (or
  `$HOME/sdks/flutter`).

Artefacts
---------

* `docs/security/license-inventory.md` — 364 transitive Rust
  crates with full license texts. Apache-2.0 (276), MIT (55),
  Unicode-3.0 (19), BSD-3-Clause (7), ISC (7). Zero copyleft.
* `docs/security/license-inventory.html` — same data rendered as
  styled HTML for reviewer convenience.
* `docs/security/flutter-license-inventory.md` — 94 Dart / Flutter
  packages with their LICENSE texts. Zero packages without a
  resolvable LICENSE in this RC.

CI
--

* New `supply-chain` job runs `cargo deny check --workspace
  --all-features` via `EmbarkStudios/cargo-deny-action@v2`. Fails
  the build on any GPL / LGPL / AGPL / commercial-source license
  surfacing transitively.
* New `license-inventory` job installs `cargo-about --features cli`
  and regenerates `docs/security/license-inventory.md`; diffs
  against the committed copy and fails on drift. Forces
  contributors who touch the Cargo.lock to refresh the inventory.
* New `flutter-license-inventory` job runs
  `tools/dump_flutter_licenses.sh` against the just-resolved pub
  cache; same diff-on-drift semantics.

Governance
----------

* `docs/governance/legal-review-readiness.md` §5 cross-links the
  three new artefacts in a "Reviewer artefacts" subsection.
* The open-work table at the bottom of the doc is rewritten as a
  status grid: items 1–3 now read **Done**; item 4 (signed iOS /
  macOS builds) remains the only open engineering blocker, with a
  pointer back to `staged-release-plan.md`.

Verification
------------

* `CHANORA_DISABLE_KEYRING=1 cargo test --workspace`: all 49 unit
  + integration tests green (unchanged from v1.0.0-rc.1).
* `cargo deny check`: advisories ok, bans ok, licenses ok,
  sources ok.
* `cargo about generate --output-file …`: zero warnings.
* `tools/dump_flutter_licenses.sh`: 94 packages, 0 without LICENSE.
* `flutter analyze`: clean.

No code changes touch the runtime; this is governance-tooling only.
2026-05-15 14:00:16 +08:00
EdisonJwa 50768a8f48 feat(mvp): v1.0.0-rc.1 — keyring-backed DEK, encrypted bookmarks, MVP release-gate docs
Closes the v0.4 dual-file weakness in identity-at-rest and turns the
release into an MVP public release candidate. The remaining work
before `v1.0.0` is DEC-012 legal sign-off — see
`docs/governance/legal-review-readiness.md` — and the staged
platform promotions in `docs/governance/staged-release-plan.md`.
No decision rows in `product-decision-register.md` change; the
register's change-history advances to 0.9.8.

`chanora_storage`
-----------------

* New public `Crypto` trait + `IdentityFileStore::crypto()` give
  callers an encrypt / decrypt pair anchored on the per-install
  32-byte DEK without exposing the key material.
* `IdentityFileStore` keyring-first DEK retrieval (Linux Secret
  Service via D-Bus, macOS Keychain, Windows Credential Manager,
  iOS Keychain via the `keyring` crate). Pre-existing
  `identity.dek` files are opportunistically migrated into the
  keyring on first run; the on-disk DEK copy is removed once the
  keyring acknowledges. `CHANORA_DISABLE_KEYRING=1` forces the
  file-fallback path for tests and headless / CI hosts where a
  real keyring call would prompt the user or block on a missing
  D-Bus session.
* `BookmarkRepository::with_crypto(dir, crypto)` encrypts the
  server password into a new `password_blob` BLOB column under
  the same per-install DEK. Schema v2 migration is idempotent —
  legacy v0.4 rows with a plain `password TEXT` are read
  transparently and lifted into `password_blob` on the next
  `update()`. `BookmarkRepository::new` (no crypto) is preserved
  for tests and as a documented fallback when the DEK is
  unreachable.
* Storage tests rise from 8 to 10: encrypted bookmark password
  round-trip + legacy-plaintext-bookmark upgrade.

`chanora_core`
--------------

* `ChanoraSession::init_storage(dir)` wires the bookmark
  repository with crypto by default. On any crypto-derivation
  failure it falls back to the plain-password repository and
  logs the gap — better than hard-failing init.
* `supervisor_loop` now tracks a 64-bit `snapshot_signature` over
  channels (id + parent + order + name) and clients (id + channel
  + name) instead of the old `(channel_count, client_count)`
  tuple. Any in-channel client move, channel rename, or reorder
  now fires `SessionEvent::SnapshotChanged`. The signature sorts
  by id before hashing so it's stable under input-vector
  reordering.
* Two new unit tests cover the signature behaviour; new
  `tests/mvp_storage.rs` integration test drives
  `ChanoraSession::init_storage` end-to-end and verifies the
  bookmark `password_blob` does not contain the plaintext.
* Re-export `ChannelId` + `ClientId` from `chanora_protocol` so
  downstream callers and tests can construct DTOs directly.

Flutter
-------

* New About dialog (info icon in the AppBar) surfaces DEC-018
  (public name "Chanora"), DEC-019 (non-affiliation statement),
  and DEC-020 (Apache-2.0 OR MIT dual license). New ARB keys in
  `app_en.arb` and `app_zh.arb`: `aboutAction`, `aboutVersion`,
  `aboutNonAffiliation`, `aboutLicenseHeading`, `aboutLicenseBody`,
  `aboutThirdPartyHeading`, `aboutThirdPartyBody`.
* `pubspec.yaml` version bumps to `1.0.0-rc.1+5`.

Governance
----------

* `docs/governance/legal-review-readiness.md` — DEC-012 handoff
  package. Enumerates trademark / non-affiliation / license-text
  / third-party-attribution / `tsclientlib`-posture / crypto-
  export / data-handling items the legal reviewer must confirm,
  and lists the concrete engineering deliverables they block on
  (`cargo about generate`, `cargo deny check licenses`,
  Flutter `LicenseRegistry` dump).
* `docs/governance/staged-release-plan.md` — DEC-002 channel
  schedule. Linux + Android sideload promote to GA on DEC-012
  sign-off; Play Store / Windows / macOS / iOS gate on per-
  platform signed-build availability. Rollback policy included.
* `product-decision-register.md` change-history advances to
  0.9.8 with a single entry summarising v0.3, v0.4, and v1.0-rc.1
  progress against DEC-001. No decision rows mutate.

Build + ops
-----------

* `NOTICE` refreshed for the MVP product-code dependency set:
  adds `chacha20poly1305`, `rand`, `zeroize`, `base64`,
  `keyring`, `connectivity_plus`, `path_provider`,
  `freezed_annotation`; drops PoC-only entries.
* `CHANGELOG.md` restructured: explicit version sections for
  v0.3.0-beta.1, v0.4.0-beta.2, v1.0.0-rc.1. Previous "Unreleased"
  contents migrated into their respective milestone sections.
* `.github/workflows/ci.yml` exports `CHANORA_DISABLE_KEYRING=1`
  for the cargo-test job — CI runners have no D-Bus session and
  the keyring crate would otherwise block.
* `run-chanora.sh` reads `CHANORA_BUNDLE_FLAVOUR` (default
  `release`) and self-copies the latest cdylib into the bundle's
  `lib/` if missing.

Verification
------------

* `cargo test --workspace` with `CHANORA_DISABLE_KEYRING=1`: all
  green (49 unit tests across the workspace; up from 36 at
  v0.4.0-beta.2).
* `cargo test -p chanora_core --release -- --ignored alpha_smoke`
  passes against the live `cn.teamspeak.app` (DNS → connect →
  snapshot → disconnect in ~2.5 s).
* `flutter analyze`: clean.
* `cargo build -p chanora_bridge --release` + `flutter build
  linux --release` produce a working Linux x86_64 bundle.

No Android live test in this commit per the user's note that the
physical device was removed; the Android arm64-v8a build path is
mechanically identical to v0.4.0-beta.2.
2026-05-15 02:24:42 +08:00
EdisonJwa 780fd7eca2 feat(beta): External Beta — passwords, channel join, mute, bookmarks, encrypted identity
The v0.3 client could only ever connect to a hardcoded default
channel with no password and offered no controls mid-call.
External Beta closes those gaps and tightens identity-at-rest.

User-facing additions
---------------------

* **Server password** on the connect form. Plumbed through
  `BridgeError`-aware `connect(host, nickname, password)`. Empty
  string means "no password" — no behaviour change for open
  servers.
* **Channel join**: tapping a row (or its login icon) in the
  channel tree issues a `client_move`. Names containing "🔒" or
  "password" prompt for a channel password first.
* **Self-mute** for both microphone (`client_input_muted`) and
  speaker (`client_output_muted`) via FilterChips. Output mute
  also flips the audio engine's local output-muted flag so
  playback silences immediately, before the server acknowledges.
* **Master output gain** slider (0–200%). Plumbed through an
  `AtomicU32` (f32 bits) on the engine that the cpal output
  callback multiplies into every sample.
* **Bookmarks**: SQLite-backed list with Save / Connect / Delete
  actions. Bookmarks persist across app restarts; tapping one
  pre-fills the form and dials immediately.

Hardening
---------

* **Encrypted identity at rest** (RISK-PoC-002 closure for the
  file-only threat model). ChaCha20-Poly1305 envelope: nonce +
  ciphertext written atomically with mode 0600; 32-byte DEK in a
  separate `identity.dek` file. Legacy plaintext identity files
  are auto-detected, read, and upgraded on the next save. Full OS-
  keyring integration is still v0.4 work — documented in the
  store's doc comment.
* **Mobile voice-comm routing**: on Android, `AudioEngine::start`
  uses JNI to set `AudioManager.setMode(MODE_IN_COMMUNICATION)`
  when `cfg.mobile_voice_preset` is true (default). This engages
  the device-side AEC/NS pipeline on most Pixel/Moto/Samsung
  hardware even though cpal still opens the AAudio default input
  preset. Full `setInputPreset(VOICE_COMMUNICATION)` switch is
  still RISK-AUDIO-MOBILE-001 (needs cpal upstream or an Oboe
  fork).
* **Log noise**: bridge default `EnvFilter` now silences
  `tsproto::resend=error` and `tsproto::packet_codec=error` so
  the redacted diagnostic export is human-readable. Still
  overridable via `RUST_LOG=...`.

Engineering
-----------

* **`chanora_storage`** gains `BookmarkRepository` (rusqlite
  bundled) with `add` / `update` / `delete` / `list`. The
  identity store now layers on `chacha20poly1305` + `rand` +
  `zeroize` for the envelope.
* **`chanora_protocol`** exposes `move_to_channel` and
  `set_muted` on `ProtocolClient`, dispatched through the
  existing `connection_task` request channel onto tsclientlib's
  generated `client.client_move(...)` and
  `state.client_update().set_input_muted/set_output_muted(...)`
  paths.
* **`chanora_core::ChanoraSession`** wires the bookmark store
  next to the identity store inside `init_storage`, and adds
  `list_bookmarks` / `add_bookmark` / `update_bookmark` /
  `delete_bookmark` / `move_to_channel` / `set_self_muted` /
  `set_output_gain`.
* **`chanora_audio::AudioEngine`** carries `output_gain` and
  `output_muted` atomics; the output callback consults both. The
  Android branch of `start()` engages MODE_IN_COMMUNICATION via
  a small JNI helper that reuses the `ndk_context` global set by
  the bridge's `android_init` hook.
* **`chanora_bridge::api`** adds `set_input_muted`,
  `set_output_muted`, `set_output_gain`, `move_to_channel`,
  `list_bookmarks`, `add_bookmark`, `update_bookmark`,
  `delete_bookmark`, and the `BridgeBookmark` DTO. FRB v2.12
  codegen regenerated.

Tests + CI
----------

* `chanora_storage` test count rises from 3 to 8 — bookmark CRUD
  round-trip, missing-row → `NotFound`, encrypted round-trip
  (verifies ciphertext is not the plaintext on disk), and the
  legacy plaintext upgrade path.
* New `.github/workflows/ci.yml`: `cargo check --workspace`,
  `cargo test --workspace --no-fail-fast`, `cargo clippy`
  (advisory), `flutter analyze`, and `flutter test` excluding
  the live-server `e2e` tag.

Live-verified on Moto G Stylus 5G against cn.teamspeak.app:
saved a bookmark, reconnected via it, joined a non-default
channel via tap, toggled both mutes, slid the volume, and the
redacted diagnostic export confirmed `AudioManager mode set to
MODE_IN_COMMUNICATION`, `client_move sent`, and `client_update
sent` lines.
2026-05-15 01:59:27 +08:00