# ADR-165: HOMECORE-MIGRATE — Migration Tooling from Python Home Assistant

| Field | Value |
|-------|-------|
| **Status** | Accepted — registry/config persistence implemented |
| **Date** | 2026-05-25 |
| **Deciders** | ruv |
| **Codename** | **HOMECORE-MIGRATE** |
| **Crate** | `v2/crates/homecore-migrate` |
| **Relates to** | [ADR-126](ADR-126-ruview-native-ha-port-master.md) (HOMECORE master — series map row "ADR-134 HOMECORE-MIGRATE"), [ADR-127](ADR-127-homecore-state-machine-rust.md) (HOMECORE-CORE), [ADR-132](ADR-132-homecore-recorder-history-semantic-search.md) (HOMECORE-RECORDER — P2 side-by-side export target) |
| **Tracking issue** | [#800](https://github.com/ruvnet/RuView/pull/800) (HOMECORE intake) |

> **Number-collision resolution (2026-06-12).** The HOMECORE series in ADR-126 §4 planned
> "ADR-134 = HOMECORE-MIGRATE", and the `homecore-migrate` crate cites "ADR-134" throughout.
> But the on-disk `ADR-134-csi-to-cir-time-domain-multipath.md` is a **different, unrelated
> decision** (First-Class CIR Support, a signal-processing tier). The migrate crate was
> therefore governed by a phantom identity (ADR-164 Gap G3 / Coverage-Gaps Lens §A). This
> ADR takes the next free number (**165**) and becomes the real governing record for
> HOMECORE-MIGRATE; the `ADR-134` references inside `v2/crates/homecore-migrate/` are
> repointed to ADR-165. The real ADR-134 (CIR) is untouched. ADR-126's series-map row still
> labels the *role* "ADR-134 HOMECORE-MIGRATE" for historical traceability; that registry
> renumber is owner-gated and left for the follow-up. This ADR reverse-documents the shipped
> P1 scaffold; it introduces no new design.

---

## 1. Context

ADR-126 decided to reimplement Home Assistant (HA) natively in Rust. A user adopting
HOMECORE has an existing HA install whose configuration lives in two places on disk:

- `.storage/*.json` — versioned JSON envelopes (`{ version, minor_version, data }`) holding
  the entity registry, device registry, and config entries;
- top-level YAML — `secrets.yaml`, `automations.yaml`.

To migrate, HOMECORE must read this foreign, **untrusted** on-disk state. It is untrusted in
the security sense: the schema can drift between HA releases, and silently mis-parsing a
registry would corrupt the imported home. ADR-164 flagged this as a CRITICAL coverage gap —
a data-integrity-sensitive importer governed by a non-existent ADR identity.

The decision an ADR must pin here is the **trust boundary and import contract**: which HA
files are read, how schema versions are validated, and what happens on an unknown version.

## 2. Decision

Ship `homecore-migrate` as a CLI + library that reads an existing HA filesystem and imports
its configuration into HOMECORE. Registry and config-entry conversion are durable; automation
conversion and secret-reference resolution remain deferred.

### 2.1 Storage reader + versioned format gate (P1, shipped)

- `HaStorageDir` / `HaStorageEnvelope` read HA's `.storage/` directory; `read_envelope(path)`
  deserializes a `.storage/*.json` envelope (`src/storage.rs`).
- Versioned parsers live under `storage_format::v<N>` (e.g. `v13` for the entity registry)
  (`src/storage_format/`).
- **Schema-version validation is the load-bearing safety rule (§6 Q5 of this ADR):** an
  unknown `minor_version` is a **hard error** (`MigrateError::UnsupportedSchemaVersion`),
  never a silent best-effort parse. Better to refuse than to corrupt.

### 2.2 Per-artifact conversion (shipped)

- `entity_registry::load()` — `core.entity_registry` → `Vec<homecore::EntityEntry>`
  (ready for import).
- `device_registry::read_device_registry()` converts the supported v13 device fields into
  `homecore::DeviceEntry`; `write_device_registry()` emits an HA-compatible v13 envelope.
- `config_entries::convert_config_entries()` emits versioned `homecore.config_entries`
  storage. Original rows are retained verbatim, while unsupported domains and fields produce
  typed warnings instead of being discarded.
- `secrets::load_secrets()` — `secrets.yaml` → `HashMap<String, String>` (resolution P2).
- `automations::load()` — `automations.yaml` → count + ID/alias list (conversion P2).

### 2.3 CLI

- `homecore-migrate inspect <ha-dir>` previews what will be migrated (entity/device/config
  counts, redacted secret/automation lists) (`src/cli.rs`, `src/main.rs`).
- `import-entities`, `import-devices`, and `import-config-entries` write destination files and
  emit one-line JSON summaries. Writes use synced same-directory temporary files and atomic
  no-clobber publication; an existing destination is never implicitly replaced.

### 2.4 Structured errors (P1, shipped)

- `MigrateError` carries context (`path`, line/field) for I/O, JSON, YAML, missing-field,
  unsupported-schema-version, and entity-id parse failures (`src/lib.rs`).
- **Secret-leak hardening (security review, 2026-06).** `secrets.yaml` parse failures must
  NOT use the generic `MigrateError::YamlParse { source }` variant: `serde_yaml`'s message
  for a typed-tag coercion error (e.g. `port: !!int <value>`) embeds the offending scalar
  verbatim (`invalid value: string "<the-secret-value>"`), and that error propagates through
  the `InspectSecrets` CLI path to stderr — leaking a secret value despite the CLI's
  deliberate `<redacted>` design. `read_secrets` now maps such failures to a dedicated
  redacting variant `MigrateError::SecretsParse { path, line, column }` that carries only the
  file path and a coarse location (`serde_yaml::Error::location()`), never the scalar content.
  Pinned by `secrets::tests::malformed_secrets_error_never_contains_secret_value` (asserts the
  rendered error **and its full `#[source]` chain** never contain the secret value).
  **Review dimensions confirmed clean with evidence:** source is never mutated; destination
  writes are explicit `--to` paths and no-clobber; paths are
  user-supplied dirs joined with fixed filenames (no `..`/absolute traversal beyond the
  user's own privileges); malformed/typed/truncated `.storage` JSON and YAML **error, never
  panic** (every production `unwrap`/`expect` is test-only); unknown schema `minor_version`
  hard-errors fail-closed; no SQL/shell injection surface.

### 2.5 Deferred to P2+ (NOT built — honestly labelled)

- Execute imported config entries (a matching HOMECORE plugin must claim the preserved domain).
- Convert `automations.yaml` → `homecore-automation` YAML.
- Side-by-side runtime mode (requires `homecore-recorder`, ADR-132; behind the `recorder`
  Cargo feature, currently a no-op stub).
- `!secret` reference resolution in non-secrets YAML files.

### 2.6 Test evidence

- Targeted tests cover registry round trips, unknown versions, lossless unsupported config
  fields/domains, malformed input, and crash-safe/no-overwrite destination behaviour.

## 3. Consequences

**Positive.**

- The trust boundary is explicit: unknown HA schema versions are rejected, not guessed, so a
  schema drift fails loudly instead of corrupting an imported home.
- Reusing HA's own `.storage` and YAML formats means no intermediate export step; the tool
  reads a live HA install directly.
- `inspect` gives users a no-risk dry run before any write.

**Negative / honest limits.**

- Imported config entries are durable but do not install or execute Python HA integrations.
- Automation conversion and secret-reference resolution are not built.
- The side-by-side recorder export depends on ADR-132 and is currently a feature-gated
  no-op.
- Performance figures in the README (envelope parse < 5 ms, 1 000-entity load < 50 ms) are
  estimates, **needs verification** with a benchmark.

**Neutral.**

- This resolves only the *identity* of the migrate decision (134→165). The broader 6-way
  duplicate-number cleanup (incl. ADR-126's series-map registry row) is owner-gated.

## 4. Links

- Crate: `v2/crates/homecore-migrate/` — `Cargo.toml`, `README.md`, `src/lib.rs`,
  `src/storage.rs`, `src/storage_format/`, `src/entity_registry.rs`,
  `src/device_registry.rs`, `src/config_entries.rs`, `src/secrets.rs`,
  `src/automations.rs`, `src/cli.rs`, `src/main.rs`.
- [ADR-126](ADR-126-ruview-native-ha-port-master.md) — HOMECORE master (series map: HOMECORE-MIGRATE).
- [ADR-132](ADR-132-homecore-recorder-history-semantic-search.md) — HOMECORE-RECORDER (P2 side-by-side export target).
- [ADR-134](ADR-134-csi-to-cir-time-domain-multipath.md) — First-Class CIR Support (the *unrelated* decision the crate was mistakenly citing).
- [ADR-164](ADR-164-adr-corpus-gap-analysis.md) — gap analysis that surfaced this collision (Gap G3).
- [Home Assistant `.storage` format](https://developers.home-assistant.io/docs/storage/).
