319 lines
17 KiB
Markdown
319 lines
17 KiB
Markdown
# Built-In Sibling Configuration Inheritance
|
|
|
|
Status: agreed feature design; not implemented. This document defines the intended
|
|
end state. The [implementation plan](implementation.md) defines execution steps.
|
|
|
|
## 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](feature-request-promptkit-inheritance.md) 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.
|
|
|
|
## Current baseline
|
|
|
|
Baseline: `b604aa167b684525f0d5d806d9c4267824151ee4`, after the filesystem-source
|
|
and underlay implementation and its remediation.
|
|
|
|
- The [configuration contract](../config.md) 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](../policy/architecture.md#success-criteria), the
|
|
composition distinction in [ADR 0001](../adr/0001-separate-composition-and-overlay.md),
|
|
the presence/provenance principles in
|
|
[ADR 0003](../adr/0003-preserve-presence-and-resolution-history.md), and the
|
|
complete-on-return design in [draft ADR 0008](../adr/0008-complete-on-return-loading.md).
|
|
|
|
## 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.
|
|
|
|
```yaml
|
|
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.
|
|
|
|
```yaml
|
|
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.
|