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.
228 lines
14 KiB
Markdown
228 lines
14 KiB
Markdown
# 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 declaration
|
||
- `android/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 of `solicitarPermisoBluetooth()`
|
||
- `lib/estado/estado_ecualizador.dart`: In-memory `_nombresPlataforma` cache, `nombrePlataforma()` getter, duplicate-entry guard update
|
||
- `lib/pantallas/pantalla_ajustes.dart`: Two `nombreVisible()` call sites updated, permission trigger added
|
||
- `lib/servicios/servicio_ecualizador.dart`: Migration constants and `migrarClavesPlaceholder()` 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 tests
|
||
- `test/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 tests
|
||
- `test/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**:
|
||
|
||
1. **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.
|
||
|
||
2. **Provider Wiring Resolution (Phase 4, Task 4.8)**: Design assumed `ServicioDispositivoAudio` would be a top-level Provider. Actual: not registered in `app.dart`. Solution: added `solicitarPermisoBluetooth()` passthrough on `EstadoEcualizador` (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
|
||
|
||
1. **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
|
||
|
||
2. **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.
|