282 lines
16 KiB
Markdown
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.
|