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

282 lines
16 KiB
Markdown

# Opt-In Named Configuration Inheritance
Status: proposed feature design for agreement; not implemented. This document
defines the intended end state and the remaining architectural decisions. A
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
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
### Explicit collection declarations
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.
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.
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.
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.
### 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.
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 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.
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
file inheritance are outside this feature's initial scope.
## 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. Identify attempted protected-path changes explicitly during design
of declarations 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,
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. The representation of inherited presence
in the existing query surface is an open decision below; Go zero-value inference
is not an acceptable substitute.
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 collection 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.
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. The eventual implementation
plan must define how expanded output and retained derivation fit existing decoded
tree/history limits, including where expansion is checked. 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 root-owned opt-in declarations, a private relationship graph
and expansion phase, and lifecycle integration before typed decoding. 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
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.
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.
## 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.
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.
## Open Decisions
### 1. Declaring collections and parent references
**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.
**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.
### 2. Meaning of inherited presence
**Recommendation:** an inherited value counts as present input for effective
`IsExplicit`/defaulting behavior, with separate derivation metadata distinguishing
it from direct child input. This treats inheritance as input expansion, keeps
typed and resolver defaults below inherited settings, and avoids consumers
reimplementing presence logic. The API must clearly explain that effective
presence differs from direct declaration at the destination path.
**Alternative:** inherited values remain non-explicit at the child path, with
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.