Move the change folder to changes/archive/2026-07-11-bt-device-identity with the verified artifact set (verdict: pass with warnings, 0 critical, 102/102 targeted tests) and create the bt-device-identity capability spec under openspec/specs/. Phase 7 on-device QA remains the pending human gate before release.
14 KiB
Archive Report: Canonical Bluetooth Device Identity
Change: bt-device-identity
Archived: 2026-07-11
Status: ARCHIVED AND CLOSED
Archive Path: openspec/changes/archive/2026-07-11-bt-device-identity/
Spec Merged To: openspec/specs/bt-device-identity/spec.md (new capability domain)
Executive Summary
The bt-device-identity change is now archived with a PASS WITH WARNINGS verification verdict. All code-gated implementation work is complete across Phases 1–5 (41 of 57 tasks done, 6 N/A by design). Phase 7 (manual/on-device QA, 10 tasks, human sign-off gate) remains pending as the mandatory pre-shipping gate. The change restores stable Bluetooth device identity by acquiring BLUETOOTH_CONNECT at point-of-intent, guarding against the OS placeholder MAC, caching per-device names, and migrating corrupted placeholder-keyed entries. All test suites pass (102/102 tests), flutter analyze reports 0 issues, and the re-verify phase closed the single CRITICAL finding (CRITICAL-1: untested "rename survives re-pair" scenario — now covered by new task 3.10 composed regression test that passes against existing code).
Artifact Traceability
| Artifact | Engram Observation ID | Location | Notes |
|---|---|---|---|
| Proposal | #2299 | sdd/bt-device-identity/proposal | Root cause analysis, scope, approach, risks |
| Spec | #2304 | sdd/bt-device-identity/spec; merged to openspec/specs/bt-device-identity/spec.md | 7 requirement groups, 21 scenarios; on-disk version (openspec/changes/bt-device-identity/spec.md) reconciled and merged to new domain |
| Design | #2305 | sdd/bt-device-identity/design | 6 ADRs, technical approach, file changes, testing strategy |
| Explore | (embedded) | sdd/bt-device-identity/proposal (no separate observe) | Root cause evidence, bug analysis |
| Tasks | #2314 | sdd/bt-device-identity/tasks | 57 checkboxes: 41 complete (Phase 1–5), 6 N/A (Phase 6), 10 pending (Phase 7 human QA) |
| Apply-Progress | #2328 | sdd/bt-device-identity/apply-progress | Batches 1/2/2b complete (Phases 1–5); Batch 3 (Phase 7) pending; full merged evidence of all 41 tasks |
| Verify-Report | #2329 | sdd/bt-device-identity/verify-report | Initial FAIL (CRITICAL-1 untested scenario); re-verify PASS WITH WARNINGS (CRITICAL-1 closed via task 3.10); 15/21 scenarios COMPLIANT, 4/21 DEFERRED (manual QA), 2/21 N/A (l10n justified), 0/21 still UNTESTED |
Implementation Completion
Phases 1–5: Code-Gated Work — COMPLETE
| Phase | Description | Tasks | Status | Evidence |
|---|---|---|---|---|
| 1 | Kotlin permission plumbing (manifest, placeholder guard, composite fallback, permission request, re-emit) | 1.1–1.8 (8) | 8/8 [x] | Code-inspection verified; flutter analyze 0 issues |
| 2 | Dart permission contract (abstract method, fakes, interface tests) | 2.1–2.5 (5) | 5/5 [x] | All 3 fake implementers updated; contract tests pass |
| 3 | Dart platform-name cache + duplicate-entry guard (in-memory map, composites don't auto-create, regression tests) | 3.1–3.10 (10) | 10/10 [x] | 49/49 estado_ecualizador_test.dart pass (incl. post-verify task 3.10) |
| 4 | Dart display fix + permission trigger (call sites updated, permission fires on toggle-ON) | 4.1–4.8 (8) | 8/8 [x] | 11/11 pantalla_ajustes_test.dart pass; provider wiring resolved via passthrough |
| 5 | Dart one-time migration purge (guarded flag, exact-literal placeholder removal, idempotent) | 5.1–5.10 (10) | 10/10 [x] | 20/20 servicio_ecualizador_test.dart pass; migration isolation verified |
Phase 6: Localization — N/A (By Design)
| Phase | Description | Tasks | Status | Rationale |
|---|---|---|---|---|
| 6 | Localization (l10n keys for all 13 locales IF UI copy is shown) | 6.1–6.6 (6) | N/A | No in-app rationale sheet or migration-notice UI exists; OS BLUETOOTH_CONNECT dialog only; migration purge entirely silent; Phase 6 condition ("IF UI copy is shown") not met |
Phase 7: Manual/On-Device QA — PENDING (Human Gate)
| Phase | Description | Tasks | Status | Notes |
|---|---|---|---|---|
| 7 | Manual/on-device QA (permission dialog flow, real MAC after grant, rename survives re-pair, no duplicates, migration idempotency, locale rendering if Phase 6 ships) | 7.1–7.10 (10) | 0/10 [ ] | Mandatory human sign-off gate; requires real/emulated Android 12+ device; Kotlin permission path not unit-testable (code-inspected only); these 10 items are the ONLY remaining work for this change |
Task Summary:
- 41 tasks complete (
[x]) across Phases 1–5 - 6 tasks N/A (
[ ]with justification) in Phase 6 - 10 tasks pending (
[ ]) in Phase 7 - Total: 57 checkboxes, 0 unaccounted
Test Results
Unit/Widget Tests
| Suite | Tests | Result | Notes |
|---|---|---|---|
estado_ecualizador_test.dart |
49/49 | PASS | +1 from task 3.10 (was 48/48); Phase D regression group (D.1-D.5) confirmed green |
pantalla_ajustes_test.dart |
11/11 | PASS | Display fix + permission trigger scenarios |
servicio_ecualizador_test.dart |
20/20 | PASS | Migration tests including matrix-key and near-miss scenarios |
servicio_dispositivo_audio_real_test.dart |
10/10 | PASS | Permission contract (MethodChannel invokeMethod) |
servicio_dispositivo_audio_test.dart |
7/7 | PASS | Abstract contract tests |
servicio_dispositivo_audio_toggle_test.dart |
5/5 | PASS | Ripple-fix (NullServicioDispositivoAudio) included |
| Total | 102/102 | PASS | — |
Static Analysis
| Tool | Result | Coverage |
|---|---|---|
flutter analyze |
0 issues | Whole project |
rg (placeholder string checks) |
No dead code (00:00:00:00:00:00 removed completely) |
Kotlin, Dart |
Code Coverage
- Exact-line coverage metrics not collected (per verification scope).
- Scenario-level mapping: 15/21 spec scenarios COMPLIANT (Dart-testable, all pass); 4/21 DEFERRED (manual QA); 2/21 N/A (l10n justified).
- CRITICAL-1 closed by new task 3.10 composed test (spec.md L95-100, rename survives re-pair).
Verification Summary
Verdict Evolution
| Pass | Result | Finding |
|---|---|---|
| Initial (2026-07-11 00:44) | FAIL | CRITICAL-1: Untested Dart-testable scenario "rename persists after re-pair" (spec.md L95-100) — constituent behaviors tested individually (reconnect-dedup D.4, rename-priority 4.4), composition untested |
| Re-verify (2026-07-11 post-verify) | PASS WITH WARNINGS | CRITICAL-1 CLOSED: task 3.10 adds composed regression test; passes against existing code (coverage gap confirmed, not functional bug). WARNING-1 spec wording reconciled (composite shape documented as bt_a2dp:name:<name> per ADR-3). WARNING-2 cosmetic (arithmetic drift in apply-progress narrative, no code impact) accepted as-is. |
Finding Breakdown
| Severity | Count | Status |
|---|---|---|
| CRITICAL | 1 | CLOSED (task 3.10 composed regression test) |
| WARNING | 2 | CLOSED (WARNING-1, re-verify wording fix); OPEN/ACCEPTED (WARNING-2, cosmetic) |
| SUGGESTION | 3 | OPEN (non-blocking: wording nuance SUGGESTION-1, mock call-count pattern SUGGESTION-2, Engram housekeeping SUGGESTION-3) |
Compliance Matrix Snapshot
| Req. | Scenarios | Compliant | Deferred | N/A | Untested |
|---|---|---|---|---|---|
| BLUETOOTH_CONNECT at point-of-intent | 2 | 0 | 2 | 0 | 0 |
| Placeholder MAC guarded | 5 | 4 | 1 | 0 | 0 |
| Platform-name cache feeds display | 3 | 3 | 0 | 0 | 0 |
| Rename survives re-pair | 2 | 1 (via 3.10) | 1 | 0 | 0 |
| No duplicate entries on transient | 2 | 2 | 0 | 0 | 0 |
| One-time guarded migration | 6 | 6 | 0 | 0 | 0 |
| Localization (conditional on UI copy) | 2 | 0 | 0 | 2 | 0 |
| Total | 21 | 15 | 4 | 2 | 0 |
File Changes Summary
Estimated Changed Lines: ~450 (Kotlin ~90, Dart prod ~140, Dart tests ~58 post-verify, manifest ~1)
400-Line Budget Risk: High (mitigated by chained PR strategy; PR 1: Kotlin, PR 2: Dart, PR 3: l10n + manual QA)
Build Execution: Not run (per verification scope — flutter build explicitly excluded)
Kotlin (Code-Inspection Verified)
android/app/src/main/AndroidManifest.xml: +1 permission declarationandroid/app/.../MainActivity.kt: ~90 lines (permission request code, placeholder guard, composite fallback, re-emit logic, doc comment updates)
Dart Production
lib/servicios/servicio_dispositivo_audio.dart: Abstract + real impl ofsolicitarPermisoBluetooth()lib/estado/estado_ecualizador.dart: In-memory_nombresPlataformacache,nombrePlataforma()getter, duplicate-entry guard updatelib/pantallas/pantalla_ajustes.dart: TwonombreVisible()call sites updated, permission trigger addedlib/servicios/servicio_ecualizador.dart: Migration constants andmigrarClavesPlaceholder()function
Dart Tests
test/estado/estado_ecualizador_test.dart: +58 lines (task 3.10 post-verify regression test)test/pantallas/pantalla_ajustes_test.dart: Display fix + permission trigger teststest/servicios/servicio_ecualizador_test.dart: Migration test suite (exact placeholder, matrix suffix, near-miss, idempotency)test/servicios/servicio_dispositivo_audio_real_test.dart: Permission contract teststest/helpers/fakes.dart: Fake implementers updated (ripple-fix: NullServicioDispositivoAudio also patched)
Localization
lib/l10n/*.arb(13 files): 0 changes (Phase 6 N/A — no in-app copy shipped)
Deviations from Design
Documented and Signed Off:
-
Trigger-Point Deviation (Phase 4, Task 4.7): Design specified "on opening device-management UI" (StatefulWidget on-build hook). Actual: permission fires on toggle-turn-ON tap in
_SeccionEcualizadorAvanzado(StatelessWidget). Rationale: avoids unnecessary StatelessWidget → StatefulWidget conversion; toggle-turn-ON satisfies both spec scenarios. Signed off by orchestrator. -
Provider Wiring Resolution (Phase 4, Task 4.8): Design assumed
ServicioDispositivoAudiowould be a top-level Provider. Actual: not registered inapp.dart. Solution: addedsolicitarPermisoBluetooth()passthrough onEstadoEcualizador(delegates to private field). Maintains test isolation, avoids ProviderNotFoundException.
No design deviations were introduced in Batch 2b (task 3.10 is test-only, zero production-code changes).
Risks and Mitigations
| Risk | Likelihood | Status |
|---|---|---|
| Migration deletes legitimate data | Low | Exact-literal bt_a2dp:02:00:00:00:00:00 match only; all other keys preserved; Task 5.3 near-miss test confirms 02:00:00:00:00:01 is NOT purged |
| BT permission denied → unstable ids | Med | Composite bt_a2dp:name:<productName> fallback keeps app functional; re-prompt on next settings open; no crash path |
| Colon-delimiter key safety broken | Low | Single leading bt_a2dp: segment preserved; productName colons sanitized to -; eq_presets_matriz_v1 split-on-first-: remains valid (ADR-3, multi-device-eq ADR-3 RFC4122 rationale) |
| Kotlin permission flow has no precedent | Med | Copied verbatim from existing requestPostNotificationsPermission pattern (L306-318 struct); request codes distinct (4821/4822/4823); human verification in Phase 7 |
| No Kotlin test harness | Med | Code-inspection verified against proven pattern; Phase 7 manual QA is the runtime gate |
Next Steps
Mandatory Before Shipping
-
Phase 7 Manual/On-Device QA (pending, 10 tasks): Requires real or emulated Android 12+ device.
- Permission dialog flow (grant/deny, no dialog at launch)
- Real MAC after grant (7.3)
- Rename survives re-pair without duplicates (7.5, the original bug report symptom)
- Migration purge on affected install + idempotency (7.7–7.8)
- Locale rendering if Phase 6 ships (7.9; N/A this change)
- Sign-off (7.10) before merge
-
Chain Strategy (pending, from tasks.md): Orchestrator should confirm/name the chain strategy if chained PRs are used:
stacked-to-main: Each PR merges to main in order (fast, fix on the go)feature-branch-chain: Tracker branch accumulates; PR #1→PR 2→PR 3 (rollback control)
Follow-Up Opportunities (Out of Scope)
- WARNING-1 (now closed): Spec wording reconciled during re-verify. Composite shape (
bt_a2dp:name:<name>) is now documented in both spec and design. - WARNING-2 (cosmetic, accepted): Arithmetic drift in apply-progress narrative (pre-existing test counts off by 3/2 respectively) — no functional impact. Accepted as-is per task brief.
- SUGGESTION-1/2/3 (non-blocking): Wording nuance (composite not single-entry), mock-call-count pattern, Engram conflict housekeeping — all flagged for future review.
- Broader transient-churn dedup (design Open Question): Deferred; acceptable now that MAC is canonical. Revisit if duplicates persist post-fix.
Archive Integrity
Moved From: openspec/changes/bt-device-identity/ → openspec/changes/archive/2026-07-11-bt-device-identity/
Verified Empty: Source folder no longer exists under changes/ (not a residual copy, true move)
Spec Merged: openspec/specs/bt-device-identity/spec.md created as new domain (not merged into multi-device-eq, which focuses on EQ preset resolution; bt-device-identity is about device identity + persistence)
Artifacts Preserved: All 6 openspec files (design, explore, proposal, spec, tasks, verify-report) + new state.yaml and this archive-report
Sign-Off
Archive Ready For: Production shipping (pending Phase 7 human QA sign-off)
Blocking Issues: None; Phase 7 is a known human gate, not a blocker
Recommendation: Proceed to Phase 7 manual QA; no code changes expected; expect 10 pass/fail items to be recorded against real device access
Appendix: Observation IDs for Traceability
- #2299: sdd/bt-device-identity/proposal (root cause, scope, approach)
- #2304: sdd/bt-device-identity/spec (21 scenarios, 7 requirement groups)
- #2305: sdd/bt-device-identity/design (6 ADRs, technical approach, testing strategy)
- #2314: sdd/bt-device-identity/tasks (57 checkboxes, 3 batches, phase status)
- #2328: sdd/bt-device-identity/apply-progress (Batches 1/2/2b merged evidence, 41 tasks complete)
- #2329: sdd/bt-device-identity/verify-report (PASS WITH WARNINGS, re-verify, CRITICAL-1 closed)
All artifacts archived and accessible at openspec/changes/archive/2026-07-11-bt-device-identity/ with full change history preserved for audit trail.