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 explanation commands around the library. Broadening these boundaries requires
concrete consumer evidence, compatibility review, and an explicit design change. 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 [named-inheritance roadmap](named-inheritance.md), based on the
[downstream request](feature-request-promptkit-inheritance.md). It is not yet [downstream request](feature-request-promptkit-inheritance.md). It is not yet
implemented and is distinct from inheritance between selected overlay files. 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 Status: proposed feature design for agreement; not implemented. This document
defines the intended end state and the remaining architectural decisions. A 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 ## Purpose and priorities
Allow an application to designate a collection of named configuration entries Allow a configuration mapping to inherit settings from one sibling mapping by
whose members can inherit settings from one parent in that collection. ConfigKit using the built-in `extends` keyword. No collection registration, configurable
resolves the relationships during loading and returns final typed configuration 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, with inspectable provenance. Applications retain ownership of field meanings,
domain validation, and execution. 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 ## Target configuration model
### Explicit collection declarations ### Built-in keyword and sibling lookup
Applications opt in particular string-keyed collections through their reusable A mapping containing `extends: foo` inherits from the mapping named `foo` in
configuration definition, consistently for ordinary and layered loading. Each its immediately enclosing mapping. Lookup uses an exact sibling key, independent
mapping key is the entry's identity. References use exact names within the of declaration order. It does not interpret names as paths, search enclosing
declared collection, independent of filesystem namespaces or declaration order. scopes, or search unrelated branches. One mapping can name one parent; chains
One entry can name one parent; chains and shared parents are supported. and shared parents are supported.
The declaration identifies structural metadata rather than enabling callbacks ```yaml
that choose arbitrary lookup or merge strategies. Ordinary fields remain ordinary baz:
data outside declared collections. The declaration syntax and treatment of the foo:
parent-reference field are addressed in Open Decisions below. width: 10
length: 20
bar:
extends: foo
width: 20
```
Mappings with no parent remain independent entries. Explicit null or scalar The resulting `bar` has `width: 20` and `length: 20`. ConfigKit consumes the
entries retain their ordinary data meaning and are subject to normal final `extends` field as structural metadata, retaining its provenance for inspection
schema validation; an entry used as a parent must resolve to a mapping so its rather than passing it to the application's typed payload.
settings can combine with the child. Missing collections contribute no entries.
A present collection must have the mapping shape required by its declaration.
Identity comes from the collection key, not a copied field in the parent payload. `extends` becomes reserved configuration syntax throughout input mappings,
The child keeps its key. Application fields such as `name` or `id` have ordinary consistently for ordinary and layered loading and every supported source kind.
combination semantics unless consumed as explicitly declared structural metadata. 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 ### Source precedence first, inheritance second
First assemble the effective input under the existing source rules: strict First assemble the effective input under the existing source rules: strict
composition within tiers and recursive overlay across underlays, main input, composition within tiers and recursive overlay across underlays, main input,
selected overlay, and explicit CLI overrides. Strict conflicts remain errors. 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 Every parent reference sees that parent's effective definition, including source
overrides. For each entry, resolve the parent's chain, then recursively overlay 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. - Explicit null replaces the inherited value, subject to final schema acceptance.
- Cross-kind replacement follows the same overlay rules and final schema checks. - 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 parent's own `extends` relationship is consumed when its parent chain is
The child supplies its own relationship. Inherited payload values can themselves expanded; it is not copied as the child's own relationship. Nested `extends`
contain ordinary strings or fields with similar names without acquiring reference fields remain structural instructions and travel with the nested payload until
semantics outside the declared position. that destination scope is resolved, as described below.
These rules intentionally differ from the request's suggested null-as-unspecified These rules intentionally differ from the request's suggested null-as-unspecified
and whole-parameter-map replacement. Consumers adopting the feature adopt and whole-parameter-map replacement. Consumers adopting the feature adopt
ConfigKit's shared rules. Multiple inheritance, sequence merging, per-field 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. 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 ## Loading and validation
The proposed lifecycle is: existing acquisition/composition and main-owned version 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 Preserve the existing version owner's declaration and checking point. Inheritance
must not supply, replace, or remove the protected version path or bypass ancestor must not supply, replace, or remove the protected version path or bypass ancestor
protections. Identify attempted protected-path changes explicitly during design protections. Account for attempted protected-path changes in the expansion
of declarations rather than moving the checker to a different lifecycle point. design rather than moving the checker to a different lifecycle point.
All declared relationships resolve during the load, including entries the All relationships in the effective configuration resolve during the load, including
application might not select for execution. Missing parents, non-mapping parents, 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 invalid reference values, self-reference, and longer cycles fail the load with
safe structured diagnostics and relevant source locations. Traverse deterministically; safe structured diagnostics and relevant source locations. Traverse deterministically;
diagnostics should identify the relationship and chain rather than expose payload 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 ## 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. cycle detection, and memoized parent resolution to avoid repeated ancestor work.
Materialize independent public results while allowing carefully owned immutable Materialize independent public results while allowing carefully owned immutable
private structures to share work. Do not introduce global caches or reload sources 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, Assess work against input size, relationship edges, materialized effective values,
and retained provenance/history. Many children can legitimately produce much more 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 ## Work needed from the baseline
The feature requires root-owned opt-in declarations, a private relationship graph The feature requires built-in recognition of `extends`, a private dependency-aware
and expansion phase, and lifecycle integration before typed decoding. Presence, 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 derivation history, diagnostics, sensitivity propagation, and source-relative
resource origins must extend together. The public parser-independent facade resource origins must extend together. The public parser-independent facade
remains the only consumer boundary. remains the only consumer boundary.
Representative examples and behavioral tests should establish source/inheritance 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 values versus omission, unchanged sequence/null behavior, origin/resource
preservation, result independence, and safe secret inspection. Both loading entry preservation, result independence, and safe secret inspection. Both loading entry
points and supported source representations should share those semantics. 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 decisions are resolved. Architecture/deferred-scope documentation must recognize
the new structural responsibility; public API, configuration, consumer, and the new structural responsibility; public API, configuration, consumer, and
internal documentation should describe implemented behavior when it lands. internal documentation should describe implemented behavior when it lands.
Implementation sequencing belongs in a separate future plan. Existing callers Implementation sequencing belongs in a separate future plan. Reserving `extends`
that do not opt in retain their behavior; an additive minor release is the target, changes behavior for applications that currently use it as ordinary data. Document
with release preparation and publication handled separately. 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 ## Completion criteria
A consumer can declare a named collection, load mixed-source configuration once, A consumer can use `extends` between sibling mappings, load mixed-source
and receive resolved independent entries with one set of combination rules. 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, It can inspect whether each setting was direct, inherited, defaulted, or derived,
follow inherited resource origins, and validate final domain meaning. Missing or follow inherited resource origins, and validate final domain meaning. Missing or
cyclic relationships fail before a usable result is returned. The consumer needs 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 ## 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 **Recommendation:** an omitted `extends` means no relationship; explicit null
collection path and one direct parent-reference field name within each entry. cancels a relationship supplied by a lower-precedence source or inherited nested
Consume that field as structural metadata during expansion and retain its source payload. A string names an exact sibling key, including an empty string if that
location for inspection. An omitted reference means no parent; explicit null key exists. Other value shapes are reference errors. This follows existing
removes a lower-tier relationship. A string names an exact key in the effective presence and replacement rules without introducing a deletion operator.
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.
**Alternative:** declare a separate name-to-parent table at a designated input A string-valued `extends` on the document root or directly on a sequence-element
path. Payload fields never overlap control metadata and application structs need mapping has no enclosing mapping of named siblings. Report a location-bearing
no inheritance field, but settings and relationships become separated and must reference error rather than searching elsewhere. Mappings nested within those
be kept consistent. It also introduces a second keyed collection into diagnostics structures can still use their own sibling namespaces normally. This follows
and source composition for each inheritance relationship set. 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 ### 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 direct-declaration meaning of explicitness, but requires two presence concepts
through the resolver lifecycle and risks defaults overwriting inherited settings through the resolver lifecycle and risks defaults overwriting inherited settings
unless callers adopt the new query consistently. 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.