Revise inheritance roadmap around built-in sibling references

This commit is contained in:
2026-09-29 21:10:04 +00:00
parent 0a4b774bfe
commit ac1eec2e30
2 changed files with 123 additions and 74 deletions

View File

@@ -28,7 +28,7 @@ Applications may build their own validate, effective-value, source, bootstrap, a
explanation commands around the library. Broadening these boundaries requires
concrete consumer evidence, compatibility review, and an explicit design change.
Opt-in inheritance between named configuration entries is proposed in the
Built-in sibling inheritance using `extends` is proposed in the
[named-inheritance roadmap](named-inheritance.md), based on the
[downstream request](feature-request-promptkit-inheritance.md). It is not yet
implemented and is distinct from inheritance between selected overlay files.

View File

@@ -1,4 +1,4 @@
# Opt-In Named Configuration Inheritance
# Built-In Sibling Configuration Inheritance
Status: proposed feature design for agreement; not implemented. This document
defines the intended end state and the remaining architectural decisions. A
@@ -6,9 +6,10 @@ separate implementation plan will define execution steps after design agreement.
## Purpose and priorities
Allow an application to designate a collection of named configuration entries
whose members can inherit settings from one parent in that collection. ConfigKit
resolves the relationships during loading and returns final typed configuration
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.
@@ -50,35 +51,47 @@ complete-on-return design in [draft ADR 0008](../adr/0008-complete-on-return-loa
## Target configuration model
### Explicit collection declarations
### Built-in keyword and sibling lookup
Applications opt in particular string-keyed collections through their reusable
configuration definition, consistently for ordinary and layered loading. Each
mapping key is the entry's identity. References use exact names within the
declared collection, independent of filesystem namespaces or declaration order.
One entry can name one parent; chains and shared parents are supported.
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.
The declaration identifies structural metadata rather than enabling callbacks
that choose arbitrary lookup or merge strategies. Ordinary fields remain ordinary
data outside declared collections. The declaration syntax and treatment of the
parent-reference field are addressed in Open Decisions below.
```yaml
baz:
foo:
width: 10
length: 20
bar:
extends: foo
width: 20
```
Mappings with no parent remain independent entries. Explicit null or scalar
entries retain their ordinary data meaning and are subject to normal final
schema validation; an entry used as a parent must resolve to a mapping so its
settings can combine with the child. Missing collections contribute no entries.
A present collection must have the mapping shape required by its declaration.
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.
Identity comes from the collection key, not a copied field in the parent payload.
The child keeps its key. Application fields such as `name` or `id` have ordinary
combination semantics unless consumed as explicitly declared structural metadata.
`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-value details and positions without a sibling
namespace are addressed in Open Decisions below.
### 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 collection.
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
@@ -103,17 +116,61 @@ Parent-to-child combination uses the existing recursive mapping overlay:
- 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 inheritance-control metadata is not inherited as a payload setting.
The child supplies its own relationship. Inherited payload values can themselves
contain ordinary strings or fields with similar names without acquiring reference
semantics outside the declared position.
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-collection references, and named-overlay
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. The treatment of a relationship on a
sequence element itself is addressed below.
## Loading and validation
The proposed lifecycle is: existing acquisition/composition and main-owned version
@@ -130,11 +187,11 @@ 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. Identify attempted protected-path changes explicitly during design
of declarations rather than moving the checker to a different lifecycle point.
protections. Account for attempted protected-path changes in the expansion
design rather than moving the checker to a different lifecycle point.
All declared relationships resolve during the load, including entries the
application might not select for execution. Missing parents, non-mapping parents,
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
@@ -180,11 +237,12 @@ credential values belong in inheritance trees, metadata, or diagnostics.
## Efficiency and ownership
Model each collection as entries and single-parent edges. Use indexed name lookup,
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.
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
@@ -200,14 +258,16 @@ caps. Any necessary new public limit requires an explicit design decision.
## Work needed from the baseline
The feature requires root-owned opt-in declarations, a private relationship graph
and expansion phase, and lifecycle integration before typed decoding. Presence,
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, failure diagnostics, partial bases, explicit
ordering, chains and shared parents, enclosing-first nested resolution, independence
from parent/child name sort order, failure diagnostics, partial bases, explicit
values versus omission, 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.
@@ -216,14 +276,16 @@ A new ADR should record the selected inheritance architecture after the open
decisions are resolved. 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 a separate future plan. Existing callers
that do not opt in retain their behavior; an additive minor release is the target,
with release preparation and publication handled separately.
Implementation sequencing belongs in a separate future 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 declare a named collection, load mixed-source configuration once,
and receive resolved independent entries with one set of combination rules.
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
@@ -231,23 +293,26 @@ no separate graph resolver, generic merge pass, or provenance reconstruction.
## Open Decisions
### 1. Declaring collections and parent references
### 1. Reference values and positions without a sibling namespace
**Recommendation:** a root-owned declaration in `Definition` identifies a
collection path and one direct parent-reference field name within each entry.
Consume that field as structural metadata during expansion and retain its source
location for inspection. An omitted reference means no parent; explicit null
removes a lower-tier relationship. A string names an exact key in the effective
collection, including an empty string if such an entry exists. Other shapes are
reference errors. Outside the opted-in position, the field remains ordinary data.
This integrates naturally with existing source precedence and avoids a globally
reserved `extends` field or duplicated entry identifiers.
**Recommendation:** an omitted `extends` means no relationship; explicit null
cancels a relationship supplied by a lower-precedence source or inherited nested
payload. A string names an exact sibling key, including an empty string if that
key exists. Other value shapes are reference errors. This follows existing
presence and replacement rules without introducing a deletion operator.
**Alternative:** declare a separate name-to-parent table at a designated input
path. Payload fields never overlap control metadata and application structs need
no inheritance field, but settings and relationships become separated and must
be kept consistent. It also introduces a second keyed collection into diagnostics
and source composition for each inheritance relationship set.
A string-valued `extends` on the document root or directly on a sequence-element
mapping has no enclosing mapping of named siblings. Report a location-bearing
reference error rather than searching elsewhere. Mappings nested within those
structures can still use their own sibling namespaces normally. This follows
the selected lookup rule, rather than imposing a general ban on nested inheritance.
**Alternative:** require every present `extends` to be a string. This makes the
keyword's value type simpler, but leaves no direct way to cancel an existing
relationship while retaining the mapping's other fields. For root/sequence-element
references, an ancestor-scope fallback could supply a namespace, but would introduce
a positional exception to the agreed immediate-sibling rule; adopting it would
require revisiting that decision rather than treating it as an implementation detail.
### 2. Meaning of inherited presence
@@ -263,19 +328,3 @@ a new effective-input-presence query/defaulting rule. This preserves a narrow
direct-declaration meaning of explicitness, but requires two presence concepts
through the resolver lifecycle and risks defaults overwriting inherited settings
unless callers adopt the new query consistently.
### 3. Nested or overlapping collection declarations
**Recommendation:** initially support multiple independent, non-overlapping
declared collection paths. Report overlapping declarations as an options error.
An outer inheritance expansion can copy or replace an inner collection, so
overlap otherwise needs a selected rule for whether inner inheritance resolves
before copying or after arrival at each destination. This restriction makes that
unresolved interaction explicit rather than allowing declaration order to choose
semantics. It does not restrict ordinary nested payload mappings.
**Alternative:** define destination-based outer-to-inner expansion, resolving each
inner collection after the enclosing entry is materialized. This supports nested
presets, but creates additional namespace, dependency, and provenance semantics
and can multiply graph instances. It is viable if a concrete consumer need
justifies that additional model; it should not emerge accidentally from traversal.