docs/adr/ADR-165-homecore-migrate-from-home-assistant.md
| 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 (HOMECORE master — series map row "ADR-134 HOMECORE-MIGRATE"), ADR-127 (HOMECORE-CORE), ADR-132 (HOMECORE-RECORDER — P2 side-by-side export target) |
| Tracking issue | #800 (HOMECORE intake) |
Number-collision resolution (2026-06-12). The HOMECORE series in ADR-126 §4 planned "ADR-134 = HOMECORE-MIGRATE", and the
homecore-migratecrate cites "ADR-134" throughout. But the on-diskADR-134-csi-to-cir-time-domain-multipath.mdis 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; theADR-134references insidev2/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.
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;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.
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.
HaStorageDir / HaStorageEnvelope read HA's .storage/ directory; read_envelope(path)
deserializes a .storage/*.json envelope (src/storage.rs).storage_format::v<N> (e.g. v13 for the entity registry)
(src/storage_format/).minor_version is a hard error (MigrateError::UnsupportedSchemaVersion),
never a silent best-effort parse. Better to refuse than to corrupt.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).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.MigrateError carries context (path, line/field) for I/O, JSON, YAML, missing-field,
unsupported-schema-version, and entity-id parse failures (src/lib.rs).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.automations.yaml → homecore-automation YAML.homecore-recorder, ADR-132; behind the recorder
Cargo feature, currently a no-op stub).!secret reference resolution in non-secrets YAML files.Positive.
.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.
Neutral.
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..storage format.