Files
ConfigKit/docs/roadmap/named-inheritance.md

320 lines
17 KiB
Markdown

# Built-In Sibling Configuration Inheritance
Status: implemented. The [configuration contract](../config.md#sibling-inheritance)
owns current behavior. This document preserves the agreed design and its
baseline; the [implementation plan](implementation.md) 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](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.
## Baseline at design time
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.