provisioning-platform/crates/ontoref-compute/adrs/adr-001-non-exhaustive-constructors.ncl

72 lines
4 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
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 23 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." },
],
}