17 KiB
Built-In Sibling Configuration Inheritance
Status: implemented. The configuration contract owns current behavior. This document preserves the agreed design and its baseline; the implementation plan records completed work.
Purpose and priorities
Allow a configuration mapping to inherit settings from one sibling mapping by
using the built-in extends keyword. No collection registration, configurable
reference-field name, or inheritance field in application Go types is needed.
ConfigKit resolves the relationships during loading and returns final typed configuration
with inspectable provenance. Applications retain ownership of field meanings,
domain validation, and execution.
The downstream request supplies a concrete need for reusable named defaults and chains of specialization. ConfigKit adopts a shared structural model, using its existing recursive combination and presence conventions. The design serves named presets, connection settings, or similar configuration entries without interpreting their domain.
Priorities are consistency and predictability, accurate presence/provenance, and reduced consumer configuration infrastructure. Computational efficiency matters within those goals: resolve shared ancestors once and account for the size of materialized results and retained history. Avoid an unconditional O(n) promise.
Baseline at design time
Baseline: b604aa167b684525f0d5d806d9c4267824151ee4, after the filesystem-source
and underlay implementation and its remediation.
- The configuration contract supports host/native filesystem, byte, reader, and snapshotted mapping sources through a shared engine.
- Independent contributions compose strictly within a tier; explicitly ordered tiers combine using recursive overlay. Typed defaults sit below source input; selected overlays and explicit CLI overrides retain their documented roles.
- Named application entries can already be represented as string-keyed mappings. ConfigKit currently treats relationships between their keys as application data.
- Recorded application resolution runs after typed decoding and preserves dependencies and provenance. Consumers currently own relationship resolution; there is no built-in pre-decode inheritance phase.
- Source contexts support later application-owned resource access. Loading is synchronous and complete on return, with library-owned loading handles closed.
This feature is a deliberate extension of structural input processing. It follows the architecture goals, the composition distinction in ADR 0001, the presence/provenance principles in ADR 0003, and the complete-on-return design in draft ADR 0008.
Target configuration model
Built-in keyword and sibling lookup
A mapping containing extends: foo inherits from the mapping named foo in
its immediately enclosing mapping. Lookup uses an exact sibling key, independent
of declaration order. It does not interpret names as paths, search enclosing
scopes, or search unrelated branches. One mapping can name one parent; chains
and shared parents are supported.
baz:
foo:
width: 10
length: 20
bar:
extends: foo
width: 20
The resulting bar has width: 20 and length: 20. ConfigKit consumes the
extends field as structural metadata, retaining its provenance for inspection
rather than passing it to the application's typed payload.
extends becomes reserved configuration syntax throughout input mappings,
consistently for ordinary and layered loading and every supported source kind.
It is not a configurable keyword or an application-selected collection feature.
Mappings without a relationship retain their own payload, which can still contain
nested inheritance. Explicit null or scalar siblings retain ordinary data meaning
and final schema validation; a referenced parent must be a mapping.
Identity comes from the sibling key, not a copied field in the parent payload.
The child keeps its key. Application fields such as name or id follow ordinary
combination semantics.
Reference values and positions
A string-valued extends names an exact sibling key, including an empty string
if that key exists. Explicit extends: null means no parent and cancels a
relationship supplied by a lower-precedence source or inherited nested payload.
Omitting extends contributes no change to an existing relationship during
combination; if none remains in the effective mapping, the mapping has no parent.
Other value shapes are reference errors.
Cancellation retains the mapping's own assembled settings but adds no settings from the canceled parent. Like a string reference, the null control field is consumed as structural metadata before typed decoding. This follows existing presence and replacement rules without introducing a deletion operator.
A string-valued extends on the document root or directly on a sequence-element
mapping has no enclosing mapping of named siblings and produces a location-bearing
reference error. ConfigKit does not search farther up for a namespace. Mappings
nested within those structures can still use their own sibling namespaces normally.
Source precedence first, inheritance second
First assemble the effective input under the existing source rules: strict composition within tiers and recursive overlay across underlays, main input, selected overlay, and explicit CLI overrides. Strict conflicts remain errors. Then resolve inheritance against the resulting effective tree. Inheritance is the last structural composition operation, not the last loading lifecycle phase.
Every parent reference sees that parent's effective definition, including source overrides. For each entry, resolve the parent's chain, then recursively overlay the child's own effective payload. Each child obtains independent effective settings; resolving a child leaves its parent and siblings unchanged.
Source precedence and ancestry answer different questions. Source precedence
selects each entry's own settings and parent reference. Ancestry supplies settings
that entry omits. For example, if a child's own timeout is 10, changing its
parent's timeout to 30 through higher-priority source input leaves the child's
explicit 10 intact. To change that explicit child value, override the child.
This applies equally to CLI overrides at those respective paths.
One combination contract
Parent-to-child combination uses the existing recursive mapping overlay:
- Omitted children retain inherited values.
- Present zero, false, and empty strings replace inherited values.
- Mappings combine recursively; an empty mapping retains inherited children.
- Sequences replace as a unit, including empty sequences.
- Explicit null replaces the inherited value, subject to final schema acceptance.
- Cross-kind replacement follows the same overlay rules and final schema checks.
The parent's own extends relationship is consumed when its parent chain is
expanded; it is not copied as the child's own relationship. Nested extends
fields remain structural instructions and travel with the nested payload until
that destination scope is resolved, as described below.
These rules intentionally differ from the request's suggested null-as-unspecified and whole-parameter-map replacement. Consumers adopting the feature adopt ConfigKit's shared rules. Multiple inheritance, sequence merging, per-field strategies, deletion operators, cross-scope references, and named-overlay file inheritance are outside this feature's initial scope.
Enclosing-first, dependency-aware resolution
Resolve inheritance from the root outward. Within each mapping scope, parent dependencies determine resolution order: expand a sibling's parent chain before combining that sibling's own payload. Then resolve nested relationships within the resulting destination mappings. Nested relationships inherited from a parent are evaluated in the destination context, after enclosing combination.
presets:
base:
settings:
primary:
width: 10
secondary:
extends: primary
custom:
extends: base
settings:
primary:
width: 20
Here base.settings.secondary.width becomes 10, and
custom.settings.secondary.width becomes 20. The nested relationship is
resolved independently in each resulting settings mapping. Copying the parent's
already fully resolved nested settings would produce the wrong result for custom.
The engine therefore distinguishes expansion of a mapping's own parent chain from resolution of its nested payload. It retains unresolved nested instructions while combining enclosing mappings. Memoized work must respect that distinction and the destination namespace; a fully resolved parent subtree is not a reusable template for every child.
Visit mapping keys in lexicographic order for deterministic traversal and diagnostics, following dependencies as needed. Dependencies and nesting determine the result; alphabetical order never determines which values win. A parent may sort before or after its child without changing the meaning. Breadth-first versus recursive traversal is an implementation choice subject to these semantics.
Nested mappings inside sequences are also visited, while sequence combination continues to replace whole sequences. A relationship on a sequence element itself follows the reference-position rules above.
Loading and validation
The proposed lifecycle is: existing acquisition/composition and main-owned version checking; finish source precedence assembly; resolve named inheritance; apply typed defaults and decode; recorded application resolution; secret lookup; semantic validation; publish the final configuration and metadata.
Resolve inheritance in the private input representation while omission is still explicit. Ordinary and layered loading use the same implementation. Typed defaults remain the final decoder's baseline and are not converted into parent declarations or recursively copied from parent Go values. Domain resolution receives already expanded settings and can apply recorded normalization/defaulting under its existing contract.
Preserve the existing version owner's declaration and checking point. Inheritance must not supply, replace, or remove the protected version path or bypass ancestor protections. Account for attempted protected-path changes in the expansion design rather than moving the checker to a different lifecycle point.
All relationships in the effective configuration resolve during the load, including entries the application might not select for execution. Missing parents, non-mapping parents, invalid reference values, self-reference, and longer cycles fail the load with safe structured diagnostics and relevant source locations. Traverse deterministically; diagnostics should identify the relationship and chain rather than expose payload values. Synthetic inputs retain labels and semantic paths without fabricated file coordinates. Normal syntax and strict-composition errors remain unmaskable.
Partial base entries may omit domain-required settings. Omission is not by itself a structural error. All returned entries still undergo ordinary typed decoding; application validation determines which entries need to be independently executable and which are reusable partial definitions. This adds no alternate schema, automatic abstract-entry marker, or general exemption from validation.
Successful loading yields the completed configuration and metadata. Inheritance performs no background resolution and retains no filesystem handles. Applications may load again explicitly using the existing source-reuse contracts.
Presence, provenance, and confidentiality
Consumers must distinguish direct child input, inherited input, typed defaults, and recorded application-derived values. Explicit zero/false/empty/null settings remain distinguishable from omission. Go zero-value inference is not an acceptable substitute.
An inherited value counts as present configuration input: IsExplicit returns
true at its destination, while derivation metadata distinguishes inherited input
from a direct child declaration. Presence answers whether configuration supplied
the value; provenance explains where it came from and how it reached the destination.
Typed defaults and application resolver defaults based on absence therefore remain
below inherited settings, including inherited zero, false, empty, and null values.
The API documentation must explain this effective-input meaning of explicitness;
it does not mean the value was written directly at the destination path.
Each inherited value retains its originating source and source path, together with the relationship that placed it at the child's destination path. Chains remain explainable, including a child's explicit replacement of an inherited value. Source-precedence history remains inspectable alongside inheritance derivation; inheritance must not masquerade as another acquired file or source tier. Effective metadata, resolver-entry queries, history, explanation, and redacted rendering must agree.
A relative resource inherited from a parent keeps the context of the source that supplied that resource value. It does not acquire the child's directory. Composite origins remain composite and derived dependencies remain explicit.
Inherit secret references as unresolved configuration input; resolve typed credentials only at the existing lifecycle point. Preserve source and destination sensitivity through inheritance dependencies so copying a classified value cannot make it public. A child's independent replacement has its own provenance; inherited or displaced historical secrets remain safely explainable and redacted. No raw credential values belong in inheritance trees, metadata, or diagnostics.
Efficiency and ownership
Model each sibling scope as entries and single-parent edges. Use indexed name lookup, cycle detection, and memoized parent resolution to avoid repeated ancestor work. Materialize independent public results while allowing carefully owned immutable private structures to share work. Do not introduce global caches or reload sources for individual entries. Cache enclosing expansion separately from destination-local nested resolution, so shared work does not change reference meaning.
Assess work against input size, relationship edges, materialized effective values, and retained provenance/history. Many children can legitimately produce much more output than the source declarations. Use representative chains, shared-base trees, wide collections, and replacement cases to evaluate growth. Performance evidence is not a new release timing gate.
Existing acquired-input accounting remains in force. Expanded output and retained derivation respect existing decoded tree/history limits; the implementation plan defines accounting and check placement. Avoid silently charging one inherited value as a newly acquired source or inventing arbitrary entry/chain caps. Any necessary new public limit requires an explicit design decision.
Work needed from the baseline
The feature requires built-in recognition of extends, a private dependency-aware
expansion phase with destination-local nested resolution, and lifecycle integration
before typed decoding. No collection-registration API is needed. Presence,
derivation history, diagnostics, sensitivity propagation, and source-relative
resource origins must extend together. The public parser-independent facade
remains the only consumer boundary.
Representative examples and behavioral tests should establish source/inheritance ordering, chains and shared parents, enclosing-first nested resolution, independence from parent/child name sort order, failure diagnostics, partial bases, explicit values versus omission, cancellation of relationships with null, invalid reference positions and types, inherited presence during defaulting, unchanged sequence/null behavior, origin/resource preservation, result independence, and safe secret inspection. Both loading entry points and supported source representations should share those semantics.
A new ADR should record the selected inheritance architecture.
Architecture/deferred-scope documentation must recognize
the new structural responsibility; public API, configuration, consumer, and
internal documentation should describe implemented behavior when it lands.
Implementation sequencing belongs in the implementation plan. Reserving extends
changes behavior for applications that currently use it as ordinary data. Document
that compatibility change and the required migration rather than describing this
as a purely additive opt-in feature. Configurations without extends retain their
existing behavior. Release preparation and publication are separate work.
Completion criteria
A consumer can use extends between sibling mappings, load mixed-source
configuration once, and receive resolved independent entries with one set of combination rules.
It can inspect whether each setting was direct, inherited, defaulted, or derived,
follow inherited resource origins, and validate final domain meaning. Missing or
cyclic relationships fail before a usable result is returned. The consumer needs
no separate graph resolver, generic merge pass, or provenance reconstruction.