Document and benchmark sibling inheritance
This commit is contained in:
@@ -18,7 +18,9 @@ tags and a path predicate drive deterministic redacted effective YAML. Typed sec
|
|||||||
references resolve environment names before validation, and explicit
|
references resolve environment names before validation, and explicit
|
||||||
secrets-directory bootstrap is available as a separate startup operation. Explicit
|
secrets-directory bootstrap is available as a separate startup operation. Explicit
|
||||||
CLI override records apply above overlays. Value explanation is available through
|
CLI override records apply above overlays. Value explanation is available through
|
||||||
detached structured records and deterministic redacted output.
|
detached structured records and deterministic redacted output. Built-in
|
||||||
|
`extends` relationships let a mapping inherit from a named sibling during the
|
||||||
|
same load, retaining explicit presence and source/derivation history.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -64,7 +66,7 @@ model. The [deferred scope](docs/roadmap/deferred.md) records deliberate exclusi
|
|||||||
|
|
||||||
The [configuration contract](docs/config.md) describes implemented behavior. The
|
The [configuration contract](docs/config.md) describes implemented behavior. The
|
||||||
[consumer adoption guide](docs/consumers/README.md) provides maintained simple
|
[consumer adoption guide](docs/consumers/README.md) provides maintained simple
|
||||||
native-filesystem and layered examples alongside the migration shapes.
|
native-filesystem, layered, and sibling-inheritance examples alongside the migration shapes.
|
||||||
See the [development guide](docs/development.md) for repository orientation and the
|
See the [development guide](docs/development.md) for repository orientation and the
|
||||||
[release procedure](docs/release.md) for maintainer-only candidate and publication
|
[release procedure](docs/release.md) for maintainer-only candidate and publication
|
||||||
steps.
|
steps.
|
||||||
|
|||||||
76
docs/adr/0009-sibling-inheritance.md
Normal file
76
docs/adr/0009-sibling-inheritance.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# ADR 0009: Resolve Sibling Inheritance After Source Assembly
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted and implemented. The [configuration contract](../config.md#sibling-inheritance)
|
||||||
|
owns current behavior; the [implementation plan](../roadmap/implementation.md)
|
||||||
|
records the delivery sequence.
|
||||||
|
|
||||||
|
## Date
|
||||||
|
|
||||||
|
2026-09-29
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Named configuration mappings often share settings while retaining individual
|
||||||
|
overrides. ConfigKit already distinguishes strict composition of independent
|
||||||
|
source contributions from recursive overlay between explicit precedence tiers.
|
||||||
|
Doing another untracked merge in applications would lose omission, source
|
||||||
|
context, and historical explanation information. A shared structural operation
|
||||||
|
must preserve the same presence and provenance model as the rest of loading.
|
||||||
|
|
||||||
|
Inheritance also interacts with source precedence, nested namespaces, version
|
||||||
|
ownership, and secrets. Resolving it before source assembly would validate
|
||||||
|
superseded references and copy values that a later tier should replace. Fully
|
||||||
|
resolving nested relationships inside a reusable parent would bind them to the
|
||||||
|
wrong sibling scope when copied into a child.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Reserve `extends` as a built-in input key on mappings. A string names one exact
|
||||||
|
sibling mapping; null cancels the relationship. Complete source acquisition,
|
||||||
|
strict composition, selected overlay, and CLI precedence first. Then resolve
|
||||||
|
each effective relationship in the parser-independent document tree, before
|
||||||
|
typed defaults and decoding. A child recursively overlays its own payload on
|
||||||
|
its resolved parent using the existing mapping-combination rules. This creates
|
||||||
|
neither a source tier nor a configurable merge strategy.
|
||||||
|
|
||||||
|
Resolve enclosing sibling chains before nested relationships. Reuse memoized
|
||||||
|
parent templates with unresolved nested instructions; evaluate those instructions
|
||||||
|
at each destination. Preserve original source applications separately from
|
||||||
|
structural transfer/control evidence. Effective origins carry concrete
|
||||||
|
source-to-destination derivation, and inspection projects relevant inheritance
|
||||||
|
events between source replay and typed reconciliation. Inherited input counts as
|
||||||
|
explicit presence, while direct replacement retains its own effective origin.
|
||||||
|
|
||||||
|
The existing main-input version check remains at its original point. Reject
|
||||||
|
structural contributions or directive consumption affecting its protected path.
|
||||||
|
Classify sensitivity at corresponding source and destination paths and retain
|
||||||
|
historical redaction. Secret references remain unresolved input until the
|
||||||
|
existing lookup point. Keep all machinery behind the root-owned public facade.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Application-owned inheritance after `Load`.** Leaves every consumer to
|
||||||
|
recreate ordering, presence, provenance, error reporting, and redaction after
|
||||||
|
ConfigKit has already declared a result complete.
|
||||||
|
- **Resolve each source before precedence assembly.** Makes superseded
|
||||||
|
references active and can copy a parent definition before higher-priority
|
||||||
|
input finishes changing it.
|
||||||
|
- **Resolve nested relationships in a cached parent subtree.** Shares more
|
||||||
|
ready-made output but binds nested sibling lookups to the parent's scope,
|
||||||
|
giving incorrect values at a child destination.
|
||||||
|
- **Registered collections, configurable keywords, or multiple-parent merge
|
||||||
|
modes.** Increase public setup and semantic branches without a requirement
|
||||||
|
that warrants diverging from one structural rule.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Consumers can load a configuration once and inspect direct, inherited,
|
||||||
|
defaulted, and application-derived values through the existing result surfaces.
|
||||||
|
`extends` is no longer an ordinary application field, so existing configurations
|
||||||
|
using that key as data must migrate. Expanding many children can materialize
|
||||||
|
substantially more output than the input contains; projection and retained-history
|
||||||
|
budgets bound that work without charging copies as newly acquired sources.
|
||||||
|
Applications still own domain validation, entry selection, and runtime resource
|
||||||
|
use.
|
||||||
@@ -54,7 +54,8 @@ The current lifecycle is:
|
|||||||
```text
|
```text
|
||||||
validate options → parse the selected source → traverse/compose enabled imports
|
validate options → parse the selected source → traverse/compose enabled imports
|
||||||
→ build schema → check the optional application version → apply a selected overlay
|
→ build schema → check the optional application version → apply a selected overlay
|
||||||
→ apply explicit CLI overrides → create and clone defaults → strictly decode
|
→ apply explicit CLI overrides → expand sibling inheritance
|
||||||
|
→ create and clone defaults → strictly decode
|
||||||
→ recorded application resolution
|
→ recorded application resolution
|
||||||
→ resolve typed secret references → semantic validation → classify secrets
|
→ resolve typed secret references → semantic validation → classify secrets
|
||||||
→ snapshot configuration and metadata
|
→ snapshot configuration and metadata
|
||||||
@@ -152,6 +153,57 @@ The maintained [layered consumer example](consumers/README.md#native-filesystem-
|
|||||||
shows a main host file, two underlay tiers, embedded defaults, a mapping source,
|
shows a main host file, two underlay tiers, embedded defaults, a mapping source,
|
||||||
and explicit resource access without a second configuration pass.
|
and explicit resource access without a second configuration pass.
|
||||||
|
|
||||||
|
### Sibling inheritance
|
||||||
|
|
||||||
|
Any input mapping may declare `extends: name` to inherit from the mapping named
|
||||||
|
`name` among its immediate siblings. Names are exact keys, including the empty
|
||||||
|
string; lookup does not search ancestor or unrelated scopes. The document root
|
||||||
|
and a sequence-element mapping have no sibling namespace for their own string
|
||||||
|
relationship, though mappings nested inside them can have one. An `extends`
|
||||||
|
value of null cancels a relationship without looking up a parent. Other value
|
||||||
|
types, missing or non-mapping parents, and cycles fail the load with value-free
|
||||||
|
diagnostics.
|
||||||
|
|
||||||
|
`extends` is reserved structural input across ordinary and layered loads, every
|
||||||
|
source kind, imports, selected overlays, and explicit CLI overrides. It is
|
||||||
|
consumed before typed decoding and need not appear in the application's Go
|
||||||
|
types. Applications previously using `extends` as an ordinary YAML field must
|
||||||
|
rename that key and its field. No collection registration or keyword setup is
|
||||||
|
required.
|
||||||
|
|
||||||
|
ConfigKit completes strict composition within tiers and precedence assembly
|
||||||
|
between tiers before resolving relationships. An omitted `extends` retains a
|
||||||
|
lower-priority relationship; `extends: null` cancels it while preserving the
|
||||||
|
child's own settings. Superseded references are historical evidence, not active
|
||||||
|
relationships to validate. Each child recursively overlays its own settings
|
||||||
|
over its resolved parent: explicit zero, false, empty string, sequence, and
|
||||||
|
null values replace inherited values, while mappings combine recursively.
|
||||||
|
The child retains its key. All effective relationships resolve, including
|
||||||
|
entries the application may never select for execution. Partial parent
|
||||||
|
definitions undergo normal typed decoding and application-owned validation,
|
||||||
|
not a special structural exemption.
|
||||||
|
|
||||||
|
Enclosing relationships resolve first. Nested instructions carried by a parent
|
||||||
|
are then resolved separately at each destination, so a child's nested sibling
|
||||||
|
override can change an inherited nested relationship. Typed defaults and
|
||||||
|
application-derived values do not start another inheritance pass. Inherited
|
||||||
|
input counts as explicit presence for `Metadata.IsExplicit`,
|
||||||
|
`Resolution.IsExplicit`, and absence-based resolver defaults, even for false,
|
||||||
|
zero, empty, or null input. `Origin.Inheritance` distinguishes it from a direct
|
||||||
|
declaration with concrete source, destination, and controlling directive paths.
|
||||||
|
A direct child replacement has its own effective origin; displaced ancestry
|
||||||
|
remains inspectable in history. An inherited relative resource keeps the
|
||||||
|
declaring source's passive `SourceContext`, not the child's directory.
|
||||||
|
|
||||||
|
The optional schema-version checker still runs once against the main input at
|
||||||
|
its original point. Inheritance cannot add, replace, or consume the protected
|
||||||
|
version path; an unrelated relationship inside an enclosing mapping does not
|
||||||
|
invalidate an unchanged version. Expansion uses the decoded projection
|
||||||
|
node/depth limits without charging copied values as acquired sources;
|
||||||
|
retained evidence uses the existing history-node limit. See the
|
||||||
|
[consumer example](consumers/README.md#named-sibling-inheritance) for one-load
|
||||||
|
usage.
|
||||||
|
|
||||||
### Source resource contexts
|
### Source resource contexts
|
||||||
|
|
||||||
`Result.SourceContext(id)` and callback-scoped `Resolution.SourceContext(id)`
|
`Result.SourceContext(id)` and callback-scoped `Resolution.SourceContext(id)`
|
||||||
@@ -222,8 +274,9 @@ successful result. Callback panics remain programming failures.
|
|||||||
`Result.Metadata` describes every final canonical path. Queries retain explicit
|
`Result.Metadata` describes every final canonical path. Queries retain explicit
|
||||||
key/index distinctions using actual effective and historical container shapes;
|
key/index distinctions using actual effective and historical container shapes;
|
||||||
only parsed numeric tokens are resolved contextually. A descendant that never
|
only parsed numeric tokens are resolved contextually. A descendant that never
|
||||||
existed is not historical merely because an atomic ancestor was replaced. Exact surviving input paths
|
existed is not historical merely because an atomic ancestor was replaced. Exact
|
||||||
are explicit and have a base or overlay origin; omitted paths have a default origin.
|
surviving direct or inherited input paths are explicit and retain their source
|
||||||
|
layer and derivation; omitted paths have a default origin.
|
||||||
An explicit parent mapping does not make omitted children explicit. Null state and
|
An explicit parent mapping does not make omitted children explicit. Null state and
|
||||||
available source coordinates are retained. `History` records the Go-zero or application
|
available source coordinates are retained. `History` records the Go-zero or application
|
||||||
default baseline followed by exact base or overlay definitions, including
|
default baseline followed by exact base or overlay definitions, including
|
||||||
@@ -238,8 +291,9 @@ domain ownership rules remain application responsibilities. The method's Go
|
|||||||
documentation in [metadata.go](../metadata.go) defines enumeration and path-ordering
|
documentation in [metadata.go](../metadata.go) defines enumeration and path-ordering
|
||||||
details.
|
details.
|
||||||
|
|
||||||
History is chronological: a path can have a synthetic default record, its exact base
|
History is chronological: a path can have a synthetic default record, its exact
|
||||||
definition, an overlay replacement, and a recorded resolution operation. A descendant
|
base definition, an overlay replacement, structural inheritance, and a recorded
|
||||||
|
resolution operation. A descendant
|
||||||
query uses the ancestor's path for a whole-container or whole-sequence replacement;
|
query uses the ancestor's path for a whole-container or whole-sequence replacement;
|
||||||
the descendant can therefore retain history after `Lookup` stops reporting it as
|
the descendant can therefore retain history after `Lookup` stops reporting it as
|
||||||
effective. Equal-valued input is still an explicit base definition, while a derived
|
effective. Equal-valued input is still an explicit base definition, while a derived
|
||||||
@@ -255,6 +309,15 @@ operation. Reasons and dependency paths retain first occurrence order with dupli
|
|||||||
removed. Source records remain ordered as selected source, depth-first imports, the
|
removed. Source records remain ordered as selected source, depth-first imports, the
|
||||||
selected overlay, then synthetic CLI records in supplied order.
|
selected overlay, then synthetic CLI records in supplied order.
|
||||||
|
|
||||||
|
Inheritance adds no acquired source or precedence layer. `Origin.Inheritance`
|
||||||
|
lists concrete transfers from contributor paths through destinations, with
|
||||||
|
each step's controlling `extends` path and location. `History` retains source
|
||||||
|
applications at their original paths and projects parent contributions, child
|
||||||
|
replacements, and consumed controls at destination paths after source assembly
|
||||||
|
and before typed decoding. `HistoricalPaths` includes consumed directives and
|
||||||
|
displaced inherited descendants. A control can have history even when `Lookup`
|
||||||
|
reports it absent. Queries return detached records.
|
||||||
|
|
||||||
### Secret classification and effective rendering
|
### Secret classification and effective rendering
|
||||||
|
|
||||||
Secret classification combines `configkit:"secret"` field tags with the optional
|
Secret classification combines `configkit:"secret"` field tags with the optional
|
||||||
@@ -270,6 +333,11 @@ Propagation reaches a fixed point, so record/map iteration order does not affect
|
|||||||
result and dependency cycles terminate. A dependency on a classified descendant or
|
result and dependency cycles terminate. A dependency on a classified descendant or
|
||||||
on a container that contains one is treated conservatively as sensitive. Applications
|
on a container that contains one is treated conservatively as sensitive. Applications
|
||||||
must classify secret-producing transformations whose dependencies are not recorded.
|
must classify secret-producing transformations whose dependencies are not recorded.
|
||||||
|
Inherited value and control paths link source and destination classifications
|
||||||
|
through chains and nested copies; unrelated sibling fields are not classified
|
||||||
|
merely because another field is secret. Displaced historical evidence remains
|
||||||
|
redacted. Secret references are copied only as unresolved names; credential
|
||||||
|
lookup stays after decoding and application resolution.
|
||||||
|
|
||||||
`Result.WriteEffective` writes the frozen resolved snapshot as deterministic redacted
|
`Result.WriteEffective` writes the frozen resolved snapshot as deterministic redacted
|
||||||
YAML. Mappings sort by decoded key string, sequences retain order, indentation is two
|
YAML. Mappings sort by decoded key string, sequences retain order, indentation is two
|
||||||
@@ -304,7 +372,10 @@ parser values. `Present=false` with empty YAML means absence or deletion; explic
|
|||||||
null is present and renders as `null`. The history starts with the canonical typed
|
null is present and renders as `null`. The history starts with the canonical typed
|
||||||
application-default or Go-zero baseline, then includes only relevant base sources in
|
application-default or Go-zero baseline, then includes only relevant base sources in
|
||||||
traversal order, the selected overlay, supplied CLI records, and audited net
|
traversal order, the selected overlay, supplied CLI records, and audited net
|
||||||
resolution operations. Overlapping parent/child resolution records are rendered
|
resolution operations. Structural inheritance events appear after source replay
|
||||||
|
and before typed reconciliation, including consumed or canceled `extends` input
|
||||||
|
and equal-valued child replacements. Overlapping parent/child resolution records
|
||||||
|
are rendered
|
||||||
as one net subtree event with combined reasons and dependencies, rather than
|
as one net subtree event with combined reasons and dependencies, rather than
|
||||||
pretending that the recorded assignments ran in lexical order. Explicit equal
|
pretending that the recorded assignments ran in lexical order. Explicit equal
|
||||||
writes remain visible. When input replay
|
writes remain visible. When input replay
|
||||||
@@ -556,8 +627,8 @@ input document.
|
|||||||
`[a-zA-Z0-9_-]+`; they are not paths. The empty name and exact lowercase name
|
`[a-zA-Z0-9_-]+`; they are not paths. The empty name and exact lowercase name
|
||||||
`default` select no overlay, and `overlays/default.yml` is never loaded implicitly.
|
`default` select no overlay, and `overlays/default.yml` is never loaded implicitly.
|
||||||
Every other name resolves only to `<configuration-root>/overlays/<name>.yml`, with
|
Every other name resolves only to `<configuration-root>/overlays/<name>.yml`, with
|
||||||
no extension fallback, case normalization, inheritance, or overlay stack. A selected
|
no extension fallback, case normalization, inheritance between overlay files,
|
||||||
missing overlay is an error.
|
or overlay stack. A selected missing overlay is an error.
|
||||||
|
|
||||||
The overlay is a separately policy-validated YAML mapping. It is loaded after the
|
The overlay is a separately policy-validated YAML mapping. It is loaded after the
|
||||||
base import graph is composed and its schema version is checked, but before defaults
|
base import graph is composed and its schema version is checked, but before defaults
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ sanitized shapes using only ConfigKit and the Go standard library:
|
|||||||
| Seriatim-like | Independent strict ordered speaker and autocorrect rule files | First-match and ordered replacement algorithms; no central application configuration file |
|
| Seriatim-like | Independent strict ordered speaker and autocorrect rule files | First-match and ordered replacement algorithms; no central application configuration file |
|
||||||
| Bundled file | Simple native `fs.FS` source through ordinary `Load` | Selection and lifetime of the application-owned embedded filesystem |
|
| Bundled file | Simple native `fs.FS` source through ordinary `Load` | Selection and lifetime of the application-owned embedded filesystem |
|
||||||
| Layered catalog | Embedded defaults with an import, a mapping underlay, an independent host main file, explicit false/zero input, and source-relative resource access | Domain validation and opening/interpreting the template resource |
|
| Layered catalog | Embedded defaults with an import, a mapping underlay, an independent host main file, explicit false/zero input, and source-relative resource access | Domain validation and opening/interpreting the template resource |
|
||||||
|
| Named presets | One-load sibling inheritance, explicit false, nested destination resolution, and source/derivation inspection | Choosing and executing a preset |
|
||||||
|
|
||||||
The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the
|
The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the
|
||||||
executable examples and ordinary tests. It contains no private application data,
|
executable examples and ordinary tests. It contains no private application data,
|
||||||
@@ -50,6 +51,22 @@ both without redefining their syntax. ConfigKit overlays are configuration overl
|
|||||||
they do not select or replace an application's LLM, model, pipeline, report, or
|
they do not select or replace an application's LLM, model, pipeline, report, or
|
||||||
execution profile concepts.
|
execution profile concepts.
|
||||||
|
|
||||||
|
## Named sibling inheritance
|
||||||
|
|
||||||
|
[`LoadPresets`](../../examples/consumers/inheritance.go) loads the maintained
|
||||||
|
[`presets.yml`](../../examples/consumers/testdata/inheritance/presets.yml) in one
|
||||||
|
call. `custom` inherits `enabled: false` from `base`, while its nested
|
||||||
|
`settings.primary` changes the inherited `settings.secondary` result at the
|
||||||
|
custom destination. The application struct has no `extends` field; ConfigKit
|
||||||
|
consumes the relationship before strict typed decoding.
|
||||||
|
|
||||||
|
The [executable example](../../examples/consumers/consumers_test.go) reads
|
||||||
|
`Metadata.IsExplicit` for the inherited false value, then follows
|
||||||
|
`Origin.Inheritance` back to the parent path while `Origin.Locations` identifies
|
||||||
|
the declaring source. Applications still choose which preset to execute and
|
||||||
|
validate domain meaning. The [configuration contract](../config.md#sibling-inheritance)
|
||||||
|
owns exact lookup, precedence, cancellation, and migration rules.
|
||||||
|
|
||||||
## Native filesystem and layered input
|
## Native filesystem and layered input
|
||||||
|
|
||||||
[`LoadBundled`](../../examples/consumers/layered.go) keeps the simple `Load` shape:
|
[`LoadBundled`](../../examples/consumers/layered.go) keeps the simple `Load` shape:
|
||||||
@@ -150,6 +167,7 @@ focused behavioral owner:
|
|||||||
| Canonical module objects and dynamic option handling | WeatherReporter- and Notarius-shaped examples and migration tests |
|
| Canonical module objects and dynamic option handling | WeatherReporter- and Notarius-shaped examples and migration tests |
|
||||||
| `LoadSecretsDir`, bootstrap reporting, typed lookup, CLI explanation | `Example_weatherStartup` |
|
| `LoadSecretsDir`, bootstrap reporting, typed lookup, CLI explanation | `Example_weatherStartup` |
|
||||||
| Native filesystem, grouped underlays, programmatic mapping, retained resource context | `Example_nativeFilesystem`, `Example_layeredCatalog`, and their focused consumer tests |
|
| Native filesystem, grouped underlays, programmatic mapping, retained resource context | `Example_nativeFilesystem`, `Example_layeredCatalog`, and their focused consumer tests |
|
||||||
|
| Named sibling inheritance, presence, and derivation | `Example_inheritedPresets` and `TestPresetCatalogOneLoadAndDerivation` |
|
||||||
| Results, metadata, diagnostics, effective output, and writer failures | Focused root-package load, provenance, rendering, and explanation tests |
|
| Results, metadata, diagnostics, effective output, and writer failures | Focused root-package load, provenance, rendering, and explanation tests |
|
||||||
|
|
||||||
The package-level import-boundary test covers the consumer examples too: supported
|
The package-level import-boundary test covers the consumer examples too: supported
|
||||||
|
|||||||
@@ -83,8 +83,9 @@ and current facade ownership belongs to the [component overview](internal/overvi
|
|||||||
The completed filesystem-source and underlay roadmap, original feature request,
|
The completed filesystem-source and underlay roadmap, original feature request,
|
||||||
implementation plan, and resolved remediation record are preserved in Git history
|
implementation plan, and resolved remediation record are preserved in Git history
|
||||||
at `b604aa1` under `docs/roadmap/`. Current behavior belongs to the
|
at `b604aa1` under `docs/roadmap/`. Current behavior belongs to the
|
||||||
[configuration contract](config.md#layered-loading). Named-entry inheritance is
|
[configuration contract](config.md#layered-loading). Built-in sibling
|
||||||
separate proposed work in its [roadmap](roadmap/named-inheritance.md).
|
inheritance is now part of the [configuration contract](config.md#sibling-inheritance);
|
||||||
|
its [roadmap](roadmap/named-inheritance.md) preserves the design background.
|
||||||
|
|
||||||
The completed overlay rename plan and release-workflow simplification record are
|
The completed overlay rename plan and release-workflow simplification record are
|
||||||
preserved in Git history at `73ce6e9` under `docs/roadmap/`. Current overlay
|
preserved in Git history at `73ce6e9` under `docs/roadmap/`. Current overlay
|
||||||
@@ -141,6 +142,7 @@ code changes. Use the component overview to find the corresponding implementatio
|
|||||||
| File/reader access, imports, globs, containment | [Sources](../internal/engine/source_test.go), [FIFO](../internal/engine/source_fifo_test.go), [imports](../internal/engine/imports_test.go), [globs](../internal/engine/glob_test.go) | `go test ./internal/engine -run 'Source\|Import\|Glob\|Filesystem\|Nonregular\|RegularFile'` |
|
| File/reader access, imports, globs, containment | [Sources](../internal/engine/source_test.go), [FIFO](../internal/engine/source_fifo_test.go), [imports](../internal/engine/imports_test.go), [globs](../internal/engine/glob_test.go) | `go test ./internal/engine -run 'Source\|Import\|Glob\|Filesystem\|Nonregular\|RegularFile'` |
|
||||||
| Native filesystem and mapping sources | [Native acquisition](../internal/engine/native_filesystem_test.go), [filesystem facade](../facade_filesystem_test.go), [mapping facade](../facade_mapping_test.go) | `go test ./internal/engine -run 'NativeFilesystem'` and `go test . -run 'TestFacade'` |
|
| Native filesystem and mapping sources | [Native acquisition](../internal/engine/native_filesystem_test.go), [filesystem facade](../facade_filesystem_test.go), [mapping facade](../facade_mapping_test.go) | `go test ./internal/engine -run 'NativeFilesystem'` and `go test . -run 'TestFacade'` |
|
||||||
| Composition, selected overlays, CLI precedence | [Composition](../internal/engine/compose_test.go), [overlay](../internal/engine/overlay_test.go), [selected overlays](../internal/engine/selected_overlay_test.go), [overrides](../internal/engine/override_test.go) | `go test ./internal/engine -run 'Compose\|Overlay\|Override'` |
|
| Composition, selected overlays, CLI precedence | [Composition](../internal/engine/compose_test.go), [overlay](../internal/engine/overlay_test.go), [selected overlays](../internal/engine/selected_overlay_test.go), [overrides](../internal/engine/override_test.go) | `go test ./internal/engine -run 'Compose\|Overlay\|Override'` |
|
||||||
|
| Sibling inheritance, provenance, history, sensitivity | [Expansion](../internal/engine/inheritance_test.go), [destination scopes](../internal/engine/inheritance_walk_test.go), [origins](../internal/engine/inheritance_origin_test.go), [history](../internal/engine/inheritance_history_test.go), [secrets](../internal/engine/inheritance_secret_test.go), [public loading](../facade_inheritance_test.go) | `go test ./internal/engine -run 'Inheritance\|Inherited'` and `go test . -run '^TestFacadeInheritance\|^TestFacadeInheritedSecrets'` |
|
||||||
| Layered loading and source equivalence | [Grouped lifecycle](../internal/engine/load_request_test.go), [layered facade](../facade_layered_test.go), [equivalence](../facade_equivalence_test.go) | `go test ./internal/engine -run '^TestGrouped'`, `go test . -run '^TestFacadeLayered'`, and `go test . -run '^TestEquivalentSourceForms'` |
|
| Layered loading and source equivalence | [Grouped lifecycle](../internal/engine/load_request_test.go), [layered facade](../facade_layered_test.go), [equivalence](../facade_equivalence_test.go) | `go test ./internal/engine -run '^TestGrouped'`, `go test . -run '^TestFacadeLayered'`, and `go test . -run '^TestEquivalentSourceForms'` |
|
||||||
| Schema, defaults, projection, cloning | [Schema](../internal/engine/schema_test.go), [decoding](../internal/engine/decode_test.go), [projection](../internal/engine/projection_test.go) | `go test ./internal/engine -run 'Schema\|Decode\|Projection\|Clone\|Inline\|CompareDocument'` |
|
| Schema, defaults, projection, cloning | [Schema](../internal/engine/schema_test.go), [decoding](../internal/engine/decode_test.go), [projection](../internal/engine/projection_test.go) | `go test ./internal/engine -run 'Schema\|Decode\|Projection\|Clone\|Inline\|CompareDocument'` |
|
||||||
| Lifecycle, resolution, validation, callback ownership | [Loading](../internal/engine/load_test.go), [resolution](../internal/engine/resolution_test.go), [validation](../internal/engine/validation_test.go), [ownership](../internal/engine/callback_ownership_test.go), [independence](../internal/engine/independence_test.go) | `go test ./internal/engine -run 'Load\|Resolution\|Validation\|Validator\|Hook\|Callback\|Parallel'` |
|
| Lifecycle, resolution, validation, callback ownership | [Loading](../internal/engine/load_test.go), [resolution](../internal/engine/resolution_test.go), [validation](../internal/engine/validation_test.go), [ownership](../internal/engine/callback_ownership_test.go), [independence](../internal/engine/independence_test.go) | `go test ./internal/engine -run 'Load\|Resolution\|Validation\|Validator\|Hook\|Callback\|Parallel'` |
|
||||||
@@ -149,6 +151,7 @@ code changes. Use the component overview to find the corresponding implementatio
|
|||||||
| Secrets, bootstrap, redaction, explanation | [References](../internal/engine/secret_reference_test.go), [bootstrap](../internal/engine/secrets_test.go), [rendering](../internal/engine/render_test.go), [explanation](../internal/engine/explain_test.go) | `go test ./internal/engine -run 'Secret\|Bootstrap\|Effective\|Explain\|Explanation\|History\|Sensitivity'` |
|
| Secrets, bootstrap, redaction, explanation | [References](../internal/engine/secret_reference_test.go), [bootstrap](../internal/engine/secrets_test.go), [rendering](../internal/engine/render_test.go), [explanation](../internal/engine/explain_test.go) | `go test ./internal/engine -run 'Secret\|Bootstrap\|Effective\|Explain\|Explanation\|History\|Sensitivity'` |
|
||||||
| Consumer examples and migration fixtures | [Consumer tests](../examples/consumers/consumers_test.go), [native and layered examples](../examples/consumers/layered_test.go), [API examples](../example_test.go) | `go test ./examples/consumers` and `go test . -run '^Example'` |
|
| Consumer examples and migration fixtures | [Consumer tests](../examples/consumers/consumers_test.go), [native and layered examples](../examples/consumers/layered_test.go), [API examples](../example_test.go) | `go test ./examples/consumers` and `go test . -run '^Example'` |
|
||||||
| Assembly and layered scaling evidence | [Assembly benchmarks](../internal/engine/assembly_bench_test.go), [layered benchmarks](../internal/engine/underlay_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkUnderlay' -benchmem`; repeat with `^BenchmarkCompose` and `^BenchmarkOverlay` |
|
| Assembly and layered scaling evidence | [Assembly benchmarks](../internal/engine/assembly_bench_test.go), [layered benchmarks](../internal/engine/underlay_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkUnderlay' -benchmem`; repeat with `^BenchmarkCompose` and `^BenchmarkOverlay` |
|
||||||
|
| Inheritance scaling evidence | [Expansion benchmarks](../internal/engine/inheritance_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkInheritance' -benchmem` |
|
||||||
| Documentation links and dependency isolation | [Link checks](../documentation_test.go), [import boundary](../import_boundary_test.go), [public boundary](../public_boundary_test.go) | `go test . -run 'TestLocalDocumentationLinks\|TestDependencyImportBoundary\|TestPublicFacadeBoundary'` |
|
| Documentation links and dependency isolation | [Link checks](../documentation_test.go), [import boundary](../import_boundary_test.go), [public boundary](../public_boundary_test.go) | `go test . -run 'TestLocalDocumentationLinks\|TestDependencyImportBoundary\|TestPublicFacadeBoundary'` |
|
||||||
| Development and release checker behavior | [Shell regression](../release_check_test.go) | `go test . -run 'TestCheckScripts\|TestDevelopmentCheck'` |
|
| Development and release checker behavior | [Shell regression](../release_check_test.go) | `go test . -run 'TestCheckScripts\|TestDevelopmentCheck'` |
|
||||||
| Guarded release publication and recovery | [Publication regression](../release_publish_test.go) | `go test . -run '^TestReleasePublication'` |
|
| Guarded release publication and recovery | [Publication regression](../release_publish_test.go) | `go test . -run '^TestReleasePublication'` |
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ private state; public signatures and fields do not expose engine types.
|
|||||||
Implemented component owners:
|
Implemented component owners:
|
||||||
|
|
||||||
- `internal/engine` owns host and native-filesystem source acquisition,
|
- `internal/engine` owns host and native-filesystem source acquisition,
|
||||||
grouped strict composition and recursive precedence assembly, schema
|
grouped strict composition, recursive precedence assembly, and destination-local
|
||||||
inspection, decoding, lifecycle coordination, callback auditing,
|
sibling inheritance before typed decoding. It also owns schema
|
||||||
|
inspection, lifecycle coordination, callback auditing,
|
||||||
provenance/history, passive source-context retention, secret resolution and
|
provenance/history, passive source-context retention, secret resolution and
|
||||||
bootstrap, and safe inspection rendering. It never imports the root package.
|
bootstrap, and safe inspection rendering. It never imports the root package.
|
||||||
- Root `Source`, `Filesystem`, `Input`, `Underlay`, `Path`, `Secret`, records,
|
- Root `Source`, `Filesystem`, `Input`, `Underlay`, `Path`, `Secret`, records,
|
||||||
@@ -22,7 +23,8 @@ Implemented component owners:
|
|||||||
public engine bridge methods.
|
public engine bridge methods.
|
||||||
- `internal/document` owns the parser-independent ordered tree and cloning.
|
- `internal/document` owns the parser-independent ordered tree and cloning.
|
||||||
`internal/yaml` is the sole upstream YAML adapter and parser/dumper boundary.
|
`internal/yaml` is the sole upstream YAML adapter and parser/dumper boundary.
|
||||||
- Engine mechanism tests own source, composition, decoding, callback, provenance,
|
- Engine mechanism tests own source, composition, inheritance, decoding,
|
||||||
|
callback, provenance,
|
||||||
source-context, secret, history, rendering, and bootstrap behavior. Root tests
|
source-context, secret, history, rendering, and bootstrap behavior. Root tests
|
||||||
own public API compatibility, facade integration, documentation links, and
|
own public API compatibility, facade integration, documentation links, and
|
||||||
dependency/public boundary checks. Consumer examples remain under
|
dependency/public boundary checks. Consumer examples remain under
|
||||||
@@ -39,4 +41,6 @@ semantics. The [consumer adoption guide](../consumers/README.md) owns applicatio
|
|||||||
guidance. The [architecture policy](../policy/architecture.md) owns contributor
|
guidance. The [architecture policy](../policy/architecture.md) owns contributor
|
||||||
boundaries, and [ADR 0006](../adr/0006-public-facade.md) records the facade
|
boundaries, and [ADR 0006](../adr/0006-public-facade.md) records the facade
|
||||||
decision. See the [development guide](../development.md) for validation commands
|
decision. See the [development guide](../development.md) for validation commands
|
||||||
and [release procedure](../release.md) for source-candidate gates.
|
and [release procedure](../release.md) for source-candidate gates. [ADR
|
||||||
|
0009](../adr/0009-sibling-inheritance.md) records why inheritance is a built-in
|
||||||
|
post-assembly structural operation.
|
||||||
|
|||||||
@@ -169,6 +169,15 @@ rules and opt-in version ownership, remain part of the configuration contract. C
|
|||||||
semantics do not require identical acquisition or lifetime behavior from every
|
semantics do not require identical acquisition or lifetime behavior from every
|
||||||
source kind.
|
source kind.
|
||||||
|
|
||||||
|
After source precedence is complete, built-in sibling `extends` relationships
|
||||||
|
resolve structurally in the same parser-independent representation, using the
|
||||||
|
existing recursive overlay rules. This adds neither an acquired source nor a
|
||||||
|
consumer registration mechanism. Enclosing relationships precede nested
|
||||||
|
destination-local resolution; typed defaults, application resolution, and
|
||||||
|
secret lookup retain their existing lifecycle roles. The [configuration
|
||||||
|
contract](../config.md#sibling-inheritance) owns the exact syntax and presence
|
||||||
|
rules, and [ADR 0009](../adr/0009-sibling-inheritance.md) records the rationale.
|
||||||
|
|
||||||
ConfigKit must define supported YAML semantics explicitly rather than inherit
|
ConfigKit must define supported YAML semantics explicitly rather than inherit
|
||||||
parser defaults. Processing order and diagnostics must be deterministic for the
|
parser defaults. Processing order and diagnostics must be deterministic for the
|
||||||
same inputs and options. Invalid input must not produce a successful usable
|
same inputs and options. Invalid input must not produce a successful usable
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ The following remain deliberately excluded:
|
|||||||
|
|
||||||
- automatic `conf.d` discovery;
|
- automatic `conf.d` discovery;
|
||||||
- remote imports, arbitrary providers, and additional configuration formats;
|
- remote imports, arbitrary providers, and additional configuration formats;
|
||||||
- custom merge strategies, sequence merging, and reset operators;
|
- custom merge strategies, sequence merging, reset operators, multiple-parent
|
||||||
|
inheritance, and cross-scope references;
|
||||||
- named overlay inheritance;
|
- named overlay inheritance;
|
||||||
- parser-specific or custom structural-decoding extension APIs;
|
- parser-specific or custom structural-decoding extension APIs;
|
||||||
- generic dynamic-subtree conversion and module-registry APIs;
|
- generic dynamic-subtree conversion and module-registry APIs;
|
||||||
@@ -28,7 +29,6 @@ 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.
|
||||||
|
|
||||||
Built-in sibling inheritance using `extends` is proposed in the
|
Built-in sibling inheritance using `extends` is implemented as described in the
|
||||||
[named-inheritance roadmap](named-inheritance.md), based on the
|
[configuration contract](../config.md#sibling-inheritance). It is distinct from
|
||||||
[downstream request](feature-request-promptkit-inheritance.md). It is not yet
|
inheritance between selected overlay files, which remains excluded above.
|
||||||
implemented and is distinct from inheritance between selected overlay files.
|
|
||||||
|
|||||||
@@ -314,6 +314,15 @@ not a full duplication of every mechanism test. Run `./scripts/check.sh` and
|
|||||||
|
|
||||||
## Stage 8 — Validate scaling and complete documentation
|
## Stage 8 — Validate scaling and complete documentation
|
||||||
|
|
||||||
|
**Completed 2026-09-29.** Benchmarks cover chains, shared-parent width, nested
|
||||||
|
destinations, replacements, and full inspected loads. Skipping repeated scope
|
||||||
|
assembly when no immediate relationship exists reduced measured work while a
|
||||||
|
single boundary clone preserves caller-input ownership. The maintained preset
|
||||||
|
example, configuration contract, consumer guidance, internal navigation,
|
||||||
|
architecture/deferred scope, and ADR 0009 now describe the implemented feature.
|
||||||
|
`./scripts/check.sh`, `go test -race ./...`, and the focused benchmarks passed.
|
||||||
|
No release was tagged or published.
|
||||||
|
|
||||||
Add focused benchmarks beside assembly/underlay benchmarks: long chains, wide
|
Add focused benchmarks beside assembly/underlay benchmarks: long chains, wide
|
||||||
shared-parent scopes, nested destination expansion, and replacements. Evaluate
|
shared-parent scopes, nested destination expansion, and replacements. Evaluate
|
||||||
allocations/work against input, materialized output, and retained history,
|
allocations/work against input, materialized output, and retained history,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Built-In Sibling Configuration Inheritance
|
# Built-In Sibling Configuration Inheritance
|
||||||
|
|
||||||
Status: agreed feature design; not implemented. This document defines the intended
|
Status: implemented. The [configuration contract](../config.md#sibling-inheritance)
|
||||||
end state. The [implementation plan](implementation.md) defines execution steps.
|
owns current behavior. This document preserves the agreed design and its
|
||||||
|
baseline; the [implementation plan](implementation.md) records completed work.
|
||||||
|
|
||||||
## Purpose and priorities
|
## Purpose and priorities
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ reduced consumer configuration infrastructure. Computational efficiency matters
|
|||||||
within those goals: resolve shared ancestors once and account for the size of
|
within those goals: resolve shared ancestors once and account for the size of
|
||||||
materialized results and retained history. Avoid an unconditional O(n) promise.
|
materialized results and retained history. Avoid an unconditional O(n) promise.
|
||||||
|
|
||||||
## Current baseline
|
## Baseline at design time
|
||||||
|
|
||||||
Baseline: `b604aa167b684525f0d5d806d9c4267824151ee4`, after the filesystem-source
|
Baseline: `b604aa167b684525f0d5d806d9c4267824151ee4`, after the filesystem-source
|
||||||
and underlay implementation and its remediation.
|
and underlay implementation and its remediation.
|
||||||
|
|||||||
@@ -52,6 +52,22 @@ func Example_seriatim() {
|
|||||||
// Output: visitor true the event occurred <nil> <nil>
|
// Output: visitor true the event occurred <nil> <nil>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Example_inheritedPresets() {
|
||||||
|
result, err := consumers.LoadPresets(configkit.File(fixture("inheritance", "presets.yml")))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := configkit.Keys("presets", "custom", "enabled")
|
||||||
|
info, _ := result.Metadata().Lookup(path)
|
||||||
|
custom := result.Config.Presets["custom"]
|
||||||
|
fmt.Println(custom.Enabled, result.Metadata().IsExplicit(path), custom.Settings["secondary"]["width"])
|
||||||
|
fmt.Println(info.Origin.Locations[0].SourceID, info.Origin.Inheritance[0].From)
|
||||||
|
// Output:
|
||||||
|
// false true 20
|
||||||
|
// source-1 /presets/base/enabled
|
||||||
|
}
|
||||||
|
|
||||||
func Example_weatherStartup() {
|
func Example_weatherStartup() {
|
||||||
const blockedName = "CONFIGKIT_WEATHER_EXAMPLE_BLOCKED"
|
const blockedName = "CONFIGKIT_WEATHER_EXAMPLE_BLOCKED"
|
||||||
const tokenName = "CONFIGKIT_WEATHER_EXAMPLE_TOKEN"
|
const tokenName = "CONFIGKIT_WEATHER_EXAMPLE_TOKEN"
|
||||||
|
|||||||
20
examples/consumers/inheritance.go
Normal file
20
examples/consumers/inheritance.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package consumers
|
||||||
|
|
||||||
|
import configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
|
||||||
|
// PresetCatalog is ordinary application data; no inheritance fields or
|
||||||
|
// registration hooks are required in the typed configuration.
|
||||||
|
type PresetCatalog struct {
|
||||||
|
Presets map[string]Preset `yaml:"presets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Preset struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Settings map[string]map[string]int `yaml:"settings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPresets performs one load; callers may inspect inherited presence and
|
||||||
|
// source origins on the returned result before choosing a preset to execute.
|
||||||
|
func LoadPresets(source configkit.Source) (*configkit.Result[PresetCatalog], error) {
|
||||||
|
return configkit.Load[PresetCatalog](source)
|
||||||
|
}
|
||||||
35
examples/consumers/inheritance_test.go
Normal file
35
examples/consumers/inheritance_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package consumers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPresetCatalogOneLoadAndDerivation(t *testing.T) {
|
||||||
|
result, err := consumers.LoadPresets(configkit.File(fixture("inheritance", "presets.yml")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.Config.Presets["base"].Settings["secondary"]["width"] != 10 ||
|
||||||
|
result.Config.Presets["custom"].Settings["secondary"]["width"] != 20 ||
|
||||||
|
result.Config.Presets["custom"].Enabled {
|
||||||
|
t.Fatalf("expanded presets = %#v", result.Config.Presets)
|
||||||
|
}
|
||||||
|
enabled := configkit.Keys("presets", "custom", "enabled")
|
||||||
|
if !result.Metadata().IsExplicit(enabled) {
|
||||||
|
t.Fatal("inherited false lost explicit presence")
|
||||||
|
}
|
||||||
|
info, found := result.Metadata().Lookup(enabled)
|
||||||
|
if !found || info.Origin.Locations[0].SourceID != "source-1" ||
|
||||||
|
len(info.Origin.Inheritance) != 1 || info.Origin.Inheritance[0].From.String() != "/presets/base/enabled" {
|
||||||
|
t.Fatalf("origin and derivation = %#v, %v", info, found)
|
||||||
|
}
|
||||||
|
nested := configkit.Keys("presets", "custom", "settings", "secondary", "width")
|
||||||
|
info, found = result.Metadata().Lookup(nested)
|
||||||
|
if !found || len(info.Origin.Inheritance) != 1 ||
|
||||||
|
info.Origin.Inheritance[0].From.String() != "/presets/custom/settings/primary/width" {
|
||||||
|
t.Fatalf("destination-local derivation = %#v, %v", info, found)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
examples/consumers/testdata/inheritance/presets.yml
vendored
Normal file
10
examples/consumers/testdata/inheritance/presets.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
presets:
|
||||||
|
base:
|
||||||
|
enabled: false
|
||||||
|
settings:
|
||||||
|
primary: {width: 10}
|
||||||
|
secondary: {extends: primary}
|
||||||
|
custom:
|
||||||
|
extends: base
|
||||||
|
settings:
|
||||||
|
primary: {width: 20}
|
||||||
@@ -39,7 +39,8 @@ type Explanation struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Explain returns safe effective and chronological values for a known final or
|
// Explain returns safe effective and chronological values for a known final or
|
||||||
// historical path. Unknown and malformed paths return false.
|
// historical path. Structural inheritance appears between source applications
|
||||||
|
// and typed reconciliation. Unknown and malformed paths return false.
|
||||||
func (r *Result[T]) Explain(path Path) (Explanation, bool) {
|
func (r *Result[T]) Explain(path Path) (Explanation, bool) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return Explanation{}, false
|
return Explanation{}, false
|
||||||
|
|||||||
88
internal/engine/inheritance_bench_test.go
Normal file
88
internal/engine/inheritance_bench_test.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkInheritanceExpansion(b *testing.B) {
|
||||||
|
for _, shape := range []string{"chain", "wide", "nested", "replacement"} {
|
||||||
|
for _, size := range []int{16, 32, 64} {
|
||||||
|
b.Run(shape+"/"+strconv.Itoa(size), func(b *testing.B) {
|
||||||
|
input := inheritanceBenchmarkInput(shape, size)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for range b.N {
|
||||||
|
if _, diagnostics := expandInheritanceWithOrigins(input, nil, defaultProjectionLimits()); len(diagnostics) != 0 {
|
||||||
|
b.Fatal(diagnostics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkInheritanceInspectedLoad(b *testing.B) {
|
||||||
|
type entry struct {
|
||||||
|
Value string `yaml:"value"`
|
||||||
|
}
|
||||||
|
type configuration struct {
|
||||||
|
Entries map[string]entry `yaml:"entries"`
|
||||||
|
}
|
||||||
|
for _, size := range []int{16, 32, 64} {
|
||||||
|
b.Run(strconv.Itoa(size), func(b *testing.B) {
|
||||||
|
var input strings.Builder
|
||||||
|
input.WriteString("entries:\n item-0: {value: base}\n")
|
||||||
|
for index := 1; index <= size; index++ {
|
||||||
|
input.WriteString(" item-")
|
||||||
|
input.WriteString(strconv.Itoa(index))
|
||||||
|
input.WriteString(": {extends: item-0}\n")
|
||||||
|
}
|
||||||
|
source := Bytes("inheritance-benchmark", []byte(input.String()))
|
||||||
|
query := Keys("entries", "item-"+strconv.Itoa(size), "value")
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for range b.N {
|
||||||
|
loaded, err := Load[configuration](source, testSecretBinding())
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, found := loaded.Inspection.Explain(query); !found {
|
||||||
|
b.Fatal("inherited value is not explainable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func inheritanceBenchmarkInput(shape string, size int) *document.Node {
|
||||||
|
value := func(text string) *document.Node { return &document.Node{Kind: document.String, Text: text} }
|
||||||
|
mapping := func(entries ...document.Entry) *document.Node {
|
||||||
|
return &document.Node{Kind: document.Mapping, Mapping: entries}
|
||||||
|
}
|
||||||
|
entry := func(key string, node *document.Node) document.Entry { return document.Entry{Key: key, Value: node} }
|
||||||
|
base := mapping(entry("value", value("base")), entry("settings", mapping(entry("enabled", &document.Node{Kind: document.Boolean, Text: "false"}))))
|
||||||
|
if shape == "nested" {
|
||||||
|
base = mapping(entry("settings", mapping(entry("first", mapping(entry("value", value("base")))), entry("second", mapping(entry("extends", value("first")))))))
|
||||||
|
}
|
||||||
|
definitions := make([]document.Entry, 0, size+1)
|
||||||
|
definitions = append(definitions, entry("item-0", base))
|
||||||
|
for index := 1; index <= size; index++ {
|
||||||
|
parent := "item-0"
|
||||||
|
if shape == "chain" {
|
||||||
|
parent = "item-" + strconv.Itoa(index-1)
|
||||||
|
}
|
||||||
|
payload := []document.Entry{entry("extends", value(parent))}
|
||||||
|
if shape == "nested" {
|
||||||
|
payload = append(payload, entry("settings", mapping(entry("first", mapping(entry("value", value("custom")))))))
|
||||||
|
}
|
||||||
|
if shape == "replacement" {
|
||||||
|
payload = append(payload, entry("value", value("replacement")))
|
||||||
|
}
|
||||||
|
definitions = append(definitions, entry("item-"+strconv.Itoa(index), mapping(payload...)))
|
||||||
|
}
|
||||||
|
return mapping(entry("entries", mapping(definitions...)))
|
||||||
|
}
|
||||||
@@ -19,8 +19,7 @@ type inheritanceWalker struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// expandInheritanceTree resolves each destination scope after its enclosing
|
// expandInheritanceTree resolves each destination scope after its enclosing
|
||||||
// relationships. It remains a private, test-only operation until the loading
|
// relationships. The loading lifecycle uses the origin-aware entry point.
|
||||||
// lifecycle also carries its complete provenance and inspection evidence.
|
|
||||||
func expandInheritanceTree(input *document.Node, limits projectionLimits) (*inheritanceScope, []Diagnostic) {
|
func expandInheritanceTree(input *document.Node, limits projectionLimits) (*inheritanceScope, []Diagnostic) {
|
||||||
return expandInheritanceWithOrigins(input, nil, limits)
|
return expandInheritanceWithOrigins(input, nil, limits)
|
||||||
}
|
}
|
||||||
@@ -34,9 +33,13 @@ func expandInheritanceWithBudgets(input *document.Node, inputOrigins map[string]
|
|||||||
return nil, []Diagnostic{{Code: CodeInheritanceInvalid, Severity: SeverityError,
|
return nil, []Diagnostic{{Code: CodeInheritanceInvalid, Severity: SeverityError,
|
||||||
Message: "inheritance requires a mapping document root"}}
|
Message: "inheritance requires a mapping document root"}}
|
||||||
}
|
}
|
||||||
root := input
|
root := document.Clone(input)
|
||||||
|
origins := cloneOriginIndex(inputOrigins)
|
||||||
|
if inputOrigins == nil {
|
||||||
|
origins = directInheritanceOrigins(input, Path{}, nil)
|
||||||
|
}
|
||||||
walker := inheritanceWalker{
|
walker := inheritanceWalker{
|
||||||
root: &root, limits: limits, origins: cloneOriginIndex(inputOrigins),
|
root: &root, limits: limits, origins: origins,
|
||||||
retained: newRetainedNodeCounter(historyLimit),
|
retained: newRetainedNodeCounter(historyLimit),
|
||||||
}
|
}
|
||||||
if problem := walker.visit(&root, Path{}, true); problem != nil {
|
if problem := walker.visit(&root, Path{}, true); problem != nil {
|
||||||
@@ -75,22 +78,24 @@ func (w *inheritanceWalker) visit(node **document.Node, path Path, unscoped bool
|
|||||||
*node = withoutInheritanceDirective(*node)
|
*node = withoutInheritanceDirective(*node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolved, diagnostics := expandSiblingDefinitionsWithOrigins(*node, path, w.limits, w.origins, w.retained)
|
if scopeHasInheritanceDefinitions(*node) {
|
||||||
if len(diagnostics) != 0 {
|
resolved, diagnostics := expandSiblingDefinitionsWithOrigins(*node, path, w.limits, w.origins, w.retained)
|
||||||
return &diagnostics[0]
|
if len(diagnostics) != 0 {
|
||||||
}
|
return &diagnostics[0]
|
||||||
*node = resolved.root
|
}
|
||||||
deleteOriginSubtree(w.origins, path, original)
|
*node = resolved.root
|
||||||
for pointer, origin := range resolved.origins {
|
deleteOriginSubtree(w.origins, path, original)
|
||||||
w.origins[pointer] = origin
|
for pointer, origin := range resolved.origins {
|
||||||
}
|
w.origins[pointer] = origin
|
||||||
w.transfers = append(w.transfers, resolved.transfers...)
|
}
|
||||||
w.controls = append(w.controls, resolved.controls...)
|
w.transfers = append(w.transfers, resolved.transfers...)
|
||||||
w.replacements = append(w.replacements, resolved.replacements...)
|
w.controls = append(w.controls, resolved.controls...)
|
||||||
w.operations = append(w.operations, resolved.operations...)
|
w.replacements = append(w.replacements, resolved.replacements...)
|
||||||
if !inheritanceLowerBoundWithin(*w.root, w.limits.nodes) {
|
w.operations = append(w.operations, resolved.operations...)
|
||||||
problem := inheritanceBudgetDiagnostic(path, (*node).Location)
|
if !inheritanceLowerBoundWithin(*w.root, w.limits.nodes) {
|
||||||
return &problem
|
problem := inheritanceBudgetDiagnostic(path, (*node).Location)
|
||||||
|
return &problem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
indices := make([]int, len((*node).Mapping))
|
indices := make([]int, len((*node).Mapping))
|
||||||
for index := range indices {
|
for index := range indices {
|
||||||
@@ -116,6 +121,18 @@ func (w *inheritanceWalker) visit(node **document.Node, path Path, unscoped bool
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scopeHasInheritanceDefinitions(scope *document.Node) bool {
|
||||||
|
for _, entry := range scope.Mapping {
|
||||||
|
if entry.Value.Kind != document.Mapping {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, found := inheritanceDirective(entry.Value); found {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// A directive may disappear during later destination resolution, so only
|
// A directive may disappear during later destination resolution, so only
|
||||||
// non-directive nodes form a safe early lower bound on final output size.
|
// non-directive nodes form a safe early lower bound on final output size.
|
||||||
func inheritanceLowerBoundWithin(root *document.Node, limit int) bool {
|
func inheritanceLowerBoundWithin(root *document.Node, limit int) bool {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (m Metadata) IsExplicit(path Path) bool {
|
|||||||
return ok && info.Explicit
|
return ok && info.Explicit
|
||||||
}
|
}
|
||||||
|
|
||||||
// History returns detached definition and resolution records for path. A record's
|
// History returns detached definition, inheritance, and resolution records for path. A record's
|
||||||
// Path can be an ancestor when that container operation replaced or removed path.
|
// Path can be an ancestor when that container operation replaced or removed path.
|
||||||
// Removed historical paths can have history even though Lookup returns false.
|
// Removed historical paths can have history even though Lookup returns false.
|
||||||
// Unknown and invalid paths return nil.
|
// Unknown and invalid paths return nil.
|
||||||
@@ -51,7 +51,7 @@ func (m Metadata) History(path Path) []Change {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HistoricalPaths returns the paths observed during loading, including effective
|
// HistoricalPaths returns the paths observed during loading, including effective
|
||||||
// paths and paths removed by overlays or recorded resolution. It includes default
|
// paths and paths removed by overlays, inheritance, or recorded resolution. It includes default
|
||||||
// paths and the root, but not paths that never existed. The zero Metadata returns
|
// paths and the root, but not paths that never existed. The zero Metadata returns
|
||||||
// an empty slice.
|
// an empty slice.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ type LayeredOptions[T any] struct {
|
|||||||
// imports with each input's tier. It strictly composes contributions within
|
// imports with each input's tier. It strictly composes contributions within
|
||||||
// each underlay, then recursively combines underlays in listed order beneath
|
// each underlay, then recursively combines underlays in listed order beneath
|
||||||
// main, a selected overlay, and CLI overrides. Defaults, decoding, resolution,
|
// main, a selected overlay, and CLI overrides. Defaults, decoding, resolution,
|
||||||
// and validation run once on the assembled input. Source IDs follow acquisition
|
// and validation run once after sibling `extends` expansion of the assembled
|
||||||
// order, not precedence order. Zero or one options value is accepted; any error
|
// input. Source IDs follow acquisition order, not precedence order. Zero or
|
||||||
|
// one options value is accepted; any error
|
||||||
// returns a nil result.
|
// returns a nil result.
|
||||||
func LoadLayered[T any](main Input, supplied ...LayeredOptions[T]) (*Result[T], error) {
|
func LoadLayered[T any](main Input, supplied ...LayeredOptions[T]) (*Result[T], error) {
|
||||||
options := make([]engine.LayeredOptions[T], len(supplied))
|
options := make([]engine.LayeredOptions[T], len(supplied))
|
||||||
|
|||||||
@@ -79,8 +79,9 @@ type Result[T any] struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load parses and strictly decodes zero or one selected source, explicitly enabled
|
// Load parses and strictly decodes zero or one selected source, explicitly enabled
|
||||||
// imports, and one selected overlay. It accepts at most one Options value and
|
// imports, and one selected overlay. After source precedence, built-in sibling
|
||||||
// returns no result when any error is reported.
|
// `extends` relationships resolve before defaults and typed decoding. It accepts
|
||||||
|
// at most one Options value and returns no result when any error is reported.
|
||||||
func Load[T any](source Source, supplied ...Options[T]) (*Result[T], error) {
|
func Load[T any](source Source, supplied ...Options[T]) (*Result[T], error) {
|
||||||
options := make([]engine.Options[T], len(supplied))
|
options := make([]engine.Options[T], len(supplied))
|
||||||
for index := range supplied {
|
for index := range supplied {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func (m Metadata) IsExplicit(path Path) bool {
|
|||||||
return m.value.IsExplicit(toEnginePath(path))
|
return m.value.IsExplicit(toEnginePath(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
// History returns detached definition and resolution records for path. A record's
|
// History returns detached definition, inheritance, and resolution records for path. A record's
|
||||||
// Path can be an ancestor when that container operation replaced or removed path.
|
// Path can be an ancestor when that container operation replaced or removed path.
|
||||||
// Removed historical paths can have history even though Lookup returns false.
|
// Removed historical paths can have history even though Lookup returns false.
|
||||||
// Unknown and invalid paths return nil.
|
// Unknown and invalid paths return nil.
|
||||||
@@ -36,7 +36,7 @@ func (m Metadata) History(path Path) []Change {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HistoricalPaths returns the paths observed during loading, including effective
|
// HistoricalPaths returns the paths observed during loading, including effective
|
||||||
// paths and paths removed by overlays or recorded resolution. It includes default
|
// paths and paths removed by overlays, inheritance, or recorded resolution. It includes default
|
||||||
// paths and the root, but not paths that never existed. The zero Metadata returns
|
// paths and the root, but not paths that never existed. The zero Metadata returns
|
||||||
// an empty slice.
|
// an empty slice.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -205,8 +205,9 @@ type Diagnostic struct {
|
|||||||
Related []Location
|
Related []Location
|
||||||
}
|
}
|
||||||
|
|
||||||
// InheritanceStep identifies a concrete payload transfer controlled by a
|
// InheritanceStep identifies a concrete payload transfer. From and To name
|
||||||
// relationship directive. Locations belong to that directive, not its value.
|
// semantic value paths; Reference names the controlling destination `extends`
|
||||||
|
// path. Locations belong to that directive, not the transferred value.
|
||||||
type InheritanceStep struct {
|
type InheritanceStep struct {
|
||||||
From Path
|
From Path
|
||||||
To Path
|
To Path
|
||||||
@@ -216,7 +217,8 @@ type InheritanceStep struct {
|
|||||||
|
|
||||||
// Origin describes where an effective definition came from. Multiple locations
|
// Origin describes where an effective definition came from. Multiple locations
|
||||||
// represent a composite origin; resolution origins are synthetic and identify their
|
// represent a composite origin; resolution origins are synthetic and identify their
|
||||||
// operation instead of a file location.
|
// operation instead of a file location. Inheritance records the concrete
|
||||||
|
// derivation path while Layer and Locations continue to identify value sources.
|
||||||
type Origin struct {
|
type Origin struct {
|
||||||
Layer Layer
|
Layer Layer
|
||||||
Locations []Location
|
Locations []Location
|
||||||
|
|||||||
Reference in New Issue
Block a user