docs(openspec): archive bt-device-identity and promote its spec
Build & Deploy PluriWave / Análisis de código (push) Successful in 35s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m44s

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.
This commit is contained in:
2026-07-11 01:15:06 +02:00
parent 41b35c7f44
commit 159334f997
9 changed files with 568 additions and 0 deletions
@@ -0,0 +1,227 @@
# 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 15 (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 15), 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 15); 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 15: Code-Gated Work — COMPLETE
| Phase | Description | Tasks | Status | Evidence |
|-------|-------------|-------|--------|----------|
| 1 | Kotlin permission plumbing (manifest, placeholder guard, composite fallback, permission request, re-emit) | 1.11.8 (8) | 8/8 [x] | Code-inspection verified; `flutter analyze` 0 issues |
| 2 | Dart permission contract (abstract method, fakes, interface tests) | 2.12.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.13.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.14.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.15.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.16.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.17.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 15
- 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.77.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.
@@ -0,0 +1,56 @@
change: bt-device-identity
archived_date: 2026-07-11
status: archived
archived_from: openspec/changes/bt-device-identity
verification_verdict: "PASS WITH WARNINGS"
verify_report_observations:
- id: 2329
type: verify-report
critical_issues: 0
warnings: 2 (WARNING-1 closed during re-verify; WARNING-2 cosmetic, accepted)
suggestions: 3 (non-blocking)
artifacts_migrated:
- proposal.md (obs #2299)
- spec.md (obs #2304, on-disk merged to openspec/specs/bt-device-identity/)
- design.md (obs #2305)
- explore.md
- tasks.md (obs #2314, 57 checkboxes: 41 done, 6 N/A Phase 6, 10 pending Phase 7 manual QA)
- apply-progress.md (obs #2328, Batches 1/2/2b complete, Batch 3 Phase 7 pending)
- verify-report.md (obs #2329, re-verify complete, CRITICAL-1 closed, PASS WITH WARNINGS)
phase_status:
Phase 1 (Kotlin): COMPLETE - 8/8 tasks [x]
Phase 2 (Dart Permission Contract): COMPLETE - 5/5 tasks [x]
Phase 3 (Platform-Name Cache): COMPLETE - 10/10 tasks [x] (includes task 3.10 post-verify)
Phase 4 (Display Fix + Permission Trigger): COMPLETE - 8/8 tasks [x]
Phase 5 (Migration Purge): COMPLETE - 10/10 tasks [x]
Phase 6 (Localization): N/A - 6 tasks correctly marked N/A (no in-app UI copy shipped)
Phase 7 (Manual/On-Device QA): PENDING - 10 tasks [ ] (human gate, code-inspection verified only)
test_results:
total_changed_lines: "~450 (Kotlin 90, Dart prod 140, Dart tests +58 post-verify, manifest 1)"
flutter_analyze: "0 issues"
targeted_suites:
- estado_ecualizador_test.dart: 49/49 (was 48/48, +1 task 3.10)
- pantalla_ajustes_test.dart: 11/11
- servicio_ecualizador_test.dart: 20/20
- servicio_dispositivo_audio_real_test.dart: 10/10
- servicio_dispositivo_audio_test.dart: 7/7
- servicio_dispositivo_audio_toggle_test.dart: 5/5 (ripple-fix included)
total_tests_passing: 102/102
notes: |
Phase 7 (manual/on-device QA) is a mandatory human gate before shipping.
10 tasks (7.1-7.10) require real or emulated Android 12+ device access.
WARNING-2 (cosmetic arithmetic drift in apply-progress narrative) accepted as-is;
no corrections made to the artifact per task brief.
Kotlin work is code-inspection-verified only (no Android test harness in this repo).
Real MAC behavior and permission-dialog flow will be validated during Phase 7 manual QA.
CRITICAL-1 from initial verify (untested Dart-testable spec scenario for rename-survives-re-pair)
was closed via new task 3.10 composed regression test in Batch 2b (post-verify).
Test passes against existing, unmodified implementation — confirms coverage gap, not functional bug.
@@ -222,3 +222,103 @@ Both sdd/bt-device-identity/tasks (obs #2314) and sdd/bt-device-identity/apply-p
## Verdict
**FAIL** - blocked by exactly one CRITICAL finding (CRITICAL-1: untested Dart-testable spec scenario for rename-survives-re-pair). All other 20/21 scenarios are verified COMPLIANT or correctly DEFERRED/N/A; all 101 targeted tests independently re-executed and green; flutter analyze clean; all 6 design ADRs followed; l10n N/A correctly justified and independently confirmed; Phase 6/7 task states correctly reflect their true completion status. This is a narrow, well-scoped gap with a small, clearly-specified remediation, not a systemic implementation problem. Recommend one additional composed test via sdd-apply, then re-run sdd-verify before sdd-archive.
---
## Re-verify (targeted - 2026-07-11)
**Scope**: targeted re-verify, not a full re-verification. Confirms closure of CRITICAL-1 and WARNING-1 from the FAIL verdict above, and re-confirms WARNING-2 is still open/cosmetic. The 20/21 scenarios already verified COMPLIANT/DEFERRED/N/A in the original pass above are unchanged and were not individually re-audited in this pass; only the touched artifacts (one new test, one spec-wording edit) were inspected, plus a fresh full run of the one modified test file and a whole-project `flutter analyze`.
**What changed since the FAIL verdict**:
1. New composed regression test `3.10 rename persists after re-pair` added to `test/estado/estado_ecualizador_test.dart` (test-only, +58/-0).
2. `openspec/changes/bt-device-identity/spec.md` L31 and L50 rewritten to document the implemented composite shape `bt_a2dp:name:<sanitized productName>` (was `bt_a2dp:<type>:<productName>`, e.g. `bt_a2dp:8:AirPods Pro`).
**Commit note**: the task brief cited commit ab50df0 for the test addition. `git show ab50df0 --stat` resolves to an author/date/message/diff that is identical to the current HEAD commit 8cca7c3 (message: "test(devices): cover rename surviving a re-pair cycle end to end", 1 file, `test/estado/estado_ecualizador_test.dart`, 58 insertions, 0 deletions). Checked: `git merge-base --is-ancestor ab50df0 HEAD` returns false - ab50df0 is dangling, not reachable from current HEAD, while 8cca7c3 is. Consistent with a version-bump commit (158203f, "chore: bump version to 0.1.74+75") landing between b17c582 and this commit and the branch being rebuilt on top of it after the task brief was written. Content verified byte-identical under both references - this is hash drift, not a content discrepancy.
---
### CRITICAL-1 - CLOSED
Verified independently, not taken on the apply-progress/tasks narrative alone:
- **Test exists and passes**: `flutter test test/estado/estado_ecualizador_test.dart` (re-run this pass) -> 49/49 pass, including test 46 "3.10 rename persists after re-pair - composed regression (closes bt-device-identity CRITICAL-1)". Full output archived below.
- **Sequence matches the spec scenario exactly** (`openspec/changes/bt-device-identity/spec.md` L95-100, Requirement: Rename overlay survives re-pair under canonical id):
| Spec (L97-100) | Test (`estado_ecualizador_test.dart`, spans L1323-1379) |
|---|---|
| GIVEN a custom rename "My Headphones" is stored for bt_a2dp:AA:BB:CC:DD:EE:FF | L1328-1332 defines btDevice with that exact id; L1347 connects it via emitirDispositivo(btDevice); L1353 renombrarDispositivo(btDevice.id, 'My Headphones'); asserted at L1354-1357 |
| WHEN that device disconnects and reconnects, reporting the same MAC | L1361 emitirDispositivo(builtinSpeaker) (disconnect); L1365 emitirDispositivo(btDevice) again - same id (re-pair) |
| THEN the device row MUST still display "My Headphones" | L1373-1376 expect(eq.nombreVisible(btDevice.id, eq.nombrePlataforma(btDevice.id)), equals('My Headphones')) |
| AND no second/duplicate entry MUST appear in presetsDispositivo | L1369 expect(eq.presetsDispositivo.length, equals(cantidadTrasRenombrar)) |
The test additionally asserts the preset entry itself is untouched (L1371, presetsDispositivo[btDevice.id] unchanged), a stronger check than the spec literally requires - good practice, not scope creep.
- **Zero production-code changes**: `git show 8cca7c3 --stat` (content-identical to ab50df0 --stat) -> 1 file changed, test/estado/estado_ecualizador_test.dart, 58 insertions(+), 0 deletions(-). No lib/ or android/ diff. Confirms this was a genuine coverage gap, not a latent bug - the mechanism was already correct, composed from already-tested constituents D.4 (reconnect-dedup) and 4.4 (rename-priority).
- **Assertion-quality audit (Strict TDD Step 5f)**: read the full test body. 5 expect() calls, all comparing dynamically-captured runtime values (presetTrasConectar, cantidadTrasRenombrar) against fresh reads taken after the disconnect/re-pair cycle - no tautologies, no ghost loops, no assertion-free paths, no mock-call-count coupling. Every assertion exercises real production code (emitirDispositivo -> _onDispositivoCambiado, renombrarDispositivo, nombreVisible, nombrePlataforma). 0 CRITICAL, 0 WARNING.
- **TDD classification**: apply-progress transparently labels this "Not RED-first by design" - a coverage-closing composed test run against unmodified, already-correct code, passing on first execution. This is the same category the original report already accepted without penalty for tests 3.7, 3.8, 4.4, 4.5 (approval/regression-lock style, honestly disclosed rather than overclaimed as true fail-before RED). Consistent treatment applied here - no new TDD-process flag.
**Verdict on CRITICAL-1**: CLOSED. The Dart-testable scenario in requirement group "Rename overlay survives re-pair under canonical id" is now COMPLIANT. (Spec Compliance Matrix row 11 in the original report above is left unedited for audit-trail integrity - read it as superseded by this section, not as still-accurate.)
---
### WARNING-1 - CLOSED
Verified spec vs design vs implementation triangulation across all three layers:
| Layer | Evidence | Value |
|---|---|---|
| Spec | spec.md L31 (requirement prose) | "...id MUST fall back to a deterministic composite id keeping the single leading bt_a2dp: segment, with the product name colon-sanitized (implemented shape: bt_a2dp:name:<sanitized productName>, per design ADR-3 ...)" |
| Spec | spec.md L50 (scenario) | "THEN deviceId MUST be a deterministic composite id (implemented shape: bt_a2dp:name:AirPods Pro) ..." |
| Kotlin | MainActivity.kt L777-784 (guard + fallback block), literal at L783 | "bt_a2dp:name:$safeProductName" |
| Dart | estado_ecualizador.dart L33 | static const _prefijoPlaceholderCompuesto = 'bt_a2dp:name:'; |
All four now agree on the literal "name" marker segment. The spec no longer describes a shape (bt_a2dp:<type>:<productName>, e.g. bt_a2dp:8:AirPods Pro) that the implementation never produced. This was a documentation-only fix - no code changed: the same commit-stat evidence above shows zero lib/ or android/ diff, and openspec/changes/bt-device-identity/ is entirely untracked in git (status shows it as untracked), consistent with this being a working-tree file edit rather than a commit.
**Verdict on WARNING-1**: CLOSED.
---
### WARNING-2 - still open, accepted as cosmetic (no action taken, per task brief)
Unchanged from the original report: the two arithmetic discrepancies (40 vs. git-verified 43 pre-existing estado_ecualizador_test.dart tests; "7 pre-existing" vs. actual 5 in servicio_dispositivo_audio_toggle_test.dart) live only in the apply-progress engram narrative, not in the spec, code, or tests. Actual runtime results were independently re-verified as correct both in the original pass and again in this pass (49/49, up from 48/48 - exactly +1 for the one new test, arithmetically consistent with the claimed 48-pre-existing baseline for task 3.10 specifically, so no new discrepancy was introduced by this batch). Zero functional impact. Left genuinely open per the task brief ("no action taken") rather than silently marked closed; see final verdict rationale below for why this keeps the verdict at PASS WITH WARNINGS rather than a bare PASS.
---
### Independently re-executed evidence (this pass)
```text
$ flutter test test/estado/estado_ecualizador_test.dart
...
00:01 +41: platform-name cache (bt-device-identity Phase 3) 3.1 platform name is cached from a device-change event
00:01 +42: platform-name cache (bt-device-identity Phase 3) 3.4 composite-placeholder sentinel does not create device-list entry
00:01 +43: platform-name cache (bt-device-identity Phase 3) 3.6 multiple denied-permission devices do not collide
00:01 +44: platform-name cache (bt-device-identity Phase 3) 3.7 repeated event for known id is a no-op on preset creation (regression-lock)
00:01 +45: platform-name cache (bt-device-identity Phase 3) 3.8 transient non-BT id during pairing handshake does not corrupt the BT entry (regression-lock)
00:01 +46: platform-name cache (bt-device-identity Phase 3) 3.10 rename persists after re-pair - composed regression (closes bt-device-identity CRITICAL-1)
00:01 +47: FakeServicioEcualizador - nombresDispositivos preservation (WARNING-3) guardarPrincipal preserves nombresDispositivos
00:01 +48: FakeServicioEcualizador - nombresDispositivos preservation (WARNING-3) guardarActivo preserves nombresDispositivos
00:02 +49: All tests passed!
```
```text
$ flutter analyze
Analyzing pluriwave...
No issues found! (ran in 5.4s)
```
flutter build was NOT run, per task constraints (consistent with the original pass's scope exclusion).
---
### Housekeeping note (SUGGESTION-3, still open)
The pending Engram conflict marker flagged in SUGGESTION-3 of the original report (on sdd/bt-device-identity/tasks obs #2314 and sdd/bt-device-identity/apply-progress obs #2328) is still present, and this verify-report itself (obs #2329) now also shows a pending "contested by" marker as of this session's mem_search. All three resolved to their latest/highest-revision content when fetched via mem_get_observation and were used as authoritative for this re-verify, same as before. Still recommended: resolve/judge these pending conflicts before sdd-archive - housekeeping only, not a content-accuracy concern.
---
## Updated Verdict
**PASS WITH WARNINGS**
CRITICAL-1 is closed with real, independently-executed, assertion-quality-audited test evidence composing the exact spec scenario (spec.md L95-100), against zero production-code changes (confirming a coverage gap, not a functional bug). WARNING-1 is closed - spec, design (ADR-3), Kotlin, and Dart now agree byte-for-byte on the bt_a2dp:name:<sanitized productName> composite shape. WARNING-2 remains open by the task's own account (cosmetic arithmetic drift in an internal apply-progress narrative, zero functional/spec/code impact, independently reconfirmed accurate at the runtime-result level in both verify passes) - kept open rather than silently closed, since no correction was actually made to the artifact. Zero CRITICAL findings remain. All 3 original SUGGESTIONs remain open/non-blocking and unchanged; one housekeeping note (pending Engram conflict markers, now also touching this report) is carried forward.
**Recommendation**: proceed to sdd-archive. Phase 7 (manual/on-device QA, tasks 7.1-7.10 in tasks.md) remains genuinely pending - it always has been, is a human-owned gate with no CI/emulator harness in this repo, and was correctly treated as DEFERRED (not blocking) in the original pass. It stays orthogonal to this code-correctness verdict; the maintainer should track it separately (e.g., as an archive follow-up or explicit sign-off note) rather than treat it as a re-verify blocker.
+185
View File
@@ -0,0 +1,185 @@
# Spec: Canonical Bluetooth Device Identity
## New Capability: bt-device-identity
### Purpose
Ensures each physical Bluetooth A2DP device produces a stable, unique `deviceId` and displays its own Bluetooth name by default, by acquiring `BLUETOOTH_CONNECT` at point-of-intent, guarding against the OS placeholder MAC, caching per-device platform names, and migrating away corrupted placeholder-keyed persistence entries.
### Requirements
#### Requirement: BLUETOOTH_CONNECT requested at point-of-intent
On API 31+, the system MUST request `BLUETOOTH_CONNECT` when the device-management UI (Settings → Advanced Equalization) is opened, not at app launch. `AndroidManifest.xml` MUST declare `BLUETOOTH_CONNECT`.
##### Scenario: permission requested on UI open — manual/on-device QA
- GIVEN the app has never requested `BLUETOOTH_CONNECT`
- WHEN the user opens the device-management screen on API 31+
- THEN the system permission dialog MUST appear before any BT device is enumerated for identity purposes
##### Scenario: permission not requested at app launch — manual/on-device QA
- GIVEN a fresh install
- WHEN the app starts and the user never opens device-management
- THEN no BT permission dialog MUST appear
---
#### Requirement: Placeholder MAC guarded with composite fallback
`deviceToMap()` MUST treat the OS placeholder `"02:00:00:00:00:00"` as absent, not as a usable MAC. When the MAC is absent or the placeholder, the id MUST fall back to a deterministic composite id keeping the single leading `bt_a2dp:` segment, with the product name colon-sanitized (implemented shape: `"bt_a2dp:name:<sanitized productName>"`, per design ADR-3 — the fixed `name` marker keeps matrix-key split-on-first-colon safety); the placeholder string itself MUST NOT become a `deviceId`.
##### Scenario: permission granted yields real MAC id — Dart-testable via fake device stream
- GIVEN `BLUETOOTH_CONNECT` is granted
- WHEN a BT A2DP device with MAC `AA:BB:CC:DD:EE:FF` connects
- THEN `deviceId` MUST be `"bt_a2dp:AA:BB:CC:DD:EE:FF"`
##### Scenario: permission granted yields real MAC id — manual/on-device QA (Kotlin `deviceToMap`)
- GIVEN `BLUETOOTH_CONNECT` is granted on a real device
- WHEN `deviceToMap()` runs for a connected BT A2DP device
- THEN `device.address` MUST be a real MAC, not the placeholder
- AND the resulting id MUST NOT equal `"bt_a2dp:02:00:00:00:00:00"`
##### Scenario: permission denied yields composite fallback, no crash — Dart-testable via fake device stream
- GIVEN `BLUETOOTH_CONNECT` is denied or `device.address` reports the placeholder
- WHEN a BT A2DP device with `productName` "AirPods Pro" connects
- THEN `deviceId` MUST be a deterministic composite id (implemented shape: `"bt_a2dp:name:AirPods Pro"`), MUST NOT be `"bt_a2dp:02:00:00:00:00:00"`, and app state MUST update without throwing
##### Scenario: multiple denied-permission devices do not collide — Dart-testable via fake device stream
- GIVEN `BLUETOOTH_CONNECT` is denied
- WHEN two distinct BT A2DP devices with different `productName` values connect in sequence
- THEN each MUST produce a distinct composite `deviceId`
- AND `presetsDispositivo` MUST NOT collapse them into one entry
##### Scenario: placeholder never surfaces as a stored id — Dart-testable
- GIVEN any device change event is processed after the fix
- WHEN persistence keys are inspected
- THEN no key MUST equal the literal string `"bt_a2dp:02:00:00:00:00:00"`
---
#### Requirement: Per-device platform-name cache feeds display
`EstadoEcualizador` MUST cache the last-seen platform (`productName`) name per `deviceId`. `nombreVisible(deviceId, platformName)` call sites in `pantalla_ajustes.dart` MUST pass the cached platform name, not an empty string.
##### Scenario: platform name displays with no custom rename — Dart-testable
- GIVEN a device with id `"bt_a2dp:AA:BB:CC:DD:EE:FF"` and platform name "AirPods Pro" has connected, with no custom rename stored
- WHEN the device row is rendered
- THEN the displayed name MUST be "AirPods Pro", not the raw `deviceId`
##### Scenario: custom rename overrides platform name — Dart-testable
- GIVEN the same device has both a cached platform name "AirPods Pro" and a custom rename "My Headphones"
- WHEN the device row is rendered
- THEN the displayed name MUST be "My Headphones"
##### Scenario: no platform name yet falls back to raw id — Dart-testable
- GIVEN a `deviceId` has no cached platform name and no custom rename
- WHEN the device row is rendered
- THEN the displayed name MUST be the raw `deviceId` (unchanged legacy behavior)
---
#### Requirement: Rename overlay survives re-pair under canonical id
Once `BLUETOOTH_CONNECT` is granted, a rename stored under a device's canonical MAC-based `deviceId` MUST persist across disconnect/reconnect (re-pair) of the same physical device.
##### Scenario: rename persists after re-pair — Dart-testable via fake device stream
- GIVEN a custom rename "My Headphones" is stored for `"bt_a2dp:AA:BB:CC:DD:EE:FF"`
- WHEN that device disconnects and reconnects, reporting the same MAC
- THEN the device row MUST still display "My Headphones"
- AND no second/duplicate entry MUST appear in `presetsDispositivo`
##### Scenario: rename persists after re-pair — manual/on-device QA
- GIVEN a real paired BT device is renamed in-app
- WHEN the user disconnects and re-pairs the same physical device
- THEN the rename MUST still be shown and no duplicate device row MUST appear
---
#### Requirement: `_onDispositivoCambiado` does not create duplicate entries for transient reports
Device-change events reporting an id already present in `presetsDispositivo` MUST NOT create a second entry or overwrite the existing preset with a fresh copy.
##### Scenario: repeated event for known id is a no-op on preset creation — Dart-testable
- GIVEN `presetsDispositivo` already contains an entry for `deviceId`
- WHEN `_onDispositivoCambiado` fires again for the same `deviceId`
- THEN `presetsDispositivo[deviceId]` MUST remain unchanged
- AND no new key MUST be added to `presetsDispositivo`
##### Scenario: transient non-BT id during pairing handshake does not corrupt BT entry — Dart-testable via fake device stream
- GIVEN a BT device is mid-reconnect and the OS transiently reports `"builtin_speaker"` before A2DP profile attaches
- WHEN both the transient and final BT events are processed
- THEN the BT device's own entry MUST be keyed only by its BT `deviceId`
- AND MUST NOT be merged with or overwritten by the transient `"builtin_speaker"` entry
---
#### Requirement: One-time guarded migration purges only exact placeholder-keyed entries
On first load after this change, the system MUST remove entries whose key is exactly `"bt_a2dp:02:00:00:00:00:00"` from `eq_nombres_dispositivos_v1`, `eq_presets_matriz_v1` (matching the `deviceId` segment), and `eq_preset_por_dispositivo_v1`. All other entries MUST be preserved unchanged. The migration MUST run at most once (idempotent, flagged).
##### Scenario: migration removes only exact placeholder entries — Dart-testable
- GIVEN `eq_preset_por_dispositivo_v1` contains both `"bt_a2dp:02:00:00:00:00:00"` and `"bt_a2dp:AA:BB:CC:DD:EE:FF"`
- WHEN the migration runs
- THEN the placeholder-keyed entry MUST be removed
- AND the stable-MAC entry MUST remain byte-for-byte identical
##### Scenario: matrix keys purge only the placeholder segment — Dart-testable
- GIVEN `eq_presets_matriz_v1` contains `"station1:bt_a2dp:02:00:00:00:00:00"` and `"station1:bt_a2dp:AA:BB:CC:DD:EE:FF"`
- WHEN the migration runs
- THEN only the entry with the placeholder `deviceId` segment MUST be removed
- AND the other matrix entry MUST be preserved unchanged
##### Scenario: near-miss keys are preserved — Dart-testable
- GIVEN a key `"bt_a2dp:02:00:00:00:00:01"` exists (differs by one digit from the placeholder)
- WHEN the migration runs
- THEN this entry MUST NOT be removed
##### Scenario: migration runs once — Dart-testable
- GIVEN the migration has already run once (flag set)
- WHEN the app loads again with the same data
- THEN the migration MUST NOT execute a second time
- AND no additional entries MUST be removed
##### Scenario: no placeholder entries when BLUETOOTH_CONNECT was never requested — Dart-testable
- GIVEN no BT permission was ever granted and no placeholder-keyed entries exist
- WHEN the migration runs
- THEN it MUST be a no-op and MUST NOT alter any persisted keys
---
### Localization
#### Requirement: Permission rationale and migration notice strings exist in all locales
If UI copy is shown for the `BLUETOOTH_CONNECT` rationale or the migration notice, new l10n keys MUST be added to `lib/l10n/app_en.arb` as the template and translated in all 13 target locales: `ar`, `bn`, `de`, `en`, `es`, `fr`, `hi`, `id`, `it`, `ja`, `pt`, `ru`, `zh`.
##### Scenario: new keys present in every locale — Dart-testable (arb parity check)
- GIVEN new keys `btConnectRationale` and `eqDeviceMigrationNotice` (or equivalent) are added to `app_en.arb`
- WHEN each of the 13 `app_<locale>.arb` files is parsed
- THEN each file MUST contain both keys with non-empty translated values
##### Scenario: rationale copy renders before the OS dialog — manual/on-device QA
- GIVEN the device-management UI is opened for the first time
- WHEN the in-app rationale (if any) is shown
- THEN it MUST render in the user's selected app locale before the OS permission dialog appears