{ id = "adr-001", title = "Non-exhaustive structs with named constructors as the external construction contract", status = 'accepted, date = "2026-05-12", context = m%" `Instance` and `ProviderHealth` are the two core output types of the `ComputeProvider` trait. Both are marked `#[non_exhaustive]` — a forward- compatibility guarantee that allows adding fields without a SemVer bump. The attribute blocks struct literal syntax in external crates (E0639), which became concrete when `lian-build`'s `LianBuildScriptProvider` attempted to construct `Instance { handle, provider, ... }` as the first γ-posture external consumer. Three remediation paths exist: 1. Remove `#[non_exhaustive]`: construction works, but every new field is a breaking change at all destructuring and pattern-match sites. 2. Named constructors: keep the attribute, add `::new()` / `::healthy()` / `::unhealthy()` as the stable external API. 3. Builder pattern: type-state or `Default` + field-by-field assignment. The crate is a library for provider implementors, not application code. The field set of `Instance` (ip, ssh_host, ssh_port, expires_at, metadata) is expected to grow as new provider kinds surface cloud-specific properties. Preserving `#[non_exhaustive]` is the correct API stability posture. "%, decision = m%" Keep `#[non_exhaustive]` on all public output structs. Add named constructors as the sole supported construction path for external crates: - `Instance::new(handle, provider)` — zero-optional-field form; callers assign optional fields (`ssh_host`, `ip`, `expires_at`, `metadata`) after construction. - `ProviderHealth::healthy(latency_ms)` — ok=true, latency set, detail=None. - `ProviderHealth::unhealthy(latency_ms, reason)` — ok=false, detail set. Builder and Default are explicitly excluded: a builder adds three abstractions for two required fields; Default would require callers to know which fields are meaningful versus zero-valued. The `InstanceHandle` and `ProviderKind` enums require no change — they are exhaustive and constructible via standard syntax. "%, alternatives = [ m%"Remove `#[non_exhaustive]`. Rejected: adding fields to `Instance` (e.g. `ipv6_host`) would be a breaking change at all match/destructuring sites in consumer crates."%, m%"Builder pattern. Rejected: over-engineered for structs with 2–3 meaningful initialization parameters and all-optional fields beyond the required handle/provider pair."%, m%"`Default` impl + field assignment. Rejected: requires callers to know which fields are semantically required vs truly optional; a zero-valued `provider` string is silently wrong."%, ], consequences = m%" Named constructors become a committed API surface. Adding a new *required* semantic field to `Instance` now requires both a struct field and a constructor parameter — the same cost as removing `#[non_exhaustive]` would impose, but scoped to the constructor rather than all consumers. Adding new *optional* fields is still backward-compatible: the struct gains the field, the constructor initializes it to `None`/default, existing callers use the field by name after construction if they want it. The `lian-build` `LianBuildScriptProvider` is the canonical usage example: `Instance::new(handle, "lian-build-script")` followed by direct field assignment for `ssh_host`, `ssh_port`, `expires_at`. "%, constraints = [ { kind = 'hard, statement = "`Instance`, `ProviderHealth`, and any future public output struct MUST remain `#[non_exhaustive]`." }, { kind = 'hard, statement = "Public constructors MUST initialize all required semantic fields; optional fields MUST default to `None` or the zero value." }, { kind = 'soft, statement = "Convenience constructors (`healthy`, `unhealthy`) SHOULD cover the two most common `ProviderHealth` shapes to keep provider implementations free of boilerplate." }, ], }