Compare commits
156 Commits
d898cb193d
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| c88aac990b | |||
| 48c50e49d7 | |||
| 7ed4629c8d | |||
| c63f4ff687 | |||
| 68ec552b52 | |||
| e5475ea7f8 | |||
| cf9e2fd677 | |||
| eb87a10e1c | |||
| 54bdc24865 | |||
| ad5b859337 | |||
| 1fd0adc571 | |||
| 5b03ca6278 | |||
| b9f41d698e | |||
| fb8d56cfef | |||
| b27fe0598a | |||
| 74bff9704e | |||
| 1e69abc88c | |||
| 6c60f02b5a | |||
| d211537873 | |||
| 6db441893d | |||
| e74c26a4b3 | |||
| 08bb3a20c6 | |||
| b3bb57eda0 | |||
| 94d1e333d4 | |||
| ff10d03978 | |||
| b4477a236a | |||
| d818b08556 | |||
| 587f407a32 | |||
| 335bd7bc31 | |||
| 331195202e | |||
| 21c9bc0ad0 | |||
| 68400ffdfb | |||
| 5d5d51ab11 | |||
| 69bd9ead53 | |||
| 2ff588eb64 | |||
| c96197c417 | |||
| 24ab623cb2 | |||
| 1ce147c9f6 | |||
| 9833ca3aa3 | |||
| eafbd32489 | |||
| 9ac0053590 | |||
| 791cf16136 | |||
| 31ae30372e | |||
| 7ee9e92ef5 | |||
| 556b10b2cd | |||
| 07e4ae5d92 | |||
| 7548fa101a | |||
| 197567177e | |||
| 7330107389 | |||
| ad6d3748d9 | |||
| 1ee508ba02 | |||
| e9155fced6 | |||
| 14c83a6515 | |||
| 3ebb848ead | |||
| 4289f9ff7f | |||
| a050986916 | |||
| fb71421956 | |||
| b281adb751 | |||
| 1448f9b19d | |||
| 731149fc6d | |||
| 9ff873f54c | |||
| 3f389662e9 | |||
| 906ca34b6d | |||
| 17216a40f3 | |||
| 896c1cde08 | |||
| 91769e9f9b | |||
| 98c8b7c842 | |||
| 1954052631 | |||
| 3ffaef7ede | |||
| 6a1447c327 | |||
| 12fd0a7191 | |||
| 8155465322 | |||
| 408dd13c24 | |||
| 738c8a9cc7 | |||
| 4b4e15fc58 | |||
| 76521f7cd8 | |||
| cacc558288 | |||
| 231ae66857 | |||
| da74d4190d | |||
| 11f7154874 | |||
| 3828b117e8 | |||
| 2419e63717 | |||
| a893c7716c | |||
| 28ef9b6200 | |||
| 0dd16aa8a9 | |||
| f6d54a094a | |||
| f9ec66e867 | |||
| be5cdeb502 | |||
| 1f64673d0d | |||
| 01b08e76a3 | |||
| 453da684d7 | |||
| fb6c254d38 | |||
| a71413ce67 | |||
| d66338a1c5 | |||
| 643f43ce6a | |||
| c25cf39d19 | |||
| 7d18c93339 | |||
| b1d89810ca | |||
| bb5540b62e | |||
| 80cf23e710 | |||
| ac48557535 | |||
| b4af900c10 | |||
| f132cd6468 | |||
| 75e209eb3b | |||
| 1dd94a3cc6 | |||
| d172a5e46a | |||
| 3a1b6ff355 | |||
| 68383f34de | |||
| 549fca239d | |||
| 883244936c | |||
| a8ae2e9d5e | |||
| f18582106b | |||
| d8bf6abc2b | |||
| 4bf940c579 | |||
| 4fd3bee986 | |||
| 6f6130fd12 | |||
| 3122e2f32d | |||
| e4a5ecb4be | |||
| 826062aa17 | |||
| 08f2a8f3e9 | |||
| 12d350a292 | |||
| 4fcff5f453 | |||
| 247a12bc50 | |||
| 119b42c810 | |||
| 0f9ecf12ed | |||
| 7bf2f0c687 | |||
| 8aba43da38 | |||
| 877c497325 | |||
| 08a8c25542 | |||
| ea27f17f5c | |||
| ae3408a096 | |||
| deb9aa5c69 | |||
| 2736ddfbf2 | |||
| 7889ebddfb | |||
| 4a5316b7fc | |||
| e1e94eeec7 | |||
| 176e9a5d90 | |||
| 23c1976ef1 | |||
| aab79d9508 | |||
| ecd5d5d460 | |||
| b1c278b38b | |||
| eb5a82f1f3 | |||
| b30c9f167d | |||
| 5925066d35 | |||
| 0cca633a67 | |||
| a80273433e | |||
| 320d0c5f43 | |||
| 5098b56655 | |||
| 47944d22d1 | |||
| 50199eb0af | |||
| 34cbc0d63b | |||
| 5e12b2d360 | |||
| 8dfaf44d1d | |||
| 64be8b4df5 | |||
| e585decdb2 | |||
| 6440a500f7 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,6 +28,9 @@ go.work.sum
|
||||
# Local codebase-memory tooling artifacts
|
||||
/.codebase-memory/
|
||||
|
||||
# Local release credentials and tooling
|
||||
/.releasekit/
|
||||
|
||||
# ---> Linux
|
||||
*~
|
||||
|
||||
|
||||
@@ -18,8 +18,3 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
Third-party code and assets are subject to their respective license terms.
|
||||
|
||||
Policy documents adapted from Standard Repository Policies, the root AGENTS.md
|
||||
orientation, and the Policy Reading Guide in docs/development.md remain licensed
|
||||
under CC-BY-SA-4.0. See docs/oss/attribution.md for the affected material,
|
||||
upstream credit, and license links.
|
||||
|
||||
137
README.md
137
README.md
@@ -1,56 +1,18 @@
|
||||
# ConfigKit
|
||||
|
||||
An opinionated Go library for applications that use YAML configuration files.
|
||||
ConfigKit loads YAML into your application's Go configuration type, combines the
|
||||
inputs you choose, and lets you inspect where the final values came from. You
|
||||
supply the types, defaults, and application rules; ConfigKit handles loading,
|
||||
combination, strict decoding, and inspection.
|
||||
|
||||
Linux is release-validated; macOS support is best-effort. See the
|
||||
[platform support policy](docs/policy/release.md#platform-support) for details.
|
||||
[platform support policy](docs/policy/release.md#platform-support).
|
||||
|
||||
ConfigKit's simple `Load` accepts one strict YAML source (or explicit
|
||||
defaults-only input). `LoadLayered` additionally combines explicit lower-priority
|
||||
source groups with an independently selected main input. Sources can be host
|
||||
files, native `fs.FS` files, labeled bytes or readers, or snapshotted mappings.
|
||||
Enabled explicit imports compose strictly within their group; precedence
|
||||
between groups combines mappings recursively. Both entry points return a complete,
|
||||
validated configuration with detached metadata and safe diagnostics.
|
||||
Applications may perform audited defaulting and normalization through a resolver,
|
||||
then report domain errors and warnings through read-only semantic validation. Secret
|
||||
tags and a path predicate drive deterministic redacted effective YAML. Typed secret
|
||||
references resolve environment names before validation, and explicit
|
||||
secrets-directory bootstrap is available as a separate startup operation. Explicit
|
||||
CLI override records apply after inheritance and typed decoding, before application
|
||||
resolution. Value explanation is available through
|
||||
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. The root
|
||||
`Diff` API compares frozen effective values from two successful same-type loads;
|
||||
the [configuration contract](docs/config.md#effective-configuration-diffs) and
|
||||
[consumer guide](docs/consumers/README.md#effective-configuration-diffs) describe
|
||||
its safe changed-region records and maintained workflows.
|
||||
|
||||
Sources can also be explicitly placed as sequence items. Configuration
|
||||
declarations and application registrations can give selected sequences scalar
|
||||
identities and choose how they combine across precedence and inheritance.
|
||||
Strict peers combine distinct records, while higher-priority collections can
|
||||
replace, append, or upsert whole records. See the
|
||||
[sequence contract](docs/config.md#strict-document-composition),
|
||||
[maintained catalog examples](docs/consumers/README.md#identified-prompt-sequences),
|
||||
and [ADR 0017](docs/adr/0017-collection-sequence-policy.md).
|
||||
|
||||
Imports are also explicit: a source may name files, search declared directories,
|
||||
or select files with path globs. Applications can set load-wide import defaults
|
||||
while keeping declaration-specific exceptions. Files are acquired in authored
|
||||
declaration order; dependency-ready composition uses a shared stable scheduler.
|
||||
ConfigKit does not search conventional application paths implicitly. See the
|
||||
[import contract](docs/config.md#explicit-imports), [consumer examples](docs/consumers/README.md),
|
||||
and [ADR 0015](docs/adr/0015-shared-dependency-ready-ordering.md) and
|
||||
[ADR 0016](docs/adr/0016-independent-import-controls.md).
|
||||
|
||||
Main-file discovery is explicit too: applications choose and order `Search`
|
||||
descriptions and pass `SearchConfig` to the existing load APIs. See the
|
||||
[discovery contract](docs/config.md#configuration-discovery), [consumer guide](docs/consumers/README.md#explicit-main-file-discovery),
|
||||
and [ADR 0019](docs/adr/0019-explicit-configuration-discovery.md).
|
||||
## Load a configuration file
|
||||
|
||||
```go
|
||||
import "go.maximumdirect.net/configkit"
|
||||
|
||||
type Config struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Workers int `yaml:"workers"`
|
||||
@@ -65,48 +27,57 @@ func ReadConfig(path string) (Config, error) {
|
||||
}
|
||||
```
|
||||
|
||||
Existing consumers should follow the
|
||||
[module-path migration guidance](docs/consumers/README.md#vanity-module-path-migration).
|
||||
|
||||
## Add the features you need
|
||||
|
||||
Start with one file. Optional features use the same loading and inspection model:
|
||||
|
||||
- [Choose inputs](docs/consumers/README.md#choosing-a-loading-entry-point): read
|
||||
files, an application-owned `fs.FS`, bytes, a reader, or a Go mapping. Use
|
||||
explicit search descriptions when the application should look in several places.
|
||||
- [Supply lower-priority settings](docs/consumers/README.md#native-filesystem-and-layered-input):
|
||||
use `LoadLayered` for bundled defaults that application configuration can override.
|
||||
- [Combine configuration](docs/config.md#explicit-imports): add declared imports,
|
||||
select one overlay, or pass values explicitly set by your CLI parser.
|
||||
- [Reuse settings](docs/config.md#sibling-inheritance): let one named entry inherit
|
||||
from a sibling. [Identified lists](docs/config.md#strict-document-composition)
|
||||
also support record inheritance and replace, append, or upsert operations.
|
||||
- [Apply application rules](docs/config.md#recorded-application-resolution): derive
|
||||
or normalize values in a resolver, then check the final configuration in a validator.
|
||||
- [Handle credentials](docs/config.md#typed-secret-references): use environment-name
|
||||
references and choose which configuration fields to hide in inspection output.
|
||||
- [Inspect a load](docs/config.md#layered-metadata): examine sources, presence,
|
||||
history, safe YAML output, or [changes between two loads](docs/config.md#effective-configuration-diffs).
|
||||
|
||||
The [consumer guide](docs/consumers/README.md) walks through these tasks and links
|
||||
to maintained examples. The [configuration reference](docs/config.md) defines the
|
||||
exact YAML, combination, and lifecycle rules.
|
||||
|
||||
## Design philosophy
|
||||
|
||||
ConfigKit favors one coherent, predictable configuration model over flexibility
|
||||
for reproducing each consumer's conventions. Its goal is to let applications
|
||||
load once, inspect where final values came from, and distinguish explicit values
|
||||
from omissions with minimal configuration bookkeeping. Applications retain their
|
||||
domain rules and execution responsibilities.
|
||||
ConfigKit favors consistent, predictable rules across applications. A load returns
|
||||
the final configuration and metadata that distinguish supplied values such as
|
||||
`false` and `0` from omissions. Applications keep their domain rules and runtime
|
||||
initialization.
|
||||
|
||||
Loading is synchronous and complete on return. Applications normally load once
|
||||
near startup and then use the final configuration and metadata; ConfigKit stays
|
||||
inactive until explicitly called again. Applications may initiate additional
|
||||
loads as needed. See the [loading lifecycle](docs/config.md#completion-and-resource-lifetime)
|
||||
for completion and ownership details.
|
||||
Within one layer, independent contributions combine strictly: conflicting
|
||||
values are errors. Across layers, higher-priority settings can replace
|
||||
lower-priority settings, while mappings combine by key. Identified lists have
|
||||
their declared combination rules. See the [worked combination
|
||||
examples](docs/config.md#strict-document-composition) for the details and
|
||||
exceptions.
|
||||
|
||||
Within a configuration tier, independent contributions compose strictly: shared
|
||||
mappings combine, while overlapping non-mapping definitions are errors. Between
|
||||
precedence tiers, mappings combine recursively, retaining omitted lower-priority
|
||||
children; other values replace lower-priority values. The main document and its
|
||||
imports form one strictly composed tier. Explicit underlay groups sit below it
|
||||
and above typed defaults; a selected overlay applies before inheritance, while
|
||||
explicit CLI overrides apply after decoding and before application resolution.
|
||||
The [configuration contract](docs/config.md#strict-document-composition) owns
|
||||
the detailed combination rules and input-specific constraints. Declared or
|
||||
registered collections combine distinct identified records within strict tiers
|
||||
and use their selected operation at precedence boundaries; other sequences keep
|
||||
their strict-conflict and whole-value-replacement behavior.
|
||||
Loading finishes before the call returns. ConfigKit then stays inactive until you
|
||||
call it again; inspection uses the saved result. See
|
||||
[completion and resource lifetime](docs/config.md#completion-and-resource-lifetime).
|
||||
|
||||
The [architecture goals](docs/policy/architecture.md#success-criteria) explain
|
||||
these priorities and how genuine new needs can justify extending the shared
|
||||
model. The [future feature list](docs/roadmap/future.md) preserves candidates for
|
||||
consideration in future releases.
|
||||
|
||||
The [configuration contract](docs/config.md) describes implemented behavior,
|
||||
including [input destinations](docs/config.md#input-destinations). The
|
||||
[consumer adoption guide](docs/consumers/README.md) provides maintained simple,
|
||||
layered, mounted-collection, and identified-prompt examples alongside the
|
||||
migration shapes. Input placement decisions are recorded in
|
||||
[ADR 0011](docs/adr/0011-input-destinations.md) and sequence placement and
|
||||
composition in [ADR 0014](docs/adr/0014-sequence-placement-and-composition.md).
|
||||
See the [development guide](docs/development.md) for repository orientation and
|
||||
the [release procedure](docs/release.md) for maintainer-only candidate and
|
||||
publication steps.
|
||||
The [architecture goals](docs/policy/architecture.md#success-criteria) and
|
||||
[ADRs](docs/adr) explain design decisions, including shared import and inheritance
|
||||
ordering. [Future work](docs/roadmap/future.md) lists possible extensions.
|
||||
Contributors can start with the [development guide](docs/development.md);
|
||||
release maintainers use the [release procedure](docs/release.md).
|
||||
|
||||
ConfigKit code is licensed under the [LGPL v3.0 or later](COPYRIGHT).
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package configkit
|
||||
|
||||
import "gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
import "go.maximumdirect.net/configkit/internal/engine"
|
||||
|
||||
func fromEngineLocation(value engine.Location) Location {
|
||||
return Location{SourceID: value.SourceID, Line: value.Line, Column: value.Column}
|
||||
@@ -46,21 +46,21 @@ func fromEngineSourceInfo(value engine.SourceInfo) SourceInfo {
|
||||
return SourceInfo{
|
||||
ID: value.ID, Label: value.Label, Path: value.Path, Destination: fromEnginePath(value.Destination),
|
||||
PlacementMode: PlacementMode(engine.SourceInfoPlacementModeForFacade(value)),
|
||||
DestinationUnresolved: engine.SourceInfoDestinationUnresolvedForFacade(value),
|
||||
DestinationIsFallback: engine.SourceInfoDestinationUnresolvedForFacade(value),
|
||||
Layer: Layer(value.Layer), Kind: SourceKind(value.Kind), UnderlayIndex: value.UnderlayIndex,
|
||||
ImportedFrom: fromEngineLocationPointer(value.ImportedFrom), VersionCheck: fromEngineSourceVersionCheck(value.VersionCheck),
|
||||
ImportSelections: fromEngineImportSelections(engine.SourceInfoImportSelectionsForFacade(value)),
|
||||
ImportedFrom: fromEngineLocationPointer(value.ImportedFrom), VersionEvidence: fromEngineSourceVersionEvidence(value.VersionCheck),
|
||||
EmptyImportSelections: fromEngineEmptyImportSelections(engine.SourceInfoImportSelectionsForFacade(value)),
|
||||
}
|
||||
}
|
||||
func fromEngineImportSelections(values []engine.ImportSelectionForFacade) []ImportSelection {
|
||||
func fromEngineEmptyImportSelections(values []engine.ImportSelectionForFacade) []EmptyImportSelection {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]ImportSelection, len(values))
|
||||
result := make([]EmptyImportSelection, len(values))
|
||||
for index, value := range values {
|
||||
result[index] = ImportSelection{
|
||||
result[index] = EmptyImportSelection{
|
||||
Path: fromEnginePath(value.Path), Location: fromEngineLocation(value.Location),
|
||||
Selector: ImportSelector(value.Selector), Outcome: ImportSelectionOutcome(value.Outcome),
|
||||
Selector: ImportSelector(value.Selector),
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -81,36 +81,19 @@ func toEngineOverrides(values []Override) []engine.Override {
|
||||
|
||||
func toEngineImportDefaults(value ImportDefaults) engine.ImportDefaults {
|
||||
return engine.ImportDefaults{
|
||||
Required: cloneImportDefaultBool(value.Required),
|
||||
Follow: cloneImportDefaultBool(value.Follow),
|
||||
Hidden: cloneImportDefaultBool(value.Hidden),
|
||||
Recursive: cloneImportDefaultBool(value.Recursive),
|
||||
Pattern: cloneImportDefaultStrings(value.Pattern),
|
||||
Required: value.Required,
|
||||
Follow: value.FollowSymlinks,
|
||||
Hidden: value.IncludeHidden,
|
||||
Recursive: value.Recursive,
|
||||
Pattern: value.Pattern,
|
||||
}
|
||||
}
|
||||
|
||||
func cloneImportDefaultBool(value *bool) *bool {
|
||||
func fromEngineSourceVersionEvidence(value *engine.SourceVersionCheck) *SourceVersionEvidence {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := *value
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneImportDefaultStrings(values []string) []string {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := make([]string, len(values))
|
||||
copy(cloned, values)
|
||||
return cloned
|
||||
}
|
||||
|
||||
func fromEngineSourceVersionCheck(value *engine.SourceVersionCheck) *SourceVersionCheck {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &SourceVersionCheck{
|
||||
return &SourceVersionEvidence{
|
||||
Path: fromEnginePath(value.Path),
|
||||
Version: Version{Present: value.Version.Present, Null: value.Version.Null, Text: value.Version.Text},
|
||||
Location: fromEngineLocationPointer(value.Location), Redacted: value.Redacted,
|
||||
|
||||
@@ -3,7 +3,7 @@ package configkit
|
||||
import (
|
||||
"io"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
"go.maximumdirect.net/configkit/internal/engine"
|
||||
)
|
||||
|
||||
// LoadError reports safe structured diagnostics for a failed configuration load.
|
||||
@@ -50,6 +50,21 @@ func (e *LoadError) Sources() []SourceInfo {
|
||||
return result
|
||||
}
|
||||
|
||||
// Source returns a detached record from the sanitized source evidence retained
|
||||
// before loading failed, including placement evidence available at that point.
|
||||
// IDs are local to the failed load. It does not acquire sources or access the
|
||||
// filesystem. Empty or unknown IDs return false.
|
||||
func (e *LoadError) Source(id string) (SourceInfo, bool) {
|
||||
if e == nil || e.value == nil {
|
||||
return SourceInfo{}, false
|
||||
}
|
||||
value, ok := e.value.Source(id)
|
||||
if !ok {
|
||||
return SourceInfo{}, false
|
||||
}
|
||||
return fromEngineSourceInfo(value), true
|
||||
}
|
||||
|
||||
// WriteDiagnostics writes diagnostics in canonical order. It escapes terminal
|
||||
// control characters and propagates writer errors, including short writes.
|
||||
func WriteDiagnostics(output io.Writer, diagnostics []Diagnostic) error {
|
||||
|
||||
22
diff.go
22
diff.go
@@ -1,15 +1,14 @@
|
||||
package configkit
|
||||
|
||||
import "gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
import "go.maximumdirect.net/configkit/internal/engine"
|
||||
|
||||
// Difference describes one canonical changed region between two successful
|
||||
// loads. Left is the earlier value and Right is the later value. Each snapshot
|
||||
// is detached; Present distinguishes absence from an explicit null, and
|
||||
// Redacted reports whether combined sensitivity affected that side. Mappings
|
||||
// are compared recursively, while sequences are reported as atomic values.
|
||||
// Records are ordered by canonical JSON pointer. HiddenChanged indicates that
|
||||
// a value concealed by combined sensitivity changed, without revealing which
|
||||
// concealed member changed.
|
||||
// Difference describes one changed value or subtree between two successful loads.
|
||||
// Left is the earlier value and Right is the later value. Snapshots are copies;
|
||||
// Present distinguishes absence from explicit null. Redacted reports hiding
|
||||
// based on sensitivity from either load. Mappings compare by key; a changed
|
||||
// sequence is reported as a whole value. Records are ordered by canonical JSON
|
||||
// pointer. HiddenChanged reports an actual hidden change, without identifying
|
||||
// the hidden member. Redaction alone does not set it.
|
||||
type Difference struct {
|
||||
Path Path
|
||||
Left ValueSnapshot
|
||||
@@ -17,8 +16,9 @@ type Difference struct {
|
||||
HiddenChanged bool
|
||||
}
|
||||
|
||||
// Diff compares the frozen effective values of two successful loads of the
|
||||
// same configuration type. It does not reread sources, environment variables,
|
||||
// Diff compares the saved final values of two successful loads of the same
|
||||
// configuration type, independently of later changes to Result.Config.
|
||||
// It does not reread sources, environment variables,
|
||||
// or rerun defaults or resolvers. Equal values produce no records even when
|
||||
// their sources, defaults, or resolution histories differ. Credential contents
|
||||
// are not compared: typed secret references compare by reference name only.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package configkit
|
||||
|
||||
import "gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
import "go.maximumdirect.net/configkit/internal/engine"
|
||||
|
||||
// Search is an immutable ordered description of possible host-file inputs.
|
||||
// It is consumed only when passed to SearchConfig and used by a load.
|
||||
@@ -52,8 +52,8 @@ func SearchDirectory(directory string, supplied ...DirectorySearchOptions) Searc
|
||||
return Search{value: engine.SearchDirectory(directory, options...)}
|
||||
}
|
||||
|
||||
// Paths describes literal host-file candidates in the supplied order.
|
||||
func Paths(paths ...string) Search { return Search{value: engine.Paths(paths...)} }
|
||||
// SearchPaths describes literal host-file candidates in the supplied order.
|
||||
func SearchPaths(paths ...string) Search { return Search{value: engine.Paths(paths...)} }
|
||||
|
||||
// Searches concatenates descriptions in order and snapshots their entries.
|
||||
func Searches(searches ...Search) Search {
|
||||
|
||||
22
doc.go
22
doc.go
@@ -1,9 +1,17 @@
|
||||
// Package configkit provides deterministic, application-owned configuration
|
||||
// loading for Go programs.
|
||||
// Package configkit loads YAML into an application's Go configuration type and
|
||||
// lets the application inspect where the final values came from.
|
||||
//
|
||||
// Load supports file, byte, reader, and explicit no-input sources with strict YAML,
|
||||
// typed defaults, imports, overlays, CLI overrides, schema-version checks, detached
|
||||
// metadata, recorded application resolution, typed secret references, safe effective
|
||||
// rendering, read-only semantic validation, and structured load errors. The
|
||||
// configuration contract is documented in docs/config.md.
|
||||
// Start with Load and a File, Bytes, Reader, Mapping, or Filesystem.File source.
|
||||
// Use NoSource for deliberately absent input or SearchConfig for an explicitly
|
||||
// chosen search. Applications supply types, defaults, version rules, and callbacks
|
||||
// for deriving and validating values. LoadLayered adds lower-priority input groups.
|
||||
//
|
||||
// A successful load returns the final configuration and saved inspection state.
|
||||
// Metadata tracks supplied values, including false and zero; explanations retain
|
||||
// safe earlier values. WriteEffective and Diff use the saved state even if the
|
||||
// application later changes Result.Config. Secret references resolve credentials
|
||||
// during loading, while inspection follows the configured hiding rules.
|
||||
//
|
||||
// The consumer guide is in docs/consumers/README.md. The exact YAML, combination,
|
||||
// lifecycle, and ownership rules are documented in docs/config.md.
|
||||
package configkit
|
||||
|
||||
@@ -10,6 +10,9 @@ request, and review records are preserved at `e4270c9` under `docs/roadmap/` in
|
||||
Git history; the original input-destination implementation plan remains at
|
||||
`315f464:docs/roadmap/implementation.md`.
|
||||
|
||||
[ADR 0020](0020-document-root-controls.md) amends the scope of the control-name
|
||||
reservation. The other placement decisions in this record remain in force.
|
||||
|
||||
## Date
|
||||
|
||||
2026-10-14
|
||||
|
||||
61
docs/adr/0020-document-root-controls.md
Normal file
61
docs/adr/0020-document-root-controls.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# ADR 0020: Scope Configuration Controls to Document Roots
|
||||
|
||||
## Status
|
||||
|
||||
Accepted.
|
||||
|
||||
## Date
|
||||
|
||||
2026-11-01
|
||||
|
||||
## Context
|
||||
|
||||
ConfigKit interprets configuration controls during source acquisition, before
|
||||
placing and composing source payloads. Reserving the control name throughout
|
||||
application data restricts paths unrelated to source-document controls. The
|
||||
source-local position of a control and the effective application path of a
|
||||
value are different coordinates, even when their path text is identical.
|
||||
|
||||
## Decision
|
||||
|
||||
Use `config` as the single control mapping name at the root of each acquired
|
||||
source document. Interpret each source's controls before placing its payload,
|
||||
using the existing context-specific grammar and import opt-in. A source retains
|
||||
its own document root regardless of its eventual destination, including when
|
||||
mounted below another value or as a sequence record.
|
||||
|
||||
Below a source root, `config` is ordinary application data, including in
|
||||
sequence records and selected overlays. Control-looking children in such data
|
||||
do not activate controls. Application-root `config` data produced by placement,
|
||||
defaults, CLI assignment, or resolution is also ordinary data. Placement and
|
||||
later lifecycle operations do not create another source-document parsing pass.
|
||||
|
||||
The former spellings `configkit` and `_configkit` are ordinary application keys
|
||||
at any depth, subject to normal schema and structural rules; they are not
|
||||
control aliases. Source-version checks protect actual source-local controls,
|
||||
not an application path merely because its spelling matches. Existing `extends`
|
||||
inheritance semantics and restrictions remain in force.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Reserve the control name throughout application data.** This conflates
|
||||
source-document syntax with application paths and prevents ordinary fields
|
||||
unrelated to control interpretation.
|
||||
- **Recognize controls again after placement or later operations.** This makes
|
||||
application data acquire control semantics according to its destination and
|
||||
introduces a second interpretation pass.
|
||||
- **Support multiple or configurable control spellings.** This makes control
|
||||
interpretation ambiguous and expands the configuration contract.
|
||||
|
||||
## Consequences
|
||||
|
||||
Control evidence uses source-local coordinates while assembled values use
|
||||
application coordinates. Existing source identities and contexts distinguish
|
||||
these uses without a new inspection-record format. Consumers must migrate
|
||||
genuine old control blocks and account for source-root application fields
|
||||
named `config`; ordinary nested and assembled root fields remain available.
|
||||
|
||||
The completed feature roadmap and implementation plan are preserved in Git
|
||||
history at `c63f4ff` under `docs/roadmap/`, for example
|
||||
`git show c63f4ff:docs/roadmap/implementation.md`.
|
||||
The [configuration contract](../config.md) owns implemented behavior.
|
||||
895
docs/config.md
895
docs/config.md
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,45 @@
|
||||
# Consumer Adoption Guide
|
||||
|
||||
ConfigKit removes parser setup, strict decoding, presence bookkeeping, import and
|
||||
selected-overlay traversal, precedence, provenance, secret lookup, and safe inspection from
|
||||
applications. Consumers still own their configuration structs, defaults, semantic
|
||||
validation, schema-version policy, CLI parsing, runtime matching and conversion,
|
||||
resource checks, remote acquisition, and service initialization.
|
||||
ConfigKit loads configuration into your application's Go types. You provide
|
||||
those types, defaults, and application rules. ConfigKit reads and combines the
|
||||
selected inputs, checks their types, and returns values with source information.
|
||||
Your application still owns CLI parsing, resource reads, remote acquisition, and
|
||||
runtime initialization.
|
||||
|
||||
Import `"go.maximumdirect.net/configkit"`; the package name is `configkit`.
|
||||
|
||||
Document controls now use root `config` in every source. Rename genuine old
|
||||
`configkit` or `_configkit` control blocks and update stored declaration paths
|
||||
from `/configkit/...` to `/config/...`. Old blocks become ordinary data and a
|
||||
permissive schema can accept them without executing imports. Move, rename, or
|
||||
separately mount application fields previously authored as source-root `config`.
|
||||
Nested and assembled root `config` values remain ordinary application data; the
|
||||
[mounted-root example](../../examples/consumers/config_controls_test.go) shows
|
||||
the boundary. See the [configuration contract](../config.md#declaration-and-selection-rules)
|
||||
for the complete rules.
|
||||
Start with one file, then choose the tasks your application needs:
|
||||
|
||||
| Task | Start here |
|
||||
| --- | --- |
|
||||
| Load one file | [Happy path](#happy-path) and the [README example](../../README.md#load-a-configuration-file) |
|
||||
| Supply fixed defaults and check application rules | [Defaults and application rules](#defaults-and-application-rules) |
|
||||
| Add bundled defaults beneath application settings | [Native filesystem and layered input](#native-filesystem-and-layered-input) |
|
||||
| Select files or an overlay | [Discovery](#explicit-main-file-discovery), [import defaults](#import-defaults-and-selection-inspection), and the [overlay reference](../config.md#selected-overlays) |
|
||||
| Reuse settings or combine record lists | [Inheritance](#named-sibling-inheritance) and [identified sequences](#identified-prompt-sequences) |
|
||||
| Handle credentials | [Typed-secret reference](../config.md#typed-secret-references) and the [startup example](../../examples/consumers/consumers_test.go) |
|
||||
| Inspect sources, earlier values, or changes | [Inspect a completed load](#inspect-a-completed-load) and [diffs](#effective-configuration-diffs) |
|
||||
| Upgrade an existing consumer | [Migration guidance](#intentional-migration-changes) |
|
||||
|
||||
## Choosing a loading entry point
|
||||
|
||||
- Use [`Load`](../../lifecycle.go) with one source for an ordinary configuration.
|
||||
[`SearchConfig`](../../discovery.go) is also a source, so its explicit
|
||||
discovery result can be passed to `Load` or used as the main input to
|
||||
[`LoadLayered`](../../layered.go).
|
||||
- Use `LoadLayered` when inputs must be composed in underlay groups before the
|
||||
main source. Each [`Input`](../../layered.go) owns its source, filesystem,
|
||||
destination, and import choice. The overlay filesystem context is selected
|
||||
independently in [`LayeredOptions`](../../layered.go).
|
||||
[`SearchConfig`](../../discovery.go) is also a source; pass it to `Load` or use
|
||||
it as the main input to [`LoadLayered`](../../layered.go).
|
||||
- Use `LoadLayered` when several inputs should supply lower-priority defaults
|
||||
beneath the main source. Each underlay group combines its inputs strictly.
|
||||
Each [`Input`](../../layered.go) owns its source, filesystem, destination, and
|
||||
import choice. The overlay filesystem context is selected independently in
|
||||
[`LayeredOptions`](../../layered.go).
|
||||
- Load-wide rules belong to [`Options`](../../lifecycle.go) or
|
||||
[`LayeredOptions`](../../layered.go): the application definition (including
|
||||
version policy), import defaults, and explicit overrides. These do not turn
|
||||
@@ -30,68 +54,68 @@ file and layered cases. For import-focused adoption, see the
|
||||
discovery composed with sequences, see the
|
||||
[sequence catalog](../../examples/consumers/sequence_catalog.go).
|
||||
|
||||
The maintained [`examples/consumers`](../../examples/consumers) package demonstrates
|
||||
sanitized shapes using only ConfigKit and the Go standard library:
|
||||
|
||||
| Shape | Demonstrated fit | Application code deliberately retained |
|
||||
| --- | --- | --- |
|
||||
| Narratio-like | Fresh defaults, explicit partial imports, artifact maps, recorded inherited defaults, normalization, and source ownership | Artifact meaning and the separate `model_profile` domain field |
|
||||
| Notarius-like | Canonical object bindings, a string-backed schema version accepting string or integer YAML, strict dynamic options, and `int64` dynamic integers | Catalog selection, allowed option keys, range-checked conversion to runtime `int` |
|
||||
| WeatherReporter-like | Defaults-only loading, report maps, canonical module objects, typed option unions, explicit CLI records, and typed secret startup | Report/module compatibility, rejection of explicitly empty domain lists, CLI parsing, and startup ordering |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| Mounted collections | Entry-declared relative destinations, nested metadata/prompt/profile imports, local inheritance, and a version-only higher-tier override | Collection IDs, profile meaning, and opening/interpreting prompt resources |
|
||||
| Named presets | One-load sibling inheritance, explicit false, nested destination resolution, and source/derivation inspection | Choosing and executing a preset |
|
||||
| Effective configuration diff | Same-type frozen effective comparisons for two selected files or base-versus-overlay impact | Source/options selection and consumer-owned safe presentation |
|
||||
| Shared source-version checks | One `Definition.Version` policy for accepted declarations across participating source documents, with optional or required declarations and sanitized evidence | Choosing which declarations the application accepts; final assembled schema and domain validation |
|
||||
| Sequence catalogs | Declared collection operations, record inheritance, typed CLI and resolution, inspection, exact and wildcard application registrations | Catalog meaning and final domain validation |
|
||||
|
||||
The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the
|
||||
executable examples and ordinary tests. It contains no private application data,
|
||||
credentials, network calls, or dependency on a sibling checkout.
|
||||
|
||||
The Notarius-shaped fixtures deliberately load both `version: 4` and
|
||||
`version: "4"` into the same string-backed application field. Its version checker
|
||||
still owns the supported identifier; fractional numeric input and unsupported
|
||||
integer identifiers remain negative examples.
|
||||
|
||||
## Happy path
|
||||
|
||||
An application defines ordinary types and passes one explicit source. Optional
|
||||
features appear only when the application needs them:
|
||||
Define the Go configuration type, choose one source, and call `Load`. Handle a
|
||||
load error before starting the application with `result.Config`. The
|
||||
[README example](../../README.md#load-a-configuration-file) shows this complete
|
||||
minimal loading function. An explicitly missing file is an error; use `NoSource`
|
||||
when you deliberately want defaults without a main document.
|
||||
|
||||
### Defaults and application rules
|
||||
|
||||
Using the README's `Config` type, an application can add reusable rules:
|
||||
|
||||
```go
|
||||
result, err := configkit.Load[Config](configkit.File(path), configkit.Options[Config]{
|
||||
Definition: rules,
|
||||
Imports: useDeclaredImports,
|
||||
Overlay: selectedConfigOverlay,
|
||||
Overrides: explicitlySetFlags,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
definition := configkit.Definition[Config]{
|
||||
Defaults: func() Config { return Config{Enabled: true, Workers: 2} },
|
||||
Validate: func(value *Config, validation *configkit.Validation) {
|
||||
if value.Workers < 0 {
|
||||
validation.Error(configkit.Keys("workers"), "must be nonnegative")
|
||||
}
|
||||
},
|
||||
}
|
||||
return start(result.Config)
|
||||
```
|
||||
|
||||
Simple consumers use neither imports nor selected overlays. A complex consumer can opt into
|
||||
both without redefining their syntax. ConfigKit overlays are configuration overlays;
|
||||
they do not select or replace an application's LLM, model, pipeline, report, or
|
||||
execution profile concepts.
|
||||
Pass this definition in `Options[Config]{Definition: definition}` when calling
|
||||
`Load`. Supplied `enabled: false` or `workers: 0` replaces those defaults.
|
||||
Use `Resolve` when a value must be derived or normalized before validation, and
|
||||
record that change with the provided helpers. The
|
||||
[callback reference](../config.md#recorded-application-resolution) compares the
|
||||
helpers; [executable examples](../../example_test.go) show full definitions.
|
||||
|
||||
### Inspect a completed load
|
||||
|
||||
Keep the result when you need to explain configuration. Use
|
||||
`result.Metadata().Lookup(configkit.Keys("workers"))` to inspect whether input
|
||||
supplied the value and which source or application step supplied it. Follow an
|
||||
origin's source ID with `result.Source(id)` when you need its source record.
|
||||
|
||||
Use `Explain` for earlier values, `WriteExplanation` for a readable explanation,
|
||||
and `WriteEffective` for safe YAML describing the final saved state. These
|
||||
operations describe the completed load even if your application later changes
|
||||
`result.Config`. The [inspection reference](../config.md#layered-metadata)
|
||||
explains returned values, copies, and hidden information.
|
||||
|
||||
Add imports, a selected overlay, or explicitly set CLI values through options
|
||||
only when needed. An overlay changes configuration; choosing an LLM, report, or
|
||||
execution profile remains application logic.
|
||||
|
||||
## Explicit main-file discovery
|
||||
|
||||
Applications choose the search conventions and their precedence, then pass the
|
||||
resulting `SearchConfig` source to the same `Load` or `LoadLayered` entry point.
|
||||
For example, the maintained sequence catalog puts a caller-selected file before
|
||||
a custom directory fallback while keeping its underlay and overlay choices
|
||||
independent. See [`sequence_catalog.go`](../../examples/consumers/sequence_catalog.go)
|
||||
and its [behavior tests](../../examples/consumers/sequence_catalog_test.go), plus
|
||||
the [working-directory executable example](../../examples/consumers/discovery_test.go).
|
||||
Together, the root-owned [discovery API](../../discovery.go) and [configuration
|
||||
discovery contract](../config.md#configuration-discovery) define ordered
|
||||
concatenation, directory-first Cartesian expansion, optional exhaustion, and
|
||||
selected-file behavior. [ADR 0019](../adr/0019-explicit-configuration-discovery.md)
|
||||
resulting `SearchConfig` source to the same `Load` or `LoadLayered` entry
|
||||
point. For example, the maintained sequence catalog puts a caller-selected
|
||||
file before a custom directory fallback while keeping its underlay and overlay
|
||||
choices independent. See
|
||||
[`sequence_catalog.go`](../../examples/consumers/sequence_catalog.go) and its
|
||||
[behavior tests](../../examples/consumers/sequence_catalog_test.go), plus the
|
||||
[working-directory executable
|
||||
example](../../examples/consumers/discovery_test.go). Together, the [discovery
|
||||
API](../../discovery.go) and [configuration discovery
|
||||
contract](../config.md#configuration-discovery) explain the order of attempted
|
||||
files, what happens when no file is found, and why a selected-file failure
|
||||
stops the search. [ADR 0019](../adr/0019-explicit-configuration-discovery.md)
|
||||
records why discovery is explicitly selected.
|
||||
|
||||
## Import defaults and selection inspection
|
||||
@@ -99,15 +123,20 @@ records why discovery is explicitly selected.
|
||||
`Options.ImportDefaults` and `LayeredOptions.ImportDefaults` apply one
|
||||
load-wide policy to enabled main, underlay, and transitive imports. Each field
|
||||
is resolved independently: a declaration's explicit value wins, then the
|
||||
application default, then the library default. `Required`, `Follow`, `Hidden`,
|
||||
and `Recursive` use nil pointers for unspecified values. `Pattern` applies to
|
||||
application default, then the library default. `Required`, `FollowSymlinks`,
|
||||
`IncludeHidden`, and `Recursive` use nil pointers for unspecified values.
|
||||
`Pattern` applies to
|
||||
directory searches: nil leaves filenames unrestricted, while a non-nil empty
|
||||
slice selects no files. A declaration's own `pattern` replaces the application
|
||||
default. The defaults do not enable imports or change selected-overlay behavior.
|
||||
The Go fields `FollowSymlinks` and `IncludeHidden` correspond to the unchanged
|
||||
YAML keys `follow` and `hidden`.
|
||||
|
||||
`Result.Sources`, `LoadError.Sources`, and `Resolution.Source` expose each
|
||||
declaring source's `ImportSelections`. These records identify the source-local
|
||||
declaration path, its authored location and selector, and the `none` outcome;
|
||||
declaring source's `EmptyImportSelections`. `Result.Source(id)` and
|
||||
`LoadError.Source(id)` retrieve one retained record directly when its load-local
|
||||
ID is known. These records identify the source-local declaration path, its
|
||||
authored location and selector;
|
||||
they do not include candidate names or a guessed reason for absence. Returned
|
||||
records are detached snapshots. The [configuration contract](../config.md#explicit-imports)
|
||||
owns selector applicability and exact absence semantics.
|
||||
@@ -117,7 +146,7 @@ owns selector applicability and exact absence semantics.
|
||||
[`LoadVersionedService`](../../examples/consumers/source_version.go) demonstrates
|
||||
if-present and required modes with one `Definition.Version` rule across a mounted
|
||||
main input and an underlay. The example reads only sanitized
|
||||
`SourceInfo.VersionCheck` fields; its executable test is in
|
||||
`SourceInfo.VersionEvidence` fields; its executable test is in
|
||||
[`layered_test.go`](../../examples/consumers/layered_test.go). The
|
||||
[configuration contract](../config.md#shared-source-version-checks) defines
|
||||
participation, source-local lookup and diagnostics, non-main header consumption,
|
||||
@@ -135,13 +164,14 @@ priority values remain present. Its resolver records a derived endpoint URL afte
|
||||
overlay application, and its nested typed secret reference is classified before
|
||||
the resulting records are presented.
|
||||
|
||||
The maintained [behavior tests and executable examples](../../examples/consumers/diff_test.go)
|
||||
use temporary files and disposable secret values. Presenters should use
|
||||
`ValueSnapshot.Present` for absence, record membership for change, and
|
||||
`HiddenChanged` for a withheld change indicator. Do not parse the redaction marker
|
||||
or add separately rendered full contexts that lack combined sensitivity. The
|
||||
[configuration contract](../config.md#effective-configuration-diffs) owns shared
|
||||
equality, granularity, and disclosure semantics.
|
||||
The maintained [behavior tests and executable
|
||||
examples](../../examples/consumers/diff_test.go) use temporary files and
|
||||
disposable secret values. Presenters should use `ValueSnapshot.Present` for
|
||||
absence, record membership for change, and `HiddenChanged` for a withheld
|
||||
change indicator. Do not parse the redaction marker or add separately rendered
|
||||
full contexts that lack combined sensitivity. The [configuration
|
||||
contract](../config.md#effective-configuration-diffs) owns shared equality,
|
||||
granularity, and disclosure semantics.
|
||||
|
||||
## Named sibling inheritance
|
||||
|
||||
@@ -264,14 +294,13 @@ conversion API.
|
||||
## Source-aware application path policy
|
||||
|
||||
[`LoadNotariusPaths`](../../examples/consumers/consumers.go) is the maintained
|
||||
path-bearing example. Its application resolver uses `Resolution.InputInfo` and
|
||||
path-bearing example. Its application resolver uses `Resolution.EntryInfo` and
|
||||
`Resolution.Source` before normalizing: relative CLI input uses an explicitly
|
||||
supplied invocation directory, retained defaults use the canonical main source,
|
||||
and a file-authored scalar uses its one declaring source. Absolute paths remain
|
||||
unchanged. Its executable test covers the matching effective YAML and explanation
|
||||
history, source-file ownership, and a symlink opened through an alias; canonical
|
||||
target-file directories, rather than the alias directory or process working
|
||||
directory, are the selected bases.
|
||||
unchanged. Its executable test checks the resulting YAML and explanation
|
||||
history. When the configuration filename is a symlink, this example uses the
|
||||
actual target file's directory as the base for file-authored paths.
|
||||
|
||||
The selected example policy leaves a relative value unchanged and emits a warning
|
||||
when a CLI base is absent or file provenance is missing or composite. That is an
|
||||
@@ -281,7 +310,15 @@ existence checks nor confinement checks.
|
||||
|
||||
## Intentional migration changes
|
||||
|
||||
The examples preserve domain meaning while adopting one configuration convention:
|
||||
When upgrading, start with the changes relevant to your dependency and inputs:
|
||||
|
||||
1. Update the module requirement and Go imports together as described below.
|
||||
2. Replace old public names using the old/new table.
|
||||
3. Update any layer or diagnostic-code strings stored or compared by your code.
|
||||
4. Review YAML and startup changes that apply to your application, then rebuild
|
||||
and run its configuration tests.
|
||||
|
||||
The maintained examples illustrate these configuration changes:
|
||||
|
||||
- Explicit empty strings, lists, zeroes, false values, and nulls remain intentional.
|
||||
ConfigKit accepts an empty list structurally; WeatherReporter-shaped validation
|
||||
@@ -301,10 +338,70 @@ The examples preserve domain meaning while adopting one configuration convention
|
||||
parsers translate only explicitly set configuration flags into `Override` records.
|
||||
Separate invocation state remains outside configuration.
|
||||
- Credential fields migrate to environment-name `Secret` references. Applications
|
||||
explicitly call `LoadSecretsDir` before `Load` when they choose file bootstrap;
|
||||
explicitly call `LoadSecretsIntoEnvironment` before `Load` when they choose
|
||||
file bootstrap;
|
||||
blocked empty variables remain blocked, and ordinary `Load` never changes the
|
||||
process environment.
|
||||
|
||||
### Vanity module path migration
|
||||
|
||||
ConfigKit's module identifier changed from
|
||||
`gitea.maximumdirect.net/eric/ConfigKit` to `go.maximumdirect.net/configkit` before
|
||||
v1.0.0. Update the dependency requirement in your application's `go.mod` and its
|
||||
Go imports together, selecting a revision or release that declares the new module
|
||||
path. The package name remains `configkit`; the configuration model and public
|
||||
symbol names are unchanged by this identity migration.
|
||||
|
||||
Go treats the old and new import paths as different packages, with different
|
||||
named types. For example, if another library returns a ConfigKit `Path` or
|
||||
`Secret` using the old import, your application cannot use it as the new
|
||||
package's type just by changing a module requirement. Coordinate migration
|
||||
with dependencies that pass ConfigKit values to or from your application. The
|
||||
maintained examples use the new path, but other repositories must manage their
|
||||
own migrations.
|
||||
|
||||
Historical tags keep their original module declarations and contents. Older
|
||||
releases such as v0.10.2 still use the old path and cannot be installed under the
|
||||
vanity identifier simply because discovery can find their repository. A matching
|
||||
migrated revision is needed; no old-path compatibility module is supplied here.
|
||||
|
||||
### Pre-v1 public API cleanup
|
||||
|
||||
The pre-v1 API cleanup replaces these names outright; it does not retain
|
||||
compatibility aliases:
|
||||
|
||||
| Previous API | Current API |
|
||||
| --- | --- |
|
||||
| `Resolution.InputInfo(path)` | `Resolution.EntryInfo(path)` |
|
||||
| `Definition.Secret` | `Definition.Sensitive` |
|
||||
| `SourceVersionModeDefault` | `SourceVersionModeMainOnly` |
|
||||
| `SourceVersionCheck` | `SourceVersionEvidence` |
|
||||
| `SourceInfo.VersionCheck` | `SourceInfo.VersionEvidence` |
|
||||
| `ImportSelection` | `EmptyImportSelection` |
|
||||
| `SourceInfo.ImportSelections` | `SourceInfo.EmptyImportSelections` |
|
||||
| `SourceInfo.DestinationUnresolved` | `SourceInfo.DestinationIsFallback` |
|
||||
| `ImportDefaults.Follow` / `.Hidden` | `FollowSymlinks` / `IncludeHidden` |
|
||||
| `Paths(paths...)` | `SearchPaths(paths...)` |
|
||||
| `LoadSecretsDir(directory)` | `LoadSecretsIntoEnvironment(directory)` |
|
||||
| `LayerBase` | `LayerMain` |
|
||||
| `CodeExplainLimitExceeded` | `CodeHistoryLimitExceeded` |
|
||||
| `CodeResolveInvalidKind` / `CodeOptionsUnavailable` | Removed; no replacement |
|
||||
| `ImportSelection.Outcome`, `ImportSelectionOutcome`, `ImportSelectionNone` | Removed; empty-selection records no longer carry an outcome |
|
||||
| `Resolution.Record(path, ChangeDerivedDefault, reason, deps...)` | `Resolution.RecordDerivedDefault(path, reason, deps...)` |
|
||||
| `Resolution.Record(path, ChangeNormalization, reason, deps...)` | `Resolution.RecordNormalization(path, reason, deps...)` |
|
||||
| Enumerating `Result.Sources()` / `LoadError.Sources()` to find one ID | `Result.Source(id)` / `LoadError.Source(id)` |
|
||||
| Provenance string `"base"` | `"main"` |
|
||||
| History diagnostic string `"explain.limit_exceeded"` | `"history.limit_exceeded"` |
|
||||
|
||||
The YAML import keys `follow` and `hidden`, the `configkit:"secret"` tag, and
|
||||
typed `Secret` references are unchanged. If your application compares or
|
||||
stores layer names or diagnostic codes as strings, update those strings
|
||||
alongside Go references. This repository guidance does not claim that
|
||||
downstream applications have migrated and does not select a release version
|
||||
for this breaking pre-v1 change. `ChangeKind` and its inspection constants
|
||||
remain; use `Default` and `Normalize` when the helper should both assign and
|
||||
record a value.
|
||||
|
||||
### Selected-overlay migration
|
||||
|
||||
To adopt the current selected-overlay API, rename `Options.Profile`,
|
||||
@@ -315,10 +412,40 @@ The [configuration contract](../config.md#selected-overlays) is the canonical
|
||||
behavioral reference. Application-owned profile concepts, such as `model_profile`,
|
||||
are unchanged, and any CLI flag rename remains the application's decision.
|
||||
|
||||
These examples are migration evidence, not completed changes to Narratio, Notarius,
|
||||
WeatherReporter, or Seriatim. Updating those repositories, their deployed YAML, CLI
|
||||
wiring, runtime catalogs, and operational documentation remains separate downstream
|
||||
work.
|
||||
### Background on the examples
|
||||
|
||||
These examples demonstrate migration shapes. They do not claim that Narratio,
|
||||
Notarius, WeatherReporter, or Seriatim has been migrated. Updating those
|
||||
repositories, their deployed YAML, CLI wiring, runtime catalogs, and
|
||||
operational documentation remains separate downstream work.
|
||||
|
||||
## Example background
|
||||
|
||||
The maintained [`examples/consumers`](../../examples/consumers) package demonstrates
|
||||
sanitized shapes using only ConfigKit and the Go standard library:
|
||||
|
||||
| Shape | Demonstrated fit | Application code deliberately retained |
|
||||
| --- | --- | --- |
|
||||
| Artifact defaults and normalization (Narratio-like) | Fresh defaults, explicit partial imports, artifact maps, recorded inherited defaults, normalization, and source ownership | Artifact meaning and the separate `model_profile` domain field |
|
||||
| Versions and dynamic options (Notarius-like) | Canonical object bindings, a string-backed schema version accepting string or integer YAML, strict dynamic options, and `int64` dynamic integers | Catalog selection, allowed option keys, range-checked conversion to runtime `int` |
|
||||
| Module validation and credential startup (WeatherReporter-like) | Defaults-only loading, report maps, canonical module objects, typed option unions, explicit CLI records, and typed secret startup | Report/module compatibility, rejection of explicitly empty domain lists, CLI parsing, and startup ordering |
|
||||
| Independent ordered rule files (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 |
|
||||
| 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 |
|
||||
| Mounted collections | Entry-declared relative destinations, nested metadata/prompt/profile imports, local inheritance, and a version-only higher-tier override | Collection IDs, profile meaning, and opening/interpreting prompt resources |
|
||||
| Named presets | One-load sibling inheritance, explicit false, nested destination resolution, and source/derivation inspection | Choosing and executing a preset |
|
||||
| Effective configuration diff | Same-type frozen effective comparisons for two selected files or base-versus-overlay impact | Source/options selection and consumer-owned safe presentation |
|
||||
| Shared source-version checks | One `Definition.Version` policy for accepted declarations across participating source documents, with optional or required declarations and sanitized evidence | Choosing which declarations the application accepts; final assembled schema and domain validation |
|
||||
| Sequence catalogs | Declared collection operations, record inheritance, typed CLI and resolution, inspection, exact and wildcard application registrations | Catalog meaning and final domain validation |
|
||||
|
||||
The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the
|
||||
executable examples and ordinary tests. It contains no private application data,
|
||||
credentials, network calls, or dependency on a sibling checkout.
|
||||
|
||||
The Notarius-shaped fixtures deliberately load both `version: 4` and
|
||||
`version: "4"` into the same string-backed application field. Its version checker
|
||||
still owns the supported identifier; fractional numeric input and unsupported
|
||||
integer identifiers remain negative examples.
|
||||
|
||||
## Maintained API examples
|
||||
|
||||
@@ -330,7 +457,7 @@ focused behavioral owner:
|
||||
| `Load`, `NoSource`, validation, versions, resolution, `SecretEnv`, overrides, and explanation | Executable examples in [`example_test.go`](../../example_test.go) |
|
||||
| Source/import/selected-overlay pipeline rules and artifact defaults | Narratio-shaped executable example and fixture |
|
||||
| Canonical module objects and dynamic option handling | WeatherReporter- and Notarius-shaped examples and migration tests |
|
||||
| `LoadSecretsDir`, bootstrap reporting, typed lookup, CLI explanation | `Example_weatherStartup` |
|
||||
| `LoadSecretsIntoEnvironment`, 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 |
|
||||
| Literal input destinations, root-default compatibility, and mounted peer collections | `Example_inputDestination`, `LoadCollectionBundle`, and `destinations_test.go` |
|
||||
| Named sibling inheritance, presence, and derivation | `Example_inheritedPresets` and `TestPresetCatalogOneLoadAndDerivation` |
|
||||
|
||||
@@ -64,6 +64,7 @@ Accepted decisions:
|
||||
- [Define identity and merge rules for each collection](adr/0017-collection-sequence-policy.md)
|
||||
- [Apply CLI overrides after inheritance and decoding](adr/0018-cli-after-structural-resolution.md)
|
||||
- [Keep main-file discovery explicit and composable](adr/0019-explicit-configuration-discovery.md)
|
||||
- [Scope configuration controls to document roots](adr/0020-document-root-controls.md)
|
||||
|
||||
For cross-cutting changes, follow every applicable row. Completed implementation
|
||||
and remediation plans are preserved in Git history at `cb5b435`; use current
|
||||
@@ -189,10 +190,42 @@ remediation record are preserved in Git history at `0369111`, for example
|
||||
[configuration discovery](config.md#configuration-discovery); the public
|
||||
[discovery declarations](../discovery.go) own the Go API.
|
||||
|
||||
The [consolidation roadmap](roadmap/consolidation.md) owns the current refactoring
|
||||
proposal and the associated complexity-audit findings.
|
||||
The completed consolidation proposal, implementation plan, and resolved audit
|
||||
findings are preserved in Git history at `34cbc0d` under `docs/roadmap/`. For
|
||||
example:
|
||||
|
||||
```sh
|
||||
git show 34cbc0d:docs/roadmap/implementation.md
|
||||
git show 34cbc0d:docs/roadmap/remediation.md
|
||||
```
|
||||
|
||||
Potential enhancements are tracked in [future features](roadmap/future.md).
|
||||
The completed audit-remediation, internal-boundary simplification, shared-history,
|
||||
and public API cleanup roadmaps, reviews, and resolved findings are preserved at
|
||||
`68400ff` under `docs/roadmap/`. For example:
|
||||
|
||||
```sh
|
||||
git show 68400ff:docs/roadmap/api.md
|
||||
git show 68400ff:docs/roadmap/remediation.md
|
||||
```
|
||||
|
||||
Their earlier completed implementation plans are preserved at
|
||||
`bb5540b:docs/roadmap/implementation.md` (audit remediation),
|
||||
`f6d54a0:docs/roadmap/implementation.md` (internal-boundary simplification), and
|
||||
`07e4ae5:docs/roadmap/implementation.md` (shared history). The plan at
|
||||
`68400ff:docs/roadmap/implementation.md` records the public API cleanup.
|
||||
Current behavior and migration information belong to the
|
||||
[configuration contract](config.md), public declaration comments, and
|
||||
[consumer guide](consumers/README.md#pre-v1-public-api-cleanup).
|
||||
|
||||
The consolidated audit, including
|
||||
findings, evidence, and settled scope decisions, is preserved at
|
||||
`453da68:docs/roadmap/audit.md`; the original nine reports are preserved at
|
||||
`ecd5d5d` under `docs/roadmap/`. Retrieve the consolidated record with:
|
||||
|
||||
```sh
|
||||
git show 453da68:docs/roadmap/audit.md
|
||||
```
|
||||
|
||||
Install the Go version required by `go.mod` or a compatible newer toolchain.
|
||||
Acquire declared dependencies explicitly before entering an offline
|
||||
@@ -241,28 +274,27 @@ code changes. Use the component overview to find the corresponding implementatio
|
||||
| --- | --- | --- |
|
||||
| YAML syntax, diagnostics, parser limits | [Adapter tests](../internal/yaml/parse_test.go), [source translation](../internal/engine/parse_test.go) | `go test ./internal/yaml` and `go test ./internal/engine -run '^TestParseSource'` |
|
||||
| File/reader access, imports, globs, containment | [Import internals](internal/imports.md), [source acquisition](../internal/engine/source_test.go), [FIFO](../internal/engine/source_fifo_test.go), [import traversal](../internal/engine/imports_test.go), [import evidence](../internal/engine/import_evidence_test.go), [relative destinations](../internal/engine/destination_import_test.go), [globs](../internal/engine/glob_test.go) | `go test ./internal/engine -run 'Source\|Import\|Glob\|Filesystem\|Nonregular\|RegularFile'` |
|
||||
| Explicit main-file discovery and absent-input loading | [Candidate selection](../internal/engine/discovery_test.go), [load integration](../internal/engine/discovery_load_test.go), [public facade](../facade_discovery_test.go), and maintained [catalog](../examples/consumers/sequence_catalog_test.go) and [preset](../examples/consumers/discovery_test.go) examples | `go test ./internal/engine -run 'Discovery|Search'`, `go test . -run 'Search'`, and `go test ./examples/consumers -run 'WorkingDirectoryPreset|SequenceCatalog'` |
|
||||
| Import defaults, empty-selection evidence, and dependency-ready ordering | [Defaults and facade evidence](../facade_import_defaults_test.go), [engine import evidence](../internal/engine/import_evidence_test.go), [scheduler](../internal/engine/dependency_order_test.go), [scheduling integrations](../internal/engine/imports_test.go) | `go test ./internal/engine -run 'Import|ScheduleDependencies|Inheritance'`, `go test . -run 'ImportDefaults|FacadeImport'`, and `go test ./examples/consumers` |
|
||||
| Explicit main-file discovery and absent-input loading | [Candidate selection](../internal/engine/discovery_test.go), [load integration](../internal/engine/discovery_load_test.go), [public facade](../facade_discovery_test.go), and maintained [catalog](../examples/consumers/sequence_catalog_test.go) and [preset](../examples/consumers/discovery_test.go) examples | `go test ./internal/engine -run 'Discovery\|Search'`, `go test . -run 'Search'`, and `go test ./examples/consumers -run 'WorkingDirectoryPreset\|SequenceCatalog'` |
|
||||
| Import defaults, empty-selection evidence, and dependency-ready ordering | [Defaults and facade evidence](../facade_import_defaults_test.go), [engine import evidence](../internal/engine/import_evidence_test.go), [scheduler](../internal/engine/dependency_order_test.go), [scheduling integrations](../internal/engine/imports_test.go) | `go test ./internal/engine -run 'Import\|ScheduleDependencies\|Inheritance'`, `go test . -run 'ImportDefaults\|FacadeImport'`, and `go test ./examples/consumers` |
|
||||
| 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'` |
|
||||
| Sequence identity, declarations, and operations | [Identity](../internal/engine/sequence_identity_test.go), [registrations](../internal/engine/sequence_registration_test.go), [declarations](../internal/engine/sequence_declaration_test.go), [combination](../internal/engine/sequence_combine_test.go), [final facade validation](../facade_final_sequence_identity_test.go), and [consumer catalog](../examples/consumers/sequence_catalog_test.go) | `go test ./internal/engine -run 'Sequence\|Collection'`, `go test . -run 'SequenceIdentity\|SequenceRegistration'`, and `go test ./examples/consumers` |
|
||||
| Sibling inheritance, provenance, history, sensitivity | [Implementation flow](internal/overview.md#implementation-flow), [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'` |
|
||||
| Input and relative-import destinations, mounted sources, and source ownership | [Destination engine tests](../internal/engine/destination_test.go), [tier tests](../internal/engine/destination_tiers_test.go), [import traversal and version tests](../internal/engine/destination_import_test.go), [accounting tests](../internal/engine/destination_accounting_test.go), [lifecycle inspection tests](../internal/engine/destination_import_lifecycle_test.go), [public source/error tests](../facade_destination_test.go), [resource contexts](../facade_resource_context_test.go), and [consumer examples](../examples/consumers/destinations_test.go) | `go test ./internal/engine -run 'Destination|Import|Glob|Mounted'`, `go test . -run 'Destination|Import|SourceContext|FacadeMounted'`, and `go test ./examples/consumers` |
|
||||
| Input and relative-import destinations, mounted sources, and source ownership | [Destination engine tests](../internal/engine/destination_test.go), [tier tests](../internal/engine/destination_tiers_test.go), [import traversal and version tests](../internal/engine/destination_import_test.go), [accounting tests](../internal/engine/destination_accounting_test.go), [lifecycle inspection tests](../internal/engine/destination_import_lifecycle_test.go), [public source/error tests](../facade_destination_test.go), [resource contexts](../facade_resource_context_test.go), and [consumer examples](../examples/consumers/destinations_test.go) | `go test ./internal/engine -run 'Destination\|Import\|Glob\|Mounted'`, `go test . -run 'Destination\|Import\|SourceContext\|FacadeMounted'`, and `go test ./examples/consumers` |
|
||||
| Shared source-version modes and evidence | [Engine source-version tests](../internal/engine/source_version_test.go), [public facade tests](../facade_source_version_mode_test.go), and [consumer example tests](../examples/consumers/layered_test.go) | `go test ./internal/engine -run 'SourceVersion'`, `go test . -run 'SourceVersion'`, and `go test ./examples/consumers` |
|
||||
| Schema, defaults, projection, cloning | [Implementation flow](internal/overview.md#implementation-flow), [schema](../internal/engine/schema_test.go), [decoding](../internal/engine/decode_test.go), [projection and typed cloning](../internal/engine/projection_test.go), [deferred clone review](roadmap/future.md#4-clone-traversal-review) | `go test ./internal/engine -run 'Schema\|Decode\|Projection\|Clone\|Inline\|CompareDocument'` |
|
||||
| Schema, defaults, projection, cloning | [Implementation flow](internal/overview.md#implementation-flow), [schema](../internal/engine/schema_test.go), [decoding](../internal/engine/decode_test.go), [projection and typed cloning](../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'` |
|
||||
| Paths, metadata, provenance | [Paths](../internal/engine/path_test.go), [metadata](../internal/engine/metadata_test.go), [provenance](../internal/engine/provenance_test.go) | `go test ./internal/engine -run 'Path\|Metadata\|Provenance\|Queries'` |
|
||||
| Passive resource contexts | [Engine contexts](../internal/engine/resource_context_test.go), [public contexts](../facade_resource_context_test.go) | `go test ./internal/engine -run 'Resource'` and `go test . -run 'SourceContext'` |
|
||||
| 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'` |
|
||||
| Effective configuration diff | [Engine comparison and masking tests](../internal/engine/diff_test.go), [engine operation tests](../internal/engine/diff_operation_test.go), [root facade tests](../facade_diff_test.go), and [consumer examples](../examples/consumers/diff_test.go) | `go test ./internal/engine -run 'Diff|Explain|History|Secret|Independent'`, `go test . -run 'Diff|FacadeInspection|PublicFacadeBoundary|DependencyImportBoundary'`, `go test ./examples/consumers`, and `go test . -run '^ExampleDiff$'` |
|
||||
| Effective configuration diff | [Engine comparison and masking tests](../internal/engine/diff_test.go), [engine operation tests](../internal/engine/diff_operation_test.go), [root facade tests](../facade_diff_test.go), and [consumer examples](../examples/consumers/diff_test.go) | `go test ./internal/engine -run 'Diff\|Explain\|History\|Secret\|Independent'`, `go test . -run 'Diff\|FacadeInspection\|PublicFacadeBoundary\|DependencyImportBoundary'`, `go test ./examples/consumers`, and `go test . -run '^ExampleDiff$'` |
|
||||
| Consumer examples and migration fixtures | [Consumer tests](../examples/consumers/consumers_test.go), [native, layered, and ordinary relative-import examples](../examples/consumers/layered_test.go), [mounted collection examples](../examples/consumers/destinations_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` |
|
||||
| Identified-sequence scaling evidence | [Mixed-upsert benchmark](../internal/engine/sequence_combine_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkSequenceMixedUpsert$' -benchmem` |
|
||||
| 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'` |
|
||||
| Development check behavior | [Development checker test](../development_check_test.go) | `go test . -run '^TestDevelopmentCheckHandlesFilenamesAndStopsOnFailure$'` |
|
||||
| Release profile wiring | [Release profile test](../releasekit_profile_test.go) | `go test . -run '^TestReleaseKitProfile$'` |
|
||||
|
||||
## Fuzz validation
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ This note records implementation flow and private identities for maintainers.
|
||||
|
||||
Each source's root directive is parsed into presence-aware declarations and
|
||||
resolved against load-wide defaults. `acquisition.go` owns shared acquisition
|
||||
policy, load accounting, and per-tier accounting; `import_acquisition.go` owns
|
||||
reading candidate bytes, declaration traversal, and import admission. Candidate
|
||||
policy, load-wide accounting, and tier-scoped repetition/namespace state;
|
||||
`import_acquisition.go` owns reading candidate bytes, declaration traversal,
|
||||
and import admission. Candidate
|
||||
selection in `import_selection.go` preserves a normalized
|
||||
namespace path separately from canonical access identity: overlapping roots
|
||||
coalesce the same candidate spelling in that namespace, while aliases reached
|
||||
@@ -32,6 +33,26 @@ sandbox guarantee. `source_context.go` derives passive resource contexts from
|
||||
resolved sources for later caller-owned reads; those records do not retain live
|
||||
acquisition handles.
|
||||
|
||||
Host and native link bindings share one iterative link-frame evaluator. It
|
||||
tracks active links for cycle detection and reuses successfully completed link
|
||||
targets while resolving the remaining path components; each binding retains its
|
||||
own namespace and target-containment rules. Directory reads distinguish an
|
||||
initial-open failure from failures after opening: only an initial missing
|
||||
directory can be treated as optional, while enumeration and close failures
|
||||
remain selection errors, including errors from the standard `fs.ReadDir`
|
||||
fallback.
|
||||
|
||||
Glob selection keeps traversal coordination in `selectGlobImportCandidates`:
|
||||
it advances literal and patterned segments, reads directories, unions and sorts
|
||||
candidates, validates final targets, then applies exclusions and requiredness.
|
||||
For each patterned directory entry, `prepareGlobEntry` handles matching and
|
||||
hidden filtering, containment, target and link inspection, intermediate-directory
|
||||
eligibility, host/native access-path selection, and candidate identity and label.
|
||||
It returns a candidate, an ordinary skip, or a diagnostic without mutating the
|
||||
selector's candidate set or traversal state. Directory entry preparation remains
|
||||
separate because its matching, absence, recursion, and traversal-state rules
|
||||
differ.
|
||||
|
||||
Acquisition remains authored: declarations are visited in source order and each
|
||||
selected candidate set in namespace-path lexical order. This preserves source
|
||||
records, IDs, source-local version checks, and callback ordering. The acquired
|
||||
@@ -71,6 +92,12 @@ tuple comparisons. On a stalled graph, deterministic DFS over the residual graph
|
||||
returns a closed cycle rotated to its smallest key; blocked dependents are not
|
||||
reported as cycle members.
|
||||
|
||||
Import edge preparation builds an importer-to-target adjacency index once for
|
||||
an acquisition's outer and item-local scheduling, and once for grouped
|
||||
scheduling. Each scope gathers edges from its participating identities and
|
||||
retains only edges with both endpoints in its local slots. The retained
|
||||
inclusion graph still owns cycle diagnostics and source relationships.
|
||||
|
||||
Import composition schedules a main tier or one underlay group as a single
|
||||
strict-scope graph. Applicable collection declarations are gathered for the
|
||||
scope before peer composition, including identity inherited from lower tiers.
|
||||
|
||||
@@ -20,14 +20,19 @@ Implemented component owners:
|
||||
inheritance and decoding, before application resolution. The engine also owns
|
||||
shared source-version checking with sanitized declaration evidence.
|
||||
Explicit import discovery supports declared paths, directory searches, and
|
||||
globs; load-wide defaults and source-local empty-selection evidence cross the
|
||||
root facade without changing acquisition order. Import and inheritance
|
||||
resolution share dependency-ready scheduling within their composition scopes.
|
||||
globs; root-owned defaults are converted to the engine's single defaults input
|
||||
and snapshotted during load preflight before acquisition, while source-local
|
||||
empty-selection evidence crosses the facade without changing acquisition
|
||||
order. The root constructs placement directly on engine options and inputs
|
||||
using explicit enum conversion; public types and fields remain root-owned.
|
||||
Import and inheritance resolution share dependency-ready scheduling within
|
||||
their composition scopes.
|
||||
It retains each source's effective placement alongside physical source
|
||||
identity and owns schema
|
||||
inspection, lifecycle coordination, callback auditing,
|
||||
provenance/history, passive source-context retention, secret resolution and
|
||||
bootstrap, safe inspection rendering, and effective-value comparison over
|
||||
bootstrap, operation-local secret failure-location attribution, safe
|
||||
inspection rendering, and effective-value comparison over
|
||||
frozen inspection snapshots. It never imports the root package.
|
||||
- Root `Source`, `Filesystem`, `Input`, `Underlay`, `Path`, `Secret`, records,
|
||||
lifecycle callbacks, results, and bootstrap reports are root-defined wrappers
|
||||
@@ -90,20 +95,33 @@ bounded algorithms.
|
||||
|
||||
## Implementation flow
|
||||
|
||||
The root facade converts caller-owned values into engine-owned requests. The
|
||||
ordinary and grouped entry points share a normalized `loadRequest`: see
|
||||
The root facade converts caller-owned values into engine-owned requests. Its
|
||||
`ImportDefaults` conversion maps fields without taking ownership; engine load
|
||||
preflight snapshots the defaults once before source acquisition or callbacks.
|
||||
Ordinary and grouped loading then carry that read-only value through their
|
||||
normalized `loadRequest` and acquisition policy, while declaration resolution
|
||||
copies values needed by its independently owned effective policy. Placement is
|
||||
constructed directly on the engine's options and input records after explicit
|
||||
root-to-engine enum conversion. See
|
||||
[`lifecycle.go`](../../lifecycle.go), [`layered.go`](../../layered.go), and
|
||||
[`load_request.go`](../../internal/engine/load_request.go). Load-wide definition,
|
||||
version, default-import, and override policy stays distinct from each input's
|
||||
source, filesystem, destination, and import settings.
|
||||
|
||||
[`acquisition.go`](../../internal/engine/acquisition.go) defines acquisition
|
||||
policy and load-wide and per-tier accounting state. Each input is acquired under
|
||||
that policy; grouped requests retain acquired underlay scopes until their tier is
|
||||
composed in [`load_request.go`](../../internal/engine/load_request.go). Live
|
||||
policy, load-wide accounting, and tier-scoped repetition/namespace state. Each
|
||||
input is acquired under that policy. Source-root controls are extracted from
|
||||
each acquired tree before placement; assembled application data is not parsed
|
||||
again for controls. Grouped requests retain acquired underlay
|
||||
scopes until their tier is composed in
|
||||
[`load_request.go`](../../internal/engine/load_request.go). Completed tiers then
|
||||
share one assembly handoff that applies collection declarations and projects
|
||||
only that tier's source/version evidence; the caller keeps main-version
|
||||
protection and its projection explicit. Live
|
||||
filesystem access handles are confined to acquisition and closed there. Resolved
|
||||
filesystem facts used to authorize access are separate from passively retained
|
||||
resource contexts: see
|
||||
resource contexts. Non-import validation retains those facts on `assembledInput`
|
||||
for passive-context construction and ordinary-overlay handling: see
|
||||
[`filesystem_context.go`](../../internal/engine/filesystem_context.go) and
|
||||
[`source_context.go`](../../internal/engine/source_context.go).
|
||||
|
||||
@@ -135,10 +153,37 @@ descriptor caching and type policy, and
|
||||
inline field assembly. [`decode.go`](../../internal/engine/decode.go) consumes
|
||||
those descriptors for typed decoding.
|
||||
|
||||
History and inspection use one private evidence owner. The
|
||||
[`history_model.go`](../../internal/engine/history_model.go) builder receives
|
||||
baseline, source-composition, inheritance, typed-checkpoint, and CLI evidence
|
||||
in [`preparePostDecodeOverrideContext`](../../internal/engine/override_context.go),
|
||||
before application resolution begins. The callback retains its separate fixed
|
||||
entry view; it does not read a builder that continues to change. After successful
|
||||
resolution auditing, [`resolveLoadInput`](../../internal/engine/load_result.go)
|
||||
adds the original resolution records and their net replay groups. At the existing
|
||||
pre-secret admission boundary, [`prepareInspectionHistory`](../../internal/engine/load_result.go)
|
||||
freezes that builder and counts the retained document roots before secret lookup
|
||||
and validation.
|
||||
|
||||
The frozen model is shared by `Metadata` and `Inspection`. Its shape-aware
|
||||
selection in [`history_select.go`](../../internal/engine/history_select.go)
|
||||
drives metadata history, explanation, and historical-path membership; the model
|
||||
also supplies historical diagnostic locations and sensitivity/dependency
|
||||
evidence. `Inspection` replays selected payloads to produce detached, safely
|
||||
rendered values, while metadata queries return detached attribution records.
|
||||
History and explanation behavior is covered by focused `history_*_test.go`
|
||||
tests alongside metadata, secret, inheritance, and facade integration tests.
|
||||
|
||||
Behavioral ownership follows the same handoffs: imports and grouped lifetimes are
|
||||
covered by `imports_test.go`, `load_request_test.go`, and focused source,
|
||||
discovery, and destination tests; inheritance by the inheritance test files;
|
||||
reflection behavior by `schema_test.go`, `projection_test.go`, and
|
||||
`decode_test.go`. The [development test map](../development.md#task-to-test-map)
|
||||
links focused commands. Typed clone extraction remains deferred in
|
||||
[`future.md`](../roadmap/future.md#4-clone-traversal-review).
|
||||
links focused commands. [`clone.go`](../../internal/engine/clone.go) owns typed
|
||||
copying, using the shared descriptors and per-operation traversal helpers.
|
||||
|
||||
Each schema operation builds its private reflection descriptor graph with
|
||||
operation-local caching; there is no global mutable descriptor cache. Decoding,
|
||||
projection, and cloning reuse these descriptors so field bindings agree. The
|
||||
public reference describes the resulting type, output, and ownership behavior;
|
||||
this descriptor storage is an implementation detail.
|
||||
|
||||
@@ -207,7 +207,9 @@ records this ordering.
|
||||
CLI overrides are the highest-priority input to application resolution. Supplied
|
||||
sequences replace whole sequences, and mappings combine recursively. Inheritance
|
||||
and typed defaulting do not run again after CLI application. CLI paths and values
|
||||
must not introduce structural directives; earlier source, inheritance, and
|
||||
must not introduce inheritance controls; source-document controls are interpreted
|
||||
only at acquisition, before placement, and later application `config` data is
|
||||
not reinterpreted. Earlier source, inheritance, and
|
||||
decoding failures remain errors even if a CLI assignment would replace the value.
|
||||
Semantic validation assesses the final configuration after overrides and resolution.
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
ConfigKit's selected grant is LGPL-3.0-or-later, recorded in
|
||||
[COPYRIGHT](../../COPYRIGHT), with the complete LGPLv3 and incorporated GPLv3
|
||||
texts in [LICENSE](../../LICENSE). The [release procedure](../release.md)
|
||||
owns source-only Go module publication through Gitea. No separate hosted release
|
||||
object or license asset is required for this channel.
|
||||
owns source-only Go module publication and, beginning with v1.0.0, local delivery
|
||||
of a Gitea release page using the complete committed release note. That page
|
||||
retains the note's licensing footer; separate license assets are not required.
|
||||
|
||||
## Purpose And Scope
|
||||
|
||||
|
||||
@@ -23,9 +23,10 @@ policy and link to it rather than introduce additional release requirements.
|
||||
|
||||
ConfigKit publishes a source-only Go module. Its declared deliverables are the
|
||||
immutable annotated source tag and version-matched checked-in release note.
|
||||
Standalone binaries, container images, hosted release objects, and separate
|
||||
registry publication are not required. Additional channels require a deliberate
|
||||
update to the policy and procedure.
|
||||
Beginning with v1.0.0, maintainers also deliver a Gitea release page containing
|
||||
that complete note after source publication. Standalone binaries, container
|
||||
images, and separate registry publication are not required. Further channels
|
||||
require a deliberate update to the policy and procedure.
|
||||
|
||||
The Go API, documented YAML and resolution semantics, and structured diagnostic
|
||||
contracts participate in compatibility review. A signature-preserving change
|
||||
@@ -80,9 +81,13 @@ the tagged source. The procedure must identify those deliverables and the
|
||||
responsibility of their automation. Published deliverables must be traceable
|
||||
to that source and identify the same version.
|
||||
|
||||
ConfigKit currently declares no additional deliverables. Confirmed publication
|
||||
of the intended source tag is the completion point. CI, macOS validation,
|
||||
hosted delivery, and consumer retrieval are outside the release gates.
|
||||
ConfigKit's hosted release page is delivered locally through ReleaseKit after
|
||||
confirmed source publication, using the tagged note and preserving its licensing
|
||||
footer. Hosted delivery has a separately reported outcome and may be retried
|
||||
against the same immutable tag without republishing it. Confirmed publication
|
||||
of the intended source tag remains the source-release completion point. CI,
|
||||
macOS validation, hosted-service availability, delivery credentials, and consumer
|
||||
retrieval are outside the source-publication gates.
|
||||
|
||||
## Versioning And Compatibility
|
||||
|
||||
@@ -176,8 +181,8 @@ ConfigKit uses the complete checked-in note as the annotated tag message.
|
||||
Include the release footer defined by the [OSS policy](oss.md#discoverability)
|
||||
in each new release document. The release procedure defines the concrete URLs
|
||||
and publication mechanics. Prepare these links with the release note; they do
|
||||
not add a network check or approval gate. If a hosted download page is used in
|
||||
a future channel, preserve the footer there.
|
||||
not add a network check or approval gate. ConfigKit's hosted release page reuses
|
||||
the complete committed note and its footer.
|
||||
|
||||
Handle version selection, release-note content, and retirement of completed
|
||||
roadmaps during preparation. Preserve planned and deferred work and repair
|
||||
@@ -221,6 +226,11 @@ expanded platform matrices solely because they are possible. Retain such
|
||||
checks when their distinct protection justifies their cost. A one-time migration
|
||||
check must not silently become a permanent release requirement.
|
||||
|
||||
ConfigKit selects ReleaseKit's targeted forbidden-file check to keep the local
|
||||
delivery token path out of the committed candidate. The profile owns the path;
|
||||
the check does not inspect file contents or Git history. Hosted credential
|
||||
preflight belongs to delivery and adds no source-publication gate.
|
||||
|
||||
ReleaseKit runs its selected Go commands with readonly module mode and
|
||||
`GOPROXY=off`; acquire dependencies and build the tool before candidate
|
||||
validation. Formatting and whitespace remain ordinary development checks in
|
||||
|
||||
189
docs/release.md
189
docs/release.md
@@ -1,16 +1,25 @@
|
||||
# ConfigKit Release Procedure
|
||||
|
||||
This is the maintainer runbook for ConfigKit's [release policy](policy/release.md).
|
||||
ConfigKit publishes a source-only Go module. The immutable annotated tag and
|
||||
matching checked-in release note are the deliverables; there is no binary,
|
||||
container image, generated package, or separate registry publication.
|
||||
A release publishes the Go source under a new Git tag, with a matching release
|
||||
note committed in the repository. The tag is annotated: it includes the release
|
||||
note text and points to the released commit. Published tags and contents are
|
||||
immutable. There is no binary, container image, generated package, or separate
|
||||
registry publication. Beginning with v1.0.0, maintainers also deliver a Gitea
|
||||
release page containing the complete checked-in note after source publication.
|
||||
Source publication and hosted delivery have separate outcomes.
|
||||
|
||||
A **candidate** is the committed revision you intend to release. The
|
||||
**authoritative remote** is the repository where ConfigKit publishes releases;
|
||||
its exact push URL is listed below. The **consumer minimum** is the oldest Go
|
||||
version supported for applications using ConfigKit.
|
||||
|
||||
## Release identity and support
|
||||
|
||||
- Module: `gitea.maximumdirect.net/eric/ConfigKit`
|
||||
- Module: `go.maximumdirect.net/configkit`
|
||||
- Primary branch: `main`
|
||||
- Authoritative remote: `origin`
|
||||
- Authoritative push URL: `https://gitea.maximumdirect.net/eric/ConfigKit.git`
|
||||
- Authoritative push URL: `ssh://git@forge.maximumdirect.net:22222/base-layer/configkit.git`
|
||||
- Tags: stable Semantic Versions such as `v0.5.0`; no prereleases
|
||||
- Minimum Go version: the first stable patch of the selected Go series, or a
|
||||
higher patch when required by a dependency or specific correctness fix, as
|
||||
@@ -20,10 +29,15 @@ container image, generated package, or separate registry publication.
|
||||
- Best-effort targets: macOS amd64 and arm64, under the
|
||||
[platform support policy](policy/release.md#platform-support)
|
||||
|
||||
Release validation runs one offline race-enabled Go suite, one full vet pass,
|
||||
and CGO-disabled package compilation for both Linux targets. macOS and CI are
|
||||
Release validation runs the offline Go test suite once with the race detector,
|
||||
then a full vet pass and package compilation for both Linux targets with cgo
|
||||
disabled (`CGO_ENABLED=0`). macOS and CI are
|
||||
not release gates; do not wait for or poll CI.
|
||||
|
||||
The vanity module path is the name applications put in Go imports. Source/tag
|
||||
browser links and public Go retrieval use the Forge HTTPS repository
|
||||
independently of SSH publication.
|
||||
|
||||
## 1. Select Go and prepare the tool
|
||||
|
||||
Use a clean checkout with Git, Bash 3.2 or newer, and a native compiler for race
|
||||
@@ -33,10 +47,10 @@ For a patch release, retain the series recorded in `go.mod` and leave its consum
|
||||
minimum unchanged. Follow the
|
||||
[toolchain selection policy](policy/release.md#configkit-toolchain-selection).
|
||||
|
||||
Put the selected series' latest locally available compatible patch first on
|
||||
`PATH`, ensuring it meets `go.mod`'s minimum. The same `go` executable must build
|
||||
ReleaseKit and run its child validation commands. Set local toolchain selection
|
||||
and verify it:
|
||||
Choose the latest compatible patch installed locally in that Go series. Put
|
||||
its `go` executable first on `PATH` and ensure it meets the minimum in
|
||||
`go.mod`. The same `go` executable must build ReleaseKit and run its child
|
||||
validation commands. Set local toolchain selection and verify it:
|
||||
|
||||
```sh
|
||||
export GOTOOLCHAIN=local
|
||||
@@ -61,8 +75,17 @@ use that required version instead and document the reason. When the series is
|
||||
unchanged, leave the minimum unchanged unless a minor or major release
|
||||
deliberately changes it under the
|
||||
[consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain).
|
||||
For example, validating with Go 1.26.8 leaves `go 1.26.0` unchanged. There is no
|
||||
separate requirement to validate with Go 1.26.0.
|
||||
For example:
|
||||
|
||||
| Release situation | Go used for validation | Consumer minimum |
|
||||
| --- | --- | --- |
|
||||
| Patch release in the 1.26 series | Latest compatible patch installed locally, such as 1.26.8 | Keep the existing `go.mod` minimum unchanged. |
|
||||
| Minor/major release staying in 1.26 | Latest compatible patch installed locally | Normally retain the minimum; any deliberate change follows the linked policy. |
|
||||
| Minor/major release adopting a newer installed stable series | Latest compatible patch installed locally in that series | Normally declare that series' initial stable patch, or the higher patch required by a dependency or identified fix. |
|
||||
|
||||
Using Go 1.26.8 for validation can therefore leave `go 1.26.0` unchanged. Users
|
||||
need the declared minimum, while maintainers validate with their selected patch.
|
||||
There is no separate requirement to validate with Go 1.26.0.
|
||||
|
||||
Prepare dependencies whether or not the minimum changes:
|
||||
|
||||
@@ -70,28 +93,39 @@ Prepare dependencies whether or not the minimum changes:
|
||||
go mod download
|
||||
```
|
||||
|
||||
Build the tool selected by `go.mod` into temporary storage outside the checkout.
|
||||
Build the ReleaseKit version recorded in `go.mod` into temporary storage outside
|
||||
the checkout.
|
||||
Ensure `$TMPDIR` names a system temporary location, not the repository. These
|
||||
commands never install another ReleaseKit version:
|
||||
|
||||
```sh
|
||||
releasekit_tmp=$(mktemp -d "${TMPDIR:-/tmp}/configkit-releasekit.XXXXXX")
|
||||
releasekit_bin="$releasekit_tmp/releasekit"
|
||||
go build -o "$releasekit_bin" gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit
|
||||
go list -m gitea.maximumdirect.net/eric/ReleaseKit
|
||||
go build -o "$releasekit_bin" go.maximumdirect.net/releasekit/cmd/releasekit
|
||||
go list -m go.maximumdirect.net/releasekit
|
||||
"$releasekit_bin" version
|
||||
```
|
||||
|
||||
The module-selected executable is ReleaseKit v0.4.1. Use the built executable
|
||||
for publication to preserve its exit status and publication-state diagnostics.
|
||||
This repository selects ReleaseKit v0.6.0. Use the compiled program above for
|
||||
publication so its exit status and publication report reach you unchanged.
|
||||
`go run` maps a nonzero child exit status to 1. ReleaseKit's child Go commands
|
||||
use the `go` found on `PATH` with `GOTOOLCHAIN=local`; keep the selected
|
||||
executable first on `PATH` through the publication command.
|
||||
|
||||
For hosted delivery, save the Gitea API token in the ignored local file
|
||||
`.releasekit/gitea-token` with owner-only permissions. The profile uses it when
|
||||
`RELEASEKIT_GITEA_TOKEN` is unset or blank; a nonblank environment value takes
|
||||
precedence. The file path resolves from the repository root, and ReleaseKit
|
||||
trims surrounding whitespace from its contents. Keep credentials out of release
|
||||
notes, diagnostic labels, command arguments, and commits. The selected
|
||||
`forbidden_files` check also rejects a committed token at that exact path;
|
||||
it does not scan other files or history. Source publication uses Git
|
||||
authentication and does not read or require this token.
|
||||
|
||||
Verify access to the authoritative remote without displaying credentials:
|
||||
|
||||
```sh
|
||||
git ls-remote https://gitea.maximumdirect.net/eric/ConfigKit.git HEAD refs/heads/main
|
||||
git ls-remote ssh://git@forge.maximumdirect.net:22222/base-layer/configkit.git HEAD refs/heads/main
|
||||
```
|
||||
|
||||
Publication requires explicit authorization. Local implementation commits do
|
||||
@@ -99,10 +133,10 @@ not authorize a push or release.
|
||||
|
||||
## 2. Prepare and commit the candidate
|
||||
|
||||
Choose an unused stable version locally and on the authoritative remote. Compare
|
||||
the candidate with the previous tag and review exported Go signatures and
|
||||
ownership, YAML and resolution semantics, diagnostic contracts, minimum Go
|
||||
version, and platform support. Review maintained consumer examples and the
|
||||
Choose a stable version that has not been used locally or on the publication
|
||||
remote. Compare the candidate with the previous tag. Review public Go APIs and
|
||||
value ownership, YAML and resolution rules, diagnostic contracts, the minimum
|
||||
Go version, and platform support. Review maintained consumer examples and the
|
||||
migration guidance. Explain any consumer-minimum increase and required consumer
|
||||
action under the
|
||||
[consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain).
|
||||
@@ -111,7 +145,7 @@ Create `docs/releases/<version>.md`. Its first line must be exactly
|
||||
`# ConfigKit <version>`, and it must end with the license and attribution footer:
|
||||
|
||||
```markdown
|
||||
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/VERSION/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/VERSION/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/VERSION/docs/oss/attribution.md)
|
||||
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/configkit/src/tag/VERSION/COPYRIGHT) | [LICENSE](https://forge.maximumdirect.net/base-layer/configkit/src/tag/VERSION/LICENSE) | [ATTRIBUTION](https://forge.maximumdirect.net/base-layer/configkit/src/tag/VERSION/docs/oss/attribution.md)
|
||||
```
|
||||
|
||||
Replace `VERSION` with the full tag and confirm the three target files exist in
|
||||
@@ -127,7 +161,7 @@ Push only that committed candidate to the literal authoritative URL. This does
|
||||
not follow local tags:
|
||||
|
||||
```sh
|
||||
git push --no-follow-tags https://gitea.maximumdirect.net/eric/ConfigKit.git HEAD:refs/heads/main
|
||||
git push --no-follow-tags ssh://git@forge.maximumdirect.net:22222/base-layer/configkit.git HEAD:refs/heads/main
|
||||
```
|
||||
|
||||
ReleaseKit requires authoritative `main` to equal this candidate before it
|
||||
@@ -139,46 +173,114 @@ candidate to `main`, and invoke validation again.
|
||||
|
||||
## 4. Publish the committed candidate
|
||||
|
||||
With the authorized candidate already on authoritative `main`, invoke the
|
||||
module-selected native executable exactly once:
|
||||
Once publication is authorized and the candidate is on the publication remote's
|
||||
`main`, run the compiled ReleaseKit program once to validate and publish it:
|
||||
|
||||
```sh
|
||||
"$releasekit_bin" publish vMAJOR.MINOR.PATCH
|
||||
```
|
||||
|
||||
Replace the placeholder with the selected version. ReleaseKit checks the
|
||||
effective push URL against `expected_push_url`, verifies the candidate on
|
||||
push URL Git will actually use, after URL rewrites, against `expected_push_url`.
|
||||
It verifies the candidate on
|
||||
`main`, and checks the release-note H1 for the product and version. It then
|
||||
runs the selected race-enabled standard suite once with test-time vet disabled,
|
||||
one full vet pass, and CGO-disabled package compilation for Linux amd64 and
|
||||
arm64. It publishes only the annotated tag for that candidate. Confirmed
|
||||
publication of the intended tag completes the release. The tag points to the
|
||||
captured commit and uses the complete checked-in note as its annotation message.
|
||||
The optional `check`
|
||||
command is available for diagnosis but is not a separate required pass:
|
||||
Use the optional `check` command for diagnosis when useful. It validates without
|
||||
publishing and is not required before `publish`, which performs its own checks:
|
||||
|
||||
```sh
|
||||
"$releasekit_bin" check vMAJOR.MINOR.PATCH
|
||||
```
|
||||
|
||||
Status 0 confirms publication, status 1 reports a known publication failure or
|
||||
conflict, status 2 reports usage, profile, or version errors, and status 3 means
|
||||
the push outcome remains unknown. Preserve both the native exit status and its
|
||||
diagnostics.
|
||||
For `publish`, interpret the program's exit status as follows:
|
||||
|
||||
| Status | Meaning | Next action |
|
||||
| --- | --- | --- |
|
||||
| `0` | Publication succeeded. | The source release is complete. Continue with hosted delivery. |
|
||||
| `1` | A known publication failure or conflict was reported. | Read the report and correct the stated problem. Preserve any published tag. |
|
||||
| `2` | The command, profile, or version is invalid. | Correct the command or configuration before trying again. |
|
||||
| `3` | The tag push outcome is still unknown. | Follow the recovery procedure below before retrying or declaring success. |
|
||||
|
||||
Keep both the program's exit status and its diagnostic report. Using `go run`
|
||||
would lose the distinction between its nonzero exit statuses.
|
||||
|
||||
The command does not push or repair `main`, wait for CI, or publish binaries or
|
||||
a hosted release page. A successful intended-tag publication does not require
|
||||
remote read-back or consumer retrieval; those checks remain optional.
|
||||
|
||||
## 5. Recover from a failed or uncertain tag push
|
||||
## 5. Deliver the hosted release page
|
||||
|
||||
After source publication succeeds, run local hosted delivery from the repository
|
||||
root. Keep the worktree clean and `HEAD` at the published tag's commit. Reuse the
|
||||
compiled executable from step 1. For the first release using this workflow:
|
||||
|
||||
```sh
|
||||
"$releasekit_bin" deliver v1.0.0
|
||||
```
|
||||
|
||||
For subsequent releases, substitute the selected version. The profile selects
|
||||
Gitea at `https://forge.maximumdirect.net`, owner `base-layer`, repository
|
||||
`configkit`. With no build targets, delivery creates or accepts a stable,
|
||||
non-draft release page containing the complete committed note, including its
|
||||
licensing footer. It creates no binary assets, receipts, or checksum manifest.
|
||||
An existing page with matching content is accepted; conflicting content is not
|
||||
overwritten.
|
||||
|
||||
Delivery automatically performs an authenticated, read-only repository preflight
|
||||
before hosted writes. A separate preflight before source publication is not a
|
||||
release gate. Delivery does not rerun candidate validation, publish a tag, or
|
||||
wait for CI. Hosted-service availability and delivery credentials do not gate
|
||||
source publication. Report source-publication success and hosted-delivery status
|
||||
separately.
|
||||
|
||||
| Delivery status | Meaning and next action |
|
||||
| --- | --- |
|
||||
| `0` | Delivery is complete; report the hosted release URL. |
|
||||
| `1` | Preflight was unavailable, or delivery failed, conflicted, or made partial progress. Read the reported state and correct the cause; investigate conflicts before retrying. |
|
||||
| `2` | Usage, profile, credential loading, or preflight configuration/access failed. Correct the reported problem before retrying delivery. |
|
||||
| `3` | A hosted write has an unresolved outcome. Inspect the hosted release and resolve that state before retrying. |
|
||||
|
||||
A delivery problem leaves the successfully published source tag intact. Retry
|
||||
delivery with the same immutable version after correcting the cause and
|
||||
resolving any uncertain write. For a later retry, use a clean checkout of the
|
||||
original tag, fetching that tag if needed. Build the module-selected CLI from
|
||||
that checkout using step 1's tool-build commands, without changing the tagged
|
||||
module files, and make the token available there. For example, with the tag
|
||||
available locally:
|
||||
|
||||
```sh
|
||||
release_version=v1.0.0
|
||||
git switch --detach "$release_version"
|
||||
```
|
||||
|
||||
After preparing the tool in that tagged checkout:
|
||||
|
||||
```sh
|
||||
"$releasekit_bin" deliver "$release_version"
|
||||
```
|
||||
|
||||
Do not rerun `publish`, move a tag, or replace a hosted page to repair delivery.
|
||||
If published source or content needs correction, prepare a new appropriate
|
||||
version through the normal release procedure.
|
||||
|
||||
## 6. Recover from a failed or uncertain tag push
|
||||
|
||||
The executable reports the publication state, phase, version, candidate commit,
|
||||
tag-object ID, effective endpoint, and error on standard error. It performs one
|
||||
bounded authoritative reconciliation after a failed tag push. Exit status 3
|
||||
tag-object ID, effective endpoint, and error on standard error. After a failed
|
||||
tag push, it makes one bounded check of the exact publication remote to find out
|
||||
whether the tag arrived. Exit status 3
|
||||
means the tag outcome remains unknown; do not blindly retry or move a tag.
|
||||
|
||||
For an uncertain result, use the exact endpoint, candidate commit, version, and
|
||||
tag-object ID from the report. Copy the captured endpoint into the variable
|
||||
An annotated tag is a Git object containing the release note and a pointer to
|
||||
the source commit. Its tag-object ID identifies that complete object. Checking
|
||||
only the commit ID would not establish that the intended tag object arrived.
|
||||
|
||||
For an uncertain result, use the exact endpoint (remote URL), candidate commit,
|
||||
version, and tag-object ID from the report. Copy the captured endpoint into the variable
|
||||
below using normal shell quoting. Check that Git still resolves it to itself,
|
||||
then query the authoritative refs once:
|
||||
|
||||
@@ -195,7 +297,8 @@ cannot establish absence at the captured endpoint.
|
||||
|
||||
Compare the `main` object ID with the reported candidate commit and the tag
|
||||
object ID with the reported tag-object ID. `--refs` returns the annotated tag
|
||||
object itself, not merely its peeled commit. If the tag object matches, the
|
||||
object itself. A peeled commit is the source commit reached by following that
|
||||
tag; it is a different identity. If the tag object matches, the
|
||||
release is published. If the query fails, the result is conflicting, or the
|
||||
endpoint or candidate differs, stop and resolve the state before retrying. If
|
||||
the endpoint is reachable, the tag is confirmed absent, and `main` still equals
|
||||
@@ -214,7 +317,7 @@ back `main`. A conflicting published tag must never be moved or reused. A
|
||||
defect in published source requires a corrected commit, a new semantically
|
||||
appropriate version, and a matching release note.
|
||||
|
||||
## 6. Optional consumer verification
|
||||
## 7. Optional consumer verification
|
||||
|
||||
When useful or requested, read back the remote tag or retrieve the exact
|
||||
version into a new module without a local `replace` directive. These checks are
|
||||
@@ -228,8 +331,8 @@ consumer_dir=$(mktemp -d)
|
||||
(
|
||||
cd "$consumer_dir"
|
||||
go mod init example.invalid/configkit-release-check
|
||||
GOPRIVATE=gitea.maximumdirect.net GOPROXY=direct go get "gitea.maximumdirect.net/eric/ConfigKit@$release_version"
|
||||
go list -m -f '{{.Path}} {{.Version}}' gitea.maximumdirect.net/eric/ConfigKit
|
||||
GOWORK=off GOPROXY=direct go get "go.maximumdirect.net/configkit@$release_version"
|
||||
GOWORK=off go list -m -f '{{.Path}} {{.Version}}' go.maximumdirect.net/configkit
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
42
docs/releases/v0.10.1.md
Normal file
42
docs/releases/v0.10.1.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# ConfigKit v0.10.1
|
||||
|
||||
This patch release consolidates ConfigKit's internal loading machinery and
|
||||
reduces repeated import bookkeeping. Public APIs and configuration semantics
|
||||
remain unchanged from v0.10.0.
|
||||
|
||||
## Changes
|
||||
|
||||
- Consolidate ordinary and layered input acquisition, including import requests
|
||||
and selected-overlay handling, while retaining their distinct context and
|
||||
precedence rules. Remove unused private reverse conversions.
|
||||
- Separate import acquisition, scoped composition, and evidence projection;
|
||||
clarify inheritance preparation, combination, and finalization; and simplify
|
||||
reflective projection and schema-field assembly responsibilities.
|
||||
- Retain resolved filesystem facts from non-import validation for later passive
|
||||
resource contexts and ordinary overlays. Remove duplicate absent-host context
|
||||
derivation while preserving validation timing and handle cleanup.
|
||||
- Record empty optional-import selections directly against their declaring
|
||||
source, avoiding repeated searches through acquired sources. Index import
|
||||
dependencies for scope-local edge preparation instead of scanning the whole
|
||||
inclusion graph for every item scope. Stable dependency-ready ordering,
|
||||
provenance, and source destinations are preserved.
|
||||
- Expand regression coverage for import evidence ownership, scope boundaries,
|
||||
and broad main/underlay item imports. Improve
|
||||
[consumer loading guidance](../consumers/README.md) and internal documentation.
|
||||
|
||||
## Compatibility and upgrade
|
||||
|
||||
No consumer code or configuration migration is required. Composition, collection
|
||||
operations, inheritance, discovery, CLI precedence, source-version checks, and
|
||||
inspection retain their existing contracts.
|
||||
|
||||
Select this version and rebuild:
|
||||
|
||||
```sh
|
||||
go get gitea.maximumdirect.net/eric/ConfigKit@v0.10.1
|
||||
```
|
||||
|
||||
The consumer minimum remains Go 1.26.0. Dependencies and platform support are
|
||||
unchanged. Publication follows the existing [release procedure](../release.md).
|
||||
|
||||
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.1/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.1/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.1/docs/oss/attribution.md)
|
||||
70
docs/releases/v0.10.2.md
Normal file
70
docs/releases/v0.10.2.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# ConfigKit v0.10.2
|
||||
|
||||
This patch release fixes edge-case loading failures, resource cleanup, and limit
|
||||
accounting, and simplifies ConfigKit's internal loading machinery. Public APIs
|
||||
and documented configuration semantics remain unchanged from v0.10.1.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Terminate pointer-only recursive type traversal safely during decoding and
|
||||
schema-path resolution, preserving nullable values and supported recursive
|
||||
structures. Roll back failed dynamic descriptor construction so subsequent
|
||||
operations do not reuse incomplete state or panic.
|
||||
- Accept finite, non-cyclic slice views that share backing storage while
|
||||
retaining cycle detection and detached output.
|
||||
- Deactivate resolution, validation, and version-check contexts when callbacks
|
||||
panic, and close library-owned import roots during panic unwinding. The
|
||||
original application panic continues to propagate.
|
||||
- Report directory enumeration and close failures instead of treating them as
|
||||
optional absence, including failures reported as not-exist after a successful
|
||||
open. Genuinely missing optional directories retain their existing behavior.
|
||||
- Reuse completed symlink dependencies during host and native import discovery,
|
||||
avoiding exponential repeated expansion of finite link graphs while retaining
|
||||
cycle detection and containment checks.
|
||||
- Check existing projection budgets before bulk container allocation and account
|
||||
for the complete retained inspection-history graph, including replay and
|
||||
separately copied inheritance evidence.
|
||||
- Apply the documented append/upsert sequence requirements to non-sequence
|
||||
contributions at retained collection-policy destinations.
|
||||
|
||||
## Internal improvements
|
||||
|
||||
- Index collection-policy retirement and prepare sparse incoming policy changes
|
||||
instead of repeatedly scanning or copying unrelated retained policy.
|
||||
- Reuse successful dynamic-type eligibility checks within an operation and index
|
||||
tier-history lookups, compound identity restoration, and secret-failure source
|
||||
attribution to avoid repeated scans.
|
||||
- Remove unused snapshots, evidence, helpers, fields, and test-only production
|
||||
paths. Clarify acquisition handoffs and separate collection preparation,
|
||||
application, and finalization while sharing sequence-history construction.
|
||||
- Separate patterned glob entry preparation from traversal. Consolidate engine
|
||||
import defaults, snapshot them during load preflight, and construct placement
|
||||
directly across the facade while keeping public types root-owned.
|
||||
- Add regression coverage for the corrected failures, public historical paths,
|
||||
defaults snapshots, and resource ownership. Strengthen fuzz detachment checks
|
||||
and replace redundant or implementation-coupled assertions.
|
||||
- Update release tooling from ReleaseKit v0.4.1 to v0.4.2.
|
||||
|
||||
These changes remove redundant work and improve scaling in the identified paths;
|
||||
no end-to-end performance improvement has been quantified.
|
||||
|
||||
## Compatibility and upgrade
|
||||
|
||||
No consumer API or configuration-format migration is required. The
|
||||
[configuration contract](../config.md) remains authoritative.
|
||||
|
||||
Loads that previously succeeded despite directory acquisition failures, invalid
|
||||
append/upsert contributions, or incomplete retained-history accounting may now
|
||||
return the documented structured errors. These corrections enforce existing
|
||||
requirements and limits rather than introducing new ones.
|
||||
|
||||
Select this version and rebuild:
|
||||
|
||||
```sh
|
||||
go get gitea.maximumdirect.net/eric/ConfigKit@v0.10.2
|
||||
```
|
||||
|
||||
The consumer minimum remains Go 1.26.0. The YAML dependency and platform support
|
||||
are unchanged. Publication follows the existing [release procedure](../release.md).
|
||||
|
||||
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.2/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.2/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.2/docs/oss/attribution.md)
|
||||
144
docs/releases/v1.0.0.md
Normal file
144
docs/releases/v1.0.0.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# ConfigKit v1.0.0
|
||||
|
||||
ConfigKit's first stable release completes the public API cleanup, moves the Go
|
||||
module to `go.maximumdirect.net/configkit`, and scopes configuration controls to
|
||||
the root of each source document. It also corrects presence, history, and
|
||||
sensitivity reporting and adds direct source lookup for successful and failed
|
||||
loads.
|
||||
|
||||
This release includes intentional breaking changes from v0.10.2. The public Go
|
||||
API, documented configuration and resolution semantics, and structured diagnostic
|
||||
contracts now follow the [stable-release compatibility policy](../policy/release.md#versioning-and-compatibility).
|
||||
That policy includes the existing allowance for minor releases to raise the Go
|
||||
consumer minimum.
|
||||
|
||||
## Upgrade from v0.10.2
|
||||
|
||||
### Module path
|
||||
|
||||
Update the dependency requirement and Go imports from
|
||||
`gitea.maximumdirect.net/eric/ConfigKit` to `go.maximumdirect.net/configkit`,
|
||||
selecting v1.0.0. The package name remains `configkit`.
|
||||
|
||||
Go treats the two import paths as different packages with different named types.
|
||||
Coordinate the change with dependencies that exchange ConfigKit values with your
|
||||
application. Earlier tags retain their original module declarations; no old-path
|
||||
compatibility module is provided. See the
|
||||
[module migration guide](../consumers/README.md#vanity-module-path-migration).
|
||||
|
||||
### Source-document controls
|
||||
|
||||
Rename genuine source-root `configkit` control blocks to `config` in main files,
|
||||
imported fragments, underlays, selected overlays, and programmatic mapping inputs.
|
||||
The recognized controls remain `imports` and `sequences`, subject to their
|
||||
existing context-specific rules and import opt-in.
|
||||
|
||||
The control name is reserved only at the root of each acquired document. ConfigKit
|
||||
extracts controls before placing that document's payload. Nested `config` keys,
|
||||
including those in sequence records, are ordinary application data. Application-root
|
||||
`config` data produced by placement, defaults, CLI overrides, or resolution is
|
||||
also ordinary data and is not interpreted again as controls.
|
||||
|
||||
Review source-root application fields already named `config`: move, rename, or
|
||||
separately mount that data. The former names `configkit` and `_configkit` are now
|
||||
ordinary application keys at every depth, with no control aliases. A permissive
|
||||
schema can therefore accept an unmigrated control block as data without executing
|
||||
its imports. Stored source-local declaration paths change from `/configkit/...`
|
||||
to `/config/...`.
|
||||
|
||||
Source-version checks distinguish actual source-local controls from application
|
||||
paths with the same spelling. Existing inheritance rules remain in place. See
|
||||
[imports and control scope](../config.md#declaration-and-selection-rules) and
|
||||
[the directive migration guidance](../config.md#earlier-directive-spelling).
|
||||
|
||||
### Public Go API
|
||||
|
||||
The following names replace their pre-v1 counterparts outright, without
|
||||
compatibility aliases:
|
||||
|
||||
| Previous API | v1.0.0 API |
|
||||
| --- | --- |
|
||||
| `Resolution.InputInfo(path)` | `Resolution.EntryInfo(path)` |
|
||||
| `Definition.Secret` | `Definition.Sensitive` |
|
||||
| `SourceVersionModeDefault` | `SourceVersionModeMainOnly` |
|
||||
| `SourceVersionCheck` | `SourceVersionEvidence` |
|
||||
| `SourceInfo.VersionCheck` | `SourceInfo.VersionEvidence` |
|
||||
| `ImportSelection` | `EmptyImportSelection` |
|
||||
| `SourceInfo.ImportSelections` | `SourceInfo.EmptyImportSelections` |
|
||||
| `SourceInfo.DestinationUnresolved` | `SourceInfo.DestinationIsFallback` |
|
||||
| `ImportDefaults.Follow` / `.Hidden` | `FollowSymlinks` / `IncludeHidden` |
|
||||
| `Paths(paths...)` | `SearchPaths(paths...)` |
|
||||
| `LoadSecretsDir(directory)` | `LoadSecretsIntoEnvironment(directory)` |
|
||||
| `LayerBase` | `LayerMain` |
|
||||
| `CodeExplainLimitExceeded` | `CodeHistoryLimitExceeded` |
|
||||
| `Resolution.Record(path, ChangeDerivedDefault, reason, deps...)` | `Resolution.RecordDerivedDefault(path, reason, deps...)` |
|
||||
| `Resolution.Record(path, ChangeNormalization, reason, deps...)` | `Resolution.RecordNormalization(path, reason, deps...)` |
|
||||
|
||||
The recording methods describe mutations the application has already performed;
|
||||
they do not assign values. `Default` and `Normalize` remain the helpers that both
|
||||
assign and record. `EntryInfo` names the fixed resolution-entry metadata view,
|
||||
including defaults and CLI assignments. Version evidence records what was
|
||||
observed, not proof of checker execution or acceptance.
|
||||
|
||||
The redundant `ImportSelection.Outcome`, `ImportSelectionOutcome`, and
|
||||
`ImportSelectionNone` are removed. Empty-selection records retain their selector,
|
||||
path, and location. The obsolete diagnostic constants `CodeResolveInvalidKind`
|
||||
and `CodeOptionsUnavailable` are also removed without replacements.
|
||||
|
||||
Update stored or compared strings as well as Go symbols:
|
||||
|
||||
- The main provenance layer changes from `"base"` to `"main"`.
|
||||
- The retained-history limit diagnostic changes from
|
||||
`"explain.limit_exceeded"` to `"history.limit_exceeded"`.
|
||||
|
||||
The YAML import options `follow` and `hidden`, the `configkit:"secret"` Go tag,
|
||||
and typed `Secret` references retain their existing spelling and behavior. See
|
||||
the [API migration guide](../consumers/README.md#pre-v1-public-api-cleanup) for
|
||||
the consolidated mapping and usage guidance. Rebuild and run your application's
|
||||
configuration tests after migrating its imports, API calls, and source controls.
|
||||
|
||||
## Source inspection
|
||||
|
||||
`Result.Source(id)` and `LoadError.Source(id)` return a detached `SourceInfo`
|
||||
record and a found flag for an already acquired source. They complement
|
||||
`Sources()` and the existing callback-scoped `Resolution.Source(id)`. Empty or
|
||||
unknown IDs return false. Lookup does not reopen inputs or resolve credentials;
|
||||
failed loads retain the same partial, conservatively redacted evidence as their
|
||||
source inventory. See [results and failures](../config.md#results-and-failures).
|
||||
|
||||
## Correctness and internal improvements
|
||||
|
||||
- Resolution presence queries distinguish numeric mapping keys from sequence
|
||||
indexes using the configuration's entry shape. A later shape change no longer
|
||||
transfers input explicitness to a different semantic path.
|
||||
- Metadata history and value explanations now use one shared internal history
|
||||
model. This corrects attribution across shape changes, preserves container
|
||||
origins, and avoids duplicate intermediate source-history records.
|
||||
- Final dynamically typed secret fields retain their sensitivity classification
|
||||
in inspection output. Parsed dependency paths are deduplicated without losing
|
||||
distinctions needed for sensitivity propagation.
|
||||
- Composition-conflict diagnostics preserve claimant order through structured
|
||||
reporting and rendering, consistent with scoped assembly order.
|
||||
- History selection and explanation use indexes and reuse prepared selections
|
||||
and sensitivity dependencies to reduce repeated traversal. Retained-history
|
||||
accounting follows the shared model, and secret resolution and validation share
|
||||
traversal machinery.
|
||||
|
||||
These changes preserve the documented loading and inspection rules apart from
|
||||
the explicit migrations above. No end-to-end performance improvement is claimed.
|
||||
The [configuration contract](../config.md) and public Go declaration comments
|
||||
remain the behavior references.
|
||||
|
||||
## Requirements and release delivery
|
||||
|
||||
The consumer minimum rises from Go 1.26.0 to Go 1.27.0. Consumers must upgrade
|
||||
their Go toolchain before adopting this release. The YAML dependency is unchanged.
|
||||
Linux amd64 and arm64 remain the release-validated targets; macOS support remains
|
||||
best-effort under the [platform support policy](../policy/release.md#platform-support).
|
||||
|
||||
Release tooling moves to `go.maximumdirect.net/releasekit` v0.6.0. Beginning with
|
||||
this release, the source tag and checked-in note are accompanied by a Gitea release
|
||||
page containing the complete note and licensing footer. Source publication and
|
||||
hosted delivery remain separate outcomes under the [release procedure](../release.md).
|
||||
|
||||
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.0.0/COPYRIGHT) | [LICENSE](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.0.0/LICENSE) | [ATTRIBUTION](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.0.0/docs/oss/attribution.md)
|
||||
@@ -1,481 +0,0 @@
|
||||
# ConfigKit Consolidation Toward 1.0
|
||||
|
||||
Status: selected target state; implementation pending. This document defines the
|
||||
refactor's scope and rationale; it does not authorize a release. The scope
|
||||
questions are settled. The [implementation plan](implementation.md) owns the
|
||||
concrete steps, sequencing, and implementation progress.
|
||||
|
||||
## Purpose
|
||||
|
||||
Consolidate the machinery introduced by recent import, collection, inheritance,
|
||||
and discovery work so ConfigKit is easier to understand, maintain, and adopt
|
||||
before its public contracts become stable. Success means fewer independently
|
||||
maintained paths through the same behavior, clearer state ownership, and concise
|
||||
consumer workflows. Reducing a function's line count is useful only when it
|
||||
serves those outcomes.
|
||||
|
||||
The review considered public API consolidation as well as internal extraction.
|
||||
The settled public direction retains the existing simple and layered loading APIs,
|
||||
consolidates their private translation and acquisition machinery, and removes
|
||||
unused reverse inspection conversions. No public signature or configuration
|
||||
semantic change is planned in this effort. This is an assessment of the reviewed
|
||||
consumer workflows, not a requirement to preserve every beta API in perpetuity.
|
||||
|
||||
## Evidence And Baseline
|
||||
|
||||
Reviewed worktree: `c9574ef`, immediately after the `v0.10.0` release at `5959537`.
|
||||
This document replaces the earlier `complexity.md` roadmap and `remediation.md`
|
||||
audit record. Their recommendations and relevant evidence are incorporated here;
|
||||
the original documents remain available in Git history:
|
||||
|
||||
```sh
|
||||
git show fbaaaf1:docs/roadmap/complexity.md
|
||||
git show fbaaaf1:docs/roadmap/remediation.md
|
||||
```
|
||||
|
||||
The earlier roadmap reviewed `7a83704f43ef9870db33661231cdff5d6ee502f7`, before
|
||||
discovery. The audit reviewed `5959537d1adc2516e7f7e8881df72b707ec48c3f`, including
|
||||
implemented discovery. The consolidation review checked their recommendations
|
||||
against that newer baseline and also considered the public API.
|
||||
|
||||
This review inspected the current public load/input/discovery declarations and
|
||||
maintained consumer examples, the shared request executor and acquisition flow,
|
||||
import parsing and selection, inheritance execution, reflective projection and
|
||||
cloning, schema field construction, and filesystem-context derivation. Graph
|
||||
caller traces and text-reference checks confirmed the unused reverse conversion
|
||||
chain. This was source review, not a new runtime correctness or performance audit.
|
||||
Neither this review nor the audit established a runtime correctness defect.
|
||||
|
||||
### Audit Measurements
|
||||
|
||||
The audit refreshed the codebase-memory graph and used function size, cognitive
|
||||
complexity, branch counts, and parameter counts to rank candidates for source
|
||||
inspection. It indexed 901 non-test functions/methods and 928 test-file
|
||||
declarations. Its fast index excludes examples, documentation, and scripts; a
|
||||
tracked-file inventory covered those separately. Standalone `gocyclo`, `gocognit`,
|
||||
`scc`, and `cloc` tools were unavailable.
|
||||
|
||||
These are the graph tool's reported measurements at the audited revision.
|
||||
Function spans include comments and blank lines. The scores support relative
|
||||
triage; they are not standardized acceptance thresholds or evidence of a defect.
|
||||
|
||||
| Function | Source | Lines | Cognitive score |
|
||||
| --- | --- | ---: | ---: |
|
||||
| `valueOperation.project` | [projection.go](../../internal/engine/projection.go) | 195 | 174 |
|
||||
| `parseImportDeclarationSpec` | [import_spec.go](../../internal/engine/import_spec.go) | 127 | 129 |
|
||||
| `valueOperation.clone` | [clone.go](../../internal/engine/clone.go) | 130 | 104 |
|
||||
| `schemaBuilder.describeFields` | [schema.go](../../internal/engine/schema.go) | 99 | 100 |
|
||||
| `assembleBaseInputTrackedForRole` | [load_input.go](../../internal/engine/load_input.go) | 188 | 96 |
|
||||
| `selectDirectoryImportCandidates` | [import_selection.go](../../internal/engine/import_selection.go) | 154 | 96 |
|
||||
| `valueBuilder.build` | [value.go](../../internal/engine/value.go) | 104 | 93 |
|
||||
| `decodeValue` | [decode.go](../../internal/engine/decode.go) | 115 | 85 |
|
||||
| `applyInheritanceScopeTemplates` | [inheritance_schedule.go](../../internal/engine/inheritance_schedule.go) | 219 | 75 |
|
||||
| `contextsForAcquiredSources` | [source_context.go](../../internal/engine/source_context.go) | 102 | 72 |
|
||||
|
||||
At that revision, the engine contained 88 production Go files and approximately
|
||||
20,144 physical lines; the root facade contained 16 files and 1,861 lines.
|
||||
`imports.go` was the largest production file at 1,195 lines. These measurements
|
||||
locate concentrated responsibilities. They do not establish a need for more
|
||||
packages or make the smaller document/YAML visitors and explicit facade
|
||||
conversions maintenance defects.
|
||||
|
||||
### Findings And Recommended Treatment
|
||||
|
||||
The finding numbers below preserve the audit's identifiers. The selected work
|
||||
remains unimplemented; the cloning portion of finding 3 is deferred to future
|
||||
work. Retiring the original documents does not mark their findings implemented.
|
||||
|
||||
| Audit finding | Maintenance priority | Assessment and recommended treatment |
|
||||
| --- | --- | --- |
|
||||
| 1. Import entry points and responsibilities | Higher | Main refactor. Include shared acquisition normalization, import-default ownership, declaration parsing, and directory admission. |
|
||||
| 2. Shared inheritance executor | Higher | Main refactor. Expose meaningful phases and explicit combination results while retaining one executor. |
|
||||
| 3. Projection and cloning | Higher for projection; cloning deferred | Main refactor for projection. Clone traversal review is tracked separately in [future.md](future.md#4-clone-traversal-review). |
|
||||
| 4. Schema field discovery and inline assembly | Medium | Include a focused refactor coordinated with projection. Keep one schema builder and its two descriptor views. |
|
||||
| 5. Resolved filesystem context | Medium | Include alongside acquisition work. Reuse resolved facts without merging resource lifetimes or changing validation timing. |
|
||||
| 6. Unused reverse inspection conversions | Small, independently reviewable cleanup | Include in the root facade and engine. Retain useful forward-conversion and detached-result coverage. |
|
||||
|
||||
Findings 1–3 confirm the earlier roadmap's main opportunities; findings 4–6 add
|
||||
focused work. The following sections own their rationale and intended changes.
|
||||
Implementation and validation will establish completion of the selected work;
|
||||
the deferred clone review retains its separate disposition.
|
||||
|
||||
Completed correctness and algorithmic remediations remain part of the baseline,
|
||||
including sequence records preserved at `ea46001:docs/roadmap/remediation.md`
|
||||
and discovery records preserved at `0369111:docs/roadmap/remediation.md`.
|
||||
Item-scope application matching and inheritance policy localization are examples
|
||||
of completed corrections that overlap this work and whose regressions should
|
||||
remain protected.
|
||||
|
||||
The [architecture policy](../policy/architecture.md),
|
||||
[configuration contract](../config.md), and exported declaration comments own
|
||||
existing constraints and behavior. The
|
||||
[component overview](../internal/overview.md) and
|
||||
[import internals](../internal/imports.md) locate implementation responsibilities.
|
||||
|
||||
## Consumer-Facing Target
|
||||
|
||||
### Keep Simple Loading Simple And Advanced Inputs Explicit
|
||||
|
||||
The current APIs already provide progressive setup:
|
||||
|
||||
- A simple consumer calls `Load[Config](File(path))`.
|
||||
- A consumer with defaults, imports, or resolution supplies `Options[Config]`.
|
||||
- Explicit discovery produces an ordinary `Source`, so it uses the same loading
|
||||
call and lifecycle.
|
||||
- A layered consumer supplies an `Input` and `LayeredOptions[Config]`, with
|
||||
`Underlay.Inputs` distinguishing strict peers from precedence tiers.
|
||||
|
||||
The two loading functions already converge on `loadRequest` and
|
||||
`executeLoadRequest`. Their public differences have meaning: ordinary options
|
||||
include the one input's controls; layered loading has per-input controls and a
|
||||
separate overlay filesystem. Ordinary overlay context follows its selected
|
||||
source, whereas layered overlay context is independently specified. These are
|
||||
observable conventions, not interchangeable forwarding spellings.
|
||||
|
||||
**Decision (OQ-1, settled):** retain `Load`, `LoadLayered`, `Options`, `LayeredOptions`,
|
||||
`Input`, and `Underlay`. Remove repeated translation and option reconstruction
|
||||
behind them. Keep common application rules in `Definition`; do not require
|
||||
applications to build a runtime loader or manually coordinate phases.
|
||||
|
||||
A single public request type could place main input, underlays, and common
|
||||
options together. It would reduce duplicated option fields and make promotion
|
||||
from ordinary to layered loading more uniform. A convenience entry could preserve
|
||||
the simplest call. Such a design would still need to resolve the existing
|
||||
overlay-context differences and demonstrate a clearer consumer model. The
|
||||
maintained examples do not show enough consumer benefit to justify that redesign
|
||||
in this effort. Retaining the current APIs accepts some duplicated public option
|
||||
fields and setup changes when adopting underlays; internal consolidation and
|
||||
clearer guidance address the observed maintenance needs. The decision is based on
|
||||
the resulting API, rather than the amount of refactoring needed to achieve it.
|
||||
|
||||
### Preserve Useful Convenience And Ownership Boundaries
|
||||
|
||||
| Surface reviewed | Recommended public end state |
|
||||
| --- | --- |
|
||||
| `Source`, `Filesystem`, `Search`, filename groups and presets | Retain typed, reusable source/search descriptions. Discovery selects a concrete source during loading; applications do not loop over complete loads. |
|
||||
| `FilesystemContext` | Retain the public description and current host/native rules. Consolidate its resolved private representation; do not expose acquisition handles or engine context records. |
|
||||
| `SequenceIdentityAt` and `SequenceIdentityMatching` | Retain the exact-path convenience and dynamic selector form over the common registration model. Both serve distinct consumer shapes. |
|
||||
| `Definition`, resolution and validation callbacks | Retain application-owned domain rules inside one recorded lifecycle. Preserve the distinction between mutable recorded resolution and read-only validation. |
|
||||
| Results, metadata, explanations, source context and diff | Retain detached, safe inspection with consumer-owned presentation. Remove unused inverse conversions without removing supported queries. |
|
||||
| Root wrappers and private conversions | Retain root-owned public types and explicit ownership conversion under ADR 0006. Consolidate repeated real conversions and delete unused ones. |
|
||||
|
||||
The consumer-facing work is to make the ordinary path easier to find and show
|
||||
how additional features compose, using the existing maintained examples. Update
|
||||
the adoption guide with a concise choice between simple loading, discovery, and
|
||||
layered loading, and show which options belong to an input versus a whole load.
|
||||
Keep exact behavior in its canonical owners. Examples should make application
|
||||
rules recognizable and avoid requiring consumers to recreate generic merging,
|
||||
presence, or provenance machinery.
|
||||
|
||||
Use the current simple README call, bundled-file and layered catalog examples,
|
||||
the import-focused prompt catalog, and the discovered sequence catalog to assess
|
||||
the end state. The latter exercises imports, underlays, independent overlay
|
||||
context, CLI assignment, and recorded resolution in one workflow. A high number
|
||||
of options in that example does not by itself justify a new abstraction.
|
||||
|
||||
## Internal Target
|
||||
|
||||
Keep the root facade, cohesive `internal/engine`, ordered document model, and
|
||||
private YAML adapter. Use ordinary private records, functions, and focused files
|
||||
to express responsibilities. Existing mapping combination, sequence combination,
|
||||
dependency scheduling, schema descriptors, and traversal accounting remain shared.
|
||||
|
||||
`executeLoadRequest` stays the visible lifecycle coordinator. It should show
|
||||
acquisition, assembly, inheritance, decoding/CLI assignment, resolution, secret
|
||||
processing, validation, and publication in their established order. Extract a
|
||||
phase only when it gives that work a clear input, output, or ownership boundary.
|
||||
Do not replace the visible lifecycle with a registry of configurable stages.
|
||||
|
||||
### 1. Normalize Loading Inputs And Consolidate Import Acquisition
|
||||
|
||||
The current executor constructs an engine `Options[T]` from `loadRequest`, and
|
||||
underlay acquisition constructs `Options[struct{}]` to call the same input
|
||||
assembler. `assembleBaseInputTrackedForRole` then combines discovery, directive
|
||||
processing, version rules, imports, placement, accounting, and contexts. Import
|
||||
entry points carry long positional lists through several forwarding variants.
|
||||
This repeats the representation of information already normalized for a load.
|
||||
|
||||
In [imports.go](../../internal/engine/imports.go), the
|
||||
`loadImportedDocumentsTrackedWithVersion`,
|
||||
`loadImportedDocumentsTrackedWithProcessing`, and
|
||||
`loadImportedDocumentsTrackedWithProcessingDeferred` variants have corresponding
|
||||
`loadImportedWithAccess...` variants. Both final signatures carry 13 parameters.
|
||||
Placement, processing results, sequence policy, accounting, repetition tracking,
|
||||
namespace identity, and deferred assembly must stay aligned through the chain.
|
||||
The same file's `importTraversal.visitPath` combines source-admission decisions
|
||||
in 174 lines, alongside acquisition, pending-item assembly, and evidence work.
|
||||
|
||||
Introduce cohesive private acquisition inputs and explicit state ownership,
|
||||
reusing `loadRequest`, `loadInput`, `acquisitionScope`, and `importTraversal`
|
||||
where they fit. Translate ordinary and layered public declarations into the
|
||||
shared loading model once. Acquisition should receive the source, input role,
|
||||
placement, context, and policies it needs without a fabricated application type
|
||||
or a reconstructed broad options object. Small adapters may preserve meaningful
|
||||
entry operations while callers migrate; remove obsolete forwarding variants
|
||||
within the completed refactor.
|
||||
|
||||
Separate immutable policy, mutable processing state, and results. Move import
|
||||
defaults out of `sourceVersionState` and into the load/acquisition policy owner.
|
||||
Version state continues to own version processing, projected effective version
|
||||
location, evidence, and diagnostics. Local and effective version rules have
|
||||
different meanings and must remain distinguishable.
|
||||
|
||||
The state boundaries are especially important:
|
||||
|
||||
| Lifetime or scope | State and responsibility |
|
||||
| --- | --- |
|
||||
| Whole load | Aggregate resource accounting, application definition/registrations, import defaults, shared version evidence and diagnostics. |
|
||||
| One strict tier | Repetition tracking shared by that tier's inputs, namespace scheduling anchors, and its inclusion/composition graph relationship. A later tier may reuse a file. |
|
||||
| Active import traversal | Active routes and inclusion edges, filesystem access ownership, and cycle detection, including edges crossing item-scope boundaries. |
|
||||
| One composition scope | Peer contributions, pending collection declarations, item-local helpers, and deferred item placement. Nested scopes do not become separate precedence tiers. |
|
||||
| Retained result | Source records, effective destinations, origins, history, and passive resource-context descriptions. |
|
||||
|
||||
Keep repetition tracking tier-scoped. In the current code,
|
||||
`acquireUnderlayGroupScopes` creates a fresh visited map per group. Preserve valid
|
||||
reuse across tiers while keeping shared aggregate limits. Namespace equality,
|
||||
candidate spelling, and stable scheduling keys also serve different purposes.
|
||||
|
||||
Give acquisition, already-acquired scope assembly, and evidence projection
|
||||
focused homes. Keep discovery at the acquisition boundary: resolve a search once
|
||||
per input consumption, then process the concrete file or absence. Preserve
|
||||
source IDs and source-local version processing in acquisition order, even when
|
||||
composition uses a different dependency-ready order. The layered main is acquired
|
||||
before underlays; lower-tier identity can still inform its later strict assembly.
|
||||
|
||||
Within `parseImportDeclarationSpec`, extract boolean/collection/placement field
|
||||
handling and selector-value parsing where it clarifies responsibilities. Keep
|
||||
field presence, authored error locations, selector applicability, and the order
|
||||
of consequential validation visible. Within `selectDirectoryImportCandidates` and
|
||||
`importTraversal.visitPath`, separate admission and descent decisions from
|
||||
traversal orchestration where they represent distinct work. Retain the existing
|
||||
root deduplication, candidate union, exclusions, sorting, and active-path cycle
|
||||
rules. No shared abstraction should conflate literal first-match main discovery
|
||||
with multi-file import selection.
|
||||
|
||||
### 2. Share Resolved Filesystem Facts Without Sharing Live Handles
|
||||
|
||||
`importFilesystem`, `nativeFilesystemAccess`, and `contextsForAcquiredSources`
|
||||
derive overlapping root/base/namespace facts. The latter reconstructs context
|
||||
from combinations of source kind and optional fields, including absent input.
|
||||
Discovery and independent layered overlays make these combinations consequential.
|
||||
Context construction is used by ordinary acquisition and both selected-overlay
|
||||
paths, so those callers need to use the same applicable derivation rules.
|
||||
|
||||
Use a small engine-owned resolved-context description for the facts actually
|
||||
needed by acquisition and passive views: namespace identity/kind, resolved root,
|
||||
source base, and selected source path when present. Preserve the distinction
|
||||
between host paths and native slash paths. Reuse already resolved facts where
|
||||
available instead of repeatedly deriving them from the original options.
|
||||
|
||||
Resolution and validation can occur at different existing lifecycle points.
|
||||
The record must not force every source to perform all filesystem checks early.
|
||||
Context descriptions, library-owned acquisition handles, and returned passive
|
||||
resource views have different lifetimes. Closing acquisition must remain
|
||||
explicit on success and failure; retaining context must not retain an open root
|
||||
handle or assume ownership of a borrowed `fs.FS`.
|
||||
|
||||
Cover selected and absent discovery outcomes, root-only absent host context,
|
||||
native sources, in-memory inputs with explicit context, and independently rooted
|
||||
overlays. Passive context remains associated with the source that contributed
|
||||
the value, including retained underlay and inherited values. Use the acquired
|
||||
source's canonical identity where available. Preserve containment failures,
|
||||
source associations, deferred search bases, and terminal selected-file errors.
|
||||
|
||||
Acquisition and context handling use the same private handoff rather than
|
||||
maintaining parallel models for overlapping callers. The description does not
|
||||
replace the public `FilesystemContext` or `ResourceContext` contracts.
|
||||
|
||||
### 3. Make Inheritance Combination Results Explicit
|
||||
|
||||
Keep the mapping and identified-record adapters and one shared executor.
|
||||
`applyInheritanceScopeTemplates` currently prepares templates, mutates values
|
||||
through merge callbacks, projects policy and origins, records replacements and
|
||||
replay evidence, and enforces budgets in one body. The callbacks mutate several
|
||||
surrounding result collections, making a combination difficult to review alone.
|
||||
|
||||
Give the executor named operations for child/control preparation, parent payload
|
||||
and policy preparation, combination, evidence construction, and retained-node
|
||||
accounting. Use a small private result for outputs of one combination where it
|
||||
replaces unrelated surrounding mutations. Value, policy, origin, replacement,
|
||||
and sequence-position evidence should come from the same combination outcome;
|
||||
do not rerun a merge to reconstruct its history.
|
||||
|
||||
Keep borrowed authored templates separate from owned resolved payloads and from
|
||||
retained history snapshots. Parent identity exclusion and child identity/origin
|
||||
preservation must apply consistently to values and history. Preserve nested
|
||||
unresolved instructions for destination-local evaluation, policy retirement on
|
||||
actual replacement, cancellation, and source-version movement through sequences.
|
||||
|
||||
The shared Kahn scheduler with stable keys remains unchanged. Resolve enclosing
|
||||
relationships before nested relationships and retain assembled record order.
|
||||
Preserve expansion limits and retained-history accounting for shared nodes;
|
||||
extraction must not charge shared evidence twice or make an unbounded snapshot.
|
||||
|
||||
### 4. Separate Projection Handlers And Clarify Schema Assembly
|
||||
|
||||
Keep `projectConfiguration` as the schema-checked entry and `valueOperation` as
|
||||
the operation-state owner. Move substantial struct, map, and sequence projection
|
||||
into focused handlers. Keep pointer/dynamic dispatch and scalar representation
|
||||
choices visible. All handlers share the same cycle, depth, and node state.
|
||||
|
||||
Struct projection needs particular care: physical members drive traversal,
|
||||
flattened fields drive name lookup and sensitivity, and an inline container is
|
||||
not retained as an extra output node. Preserve declaration order, deterministic
|
||||
map order, nil/empty distinctions, collision diagnostics, codec failures, and
|
||||
secret classification. Scalar projection retains the existing number, duration,
|
||||
text-codec, and secret-reference representations. Keep cycle entries active
|
||||
through child traversal and balance their release on every exit.
|
||||
|
||||
In `schemaBuilder.describeFields`, separate field discovery and tag parsing from
|
||||
inline struct assembly, inline map assembly, and ordinary field registration.
|
||||
Retain one builder/cache and its recursion detection. Make the paired updates to
|
||||
`members`, flattened `fields`, `fieldByName`, and `inlineMap` easy to follow.
|
||||
Keep index-path construction and inherited secret flags intact. These descriptor
|
||||
views are both needed; combining them into one collection would shift complexity
|
||||
into decoding and projection.
|
||||
Keep pointer-chain and recursive-inline decisions explicit, preserve supported
|
||||
field/tag forms and ambiguity paths, and retain cache/building-state behavior.
|
||||
|
||||
Clone traversal review and cleanup are deferred to
|
||||
[future.md](future.md#4-clone-traversal-review). This effort preserves cloning's
|
||||
existing behavior while changing the shared schema and projection machinery.
|
||||
Keep projection's document-building semantics distinct from typed copying.
|
||||
The mostly explicit dispatch in `decodeValue` and `valueBuilder.build` does not
|
||||
warrant a generic reflection visitor solely because it has a high complexity
|
||||
score.
|
||||
|
||||
### 5. Remove Unused Inspection Conversions And Consolidate Real Adapters
|
||||
|
||||
Remove `toEngineSourceInfo`, `toEngineOrigin`, `toEngineSourceVersionCheck`, and
|
||||
`toEngineImportSelections` from [conversion.go](../../conversion.go), along with
|
||||
the engine adapters used only by that chain.
|
||||
At the reviewed revision, the three callers of `toEngineSourceInfo` are tests;
|
||||
`toEngineOrigin` has none. `SourceInfoWithPlacement`,
|
||||
`SourceInfoWithImportSelections`, `importSelectorFromString`, and
|
||||
`importSelectionOutcomeFromString` in [records.go](../../internal/engine/records.go)
|
||||
have no production role beyond that reverse chain. Consumers receive inspection
|
||||
records; production facade operations do not send those records back to the
|
||||
engine. The unused inverse path adds another ownership mapping to maintain as
|
||||
inspection evidence grows. Recheck callers at the implementation baseline before
|
||||
removal.
|
||||
|
||||
Rewrite the affected [record tests](../../records_test.go) around real
|
||||
engine-to-public conversion and detached public inspection. Preserve useful
|
||||
placement, version evidence, import-selection, and copy-isolation assertions.
|
||||
Avoid manufacturing replacement production setters solely to keep an old
|
||||
round-trip fixture working. Existing facade loads already provide genuine sources
|
||||
and inspection records.
|
||||
|
||||
Retain reverse conversions that serve real inputs: `toEngineDiagnostics`, for
|
||||
example, supports `WriteDiagnostics`. Consolidate repeated override/input
|
||||
conversion in `toEngineOptions` and `toEngineLayeredOptions` where semantics
|
||||
match. Keep explicit root-to-engine ownership conversion and the public-boundary
|
||||
checks. A reflective transport framework or public engine aliases would obscure
|
||||
the boundary rather than simplify it.
|
||||
|
||||
## Scope, Compatibility, And Policy Consequences
|
||||
|
||||
The proposed work spans private engine code, private root-facade conversions,
|
||||
affected tests, consumer guidance, and implementation documentation. Under the
|
||||
settled answer to [OQ-1](#oq-1-should-the-public-loading-apis-change-in-this-effort),
|
||||
preserve public types/signatures and the behavior owned by the current contracts,
|
||||
including:
|
||||
|
||||
- Acquisition versus composition ordering, strict scopes versus precedence
|
||||
tiers, identity ownership, placement, and source-version protection.
|
||||
- Discovery exhaustion and error behavior, source lifetime, passive resource
|
||||
contexts, and host/native namespace distinctions.
|
||||
- Defaults, post-decode CLI assignment, recorded resolution, secret handling,
|
||||
final registered-identity enforcement, and semantic validation.
|
||||
- Explicit presence, provenance/history, sensitivity, safe diagnostics and diff,
|
||||
frozen inspection, and detached publication/query results.
|
||||
- Contracted diagnostic codes, paths, coordinates, ordering, and resource limits.
|
||||
|
||||
Do not turn incidental helper order, exact private representations, numerical
|
||||
complexity scores, or allocation counts into new guarantees. Keep the existing
|
||||
algorithmic remediations effective; watch for repeated whole-tree traversals,
|
||||
policy scans, or copies introduced by helper extraction.
|
||||
|
||||
The recommendation fits the existing architecture policy and ADRs 0006,
|
||||
0015, 0017, 0018, and 0019. It needs no new architectural ownership decision or
|
||||
policy revision merely to introduce private helpers. Update the component
|
||||
overview and import internals when code moves. Accepted ADR decision content
|
||||
remains unchanged. A public loading-model redesign is outside this effort's
|
||||
settled scope.
|
||||
|
||||
Pre-1.0 public changes are possible under the [release policy](../policy/release.md),
|
||||
but they must have a consumer benefit and documented impact. This draft does not
|
||||
propose a parallel compatibility API or temporary migration scaffold. It also
|
||||
does not make refactoring completion a new release gate or establish that 1.0
|
||||
is otherwise ready. Future performance experiments remain in
|
||||
[future.md](future.md#3-performance-experiments); no benchmark campaign was run
|
||||
or is required to claim readability improvements.
|
||||
|
||||
## Validation And Completion
|
||||
|
||||
Use the [development guide](../development.md) and
|
||||
[testing policy](../policy/testing.md) for focused checks and the ordinary full
|
||||
development check. Preserve existing behavior tests; rewrite tests coupled only
|
||||
to removed plumbing. Add a test when a consequential handoff risk lacks coverage,
|
||||
not for every new helper.
|
||||
|
||||
| Work area | Existing protection and review focus |
|
||||
| --- | --- |
|
||||
| Acquisition and imports | Selection, defaults, scheduling, relative/item placement, source-version evidence, aggregate limits, repetition within versus across tiers, and ordinary/layered equivalence. |
|
||||
| Resolved contexts | Host/native access, discovery selected/absent outcomes, terminal context failures, source-relative resources, independent overlay context, and handle closure. |
|
||||
| Inheritance | Mapping and record inheritance, nested destination-local evaluation, child identity, policy lifetime, version movement, history ordering, sensitivity, and retained-node budgets. |
|
||||
| Projection/schema | Inline fields, recursive schemas, dynamic values, nil/empty values, cycles and limits, text codecs, secret states, callback ownership, and independent loads; preserve existing cloning behavior where shared descriptors/state are affected. |
|
||||
| Facade and consumers | Forward conversion, detached records, supported API boundaries, source-form equivalence, maintained simple/catalog/discovery examples, and safe explanation/diff. |
|
||||
|
||||
Examples of established boundaries include
|
||||
[source-form equivalence](../../facade_equivalence_test.go),
|
||||
[discovery integration](../../facade_discovery_test.go),
|
||||
[final identity validation](../../facade_final_sequence_identity_test.go),
|
||||
[inheritance history](../../internal/engine/inheritance_history_test.go), and
|
||||
[projection and cloning](../../internal/engine/projection_test.go).
|
||||
Use applicable race checks when changing ownership or shared state. Reuse checks
|
||||
for unchanged content and keep benchmark work separate unless new evidence
|
||||
justifies it and it is authorized.
|
||||
|
||||
Completion requires reviewable evidence that:
|
||||
|
||||
- Simple and advanced consumer examples remain concise and valid, with no new
|
||||
public concepts required for existing workflows.
|
||||
- Both load entry points reach one normalized internal model; acquisition no
|
||||
longer reconstructs broad options or uses version state to transport defaults.
|
||||
- Import, context, inheritance, and reflection responsibilities have identifiable
|
||||
owners and explicit handoffs without duplicate semantic implementations.
|
||||
- Unused reverse conversions and their exclusive adapters are removed, with
|
||||
useful inspection ownership coverage retained.
|
||||
- Contracted values, presence, provenance/history, diagnostics, sensitivity,
|
||||
resource lifetime, and algorithmic corrections remain protected by appropriate
|
||||
tests and source review.
|
||||
- The reviewed scope is accounted for against all six audit findings, with clone
|
||||
traversal review deferred to future work. Completed portions have validation
|
||||
evidence rather than being closed because code moved.
|
||||
- Consumer guidance, contributor references, and internal documentation describe
|
||||
the resulting implementation. Current contracts continue to own behavior.
|
||||
|
||||
The [implementation plan](implementation.md) defines private records, file owners,
|
||||
and stage boundaries. No open scope questions remain, and no additional feature
|
||||
semantics are needed to carry out the selected refactors.
|
||||
|
||||
## Settled Decisions
|
||||
|
||||
### OQ-1: Should The Public Loading APIs Change In This Effort?
|
||||
|
||||
**Settled:** retain `Load`/`Options`, `LoadLayered`/`LayeredOptions`, `Input`, and
|
||||
`Underlay`, including their existing overlay-context behavior. Consolidate their
|
||||
private inputs, conversions, and acquisition machinery, and improve consumer
|
||||
guidance. The [consumer-facing target](#consumer-facing-target) records the
|
||||
rationale and tradeoffs. This decision governs the consolidation effort; it does
|
||||
not establish a perpetual restriction on future API design.
|
||||
|
||||
### OQ-2: How Much Clone Cleanup Should This Effort Commit To?
|
||||
|
||||
**Settled:** defer clone traversal review and any resulting cleanup to
|
||||
[future.md](future.md#4-clone-traversal-review). Neither the review nor clone
|
||||
refactoring is part of this consolidation effort. Projection and schema
|
||||
refactoring remain in scope, including protection of existing cloning behavior
|
||||
where they share descriptors or operation state. The future review can conclude
|
||||
that no clone edits are warranted; its goal is demonstrated maintenance benefit.
|
||||
@@ -1,9 +1,9 @@
|
||||
# Potential Future Features
|
||||
# Potential Future Work
|
||||
|
||||
This document lists potential enhancements for consideration in future ConfigKit
|
||||
releases. Entries are not accepted contracts, implementation plans, or release
|
||||
commitments. Adopting a candidate requires an explicit design decision and a
|
||||
separate plan. Significant architectural exclusions and their rationale belong
|
||||
This document lists potential enhancements and maintenance work for consideration
|
||||
in future ConfigKit releases. Entries are not accepted contracts, implementation
|
||||
plans, or release commitments. Adopting a candidate requires an explicit design
|
||||
decision and a separate plan. Significant architectural exclusions and their rationale belong
|
||||
in ADRs; this document does not maintain a list of rejected proposals.
|
||||
|
||||
## 1. Recursive body merging for identified records
|
||||
@@ -84,35 +84,21 @@ evidence are preserved in Git history at `8e91a2b` under `docs/roadmap/`.
|
||||
- What cache lifetime and concurrency design would make schema reuse worthwhile
|
||||
without excessive retained memory?
|
||||
|
||||
## 4. Clone traversal review
|
||||
## 4. Public path comparison
|
||||
|
||||
Status: deferred. Review typed configuration cloning after the projection and
|
||||
schema consolidation work, when the resulting structure can inform the review.
|
||||
This is future maintenance work, not a prerequisite for completing consolidation.
|
||||
Status: deferred pending a concrete consumer need. This preserves A2 from the
|
||||
completed API review at `68400ff:docs/roadmap/review.md`.
|
||||
|
||||
The complexity audit identified `valueOperation.clone` in
|
||||
[clone.go](../../internal/engine/clone.go) as a candidate for cleanup. Much of its
|
||||
size is straightforward type dispatch. Focused handlers may clarify allocation,
|
||||
recursive copying, cycle tracking, secret-state handling, or text-codec
|
||||
reconstruction, but extraction may also add indirection without making the code
|
||||
easier to understand.
|
||||
`Path.String()` is a JSON Pointer spelling, not a complete semantic identity:
|
||||
an explicit numeric mapping key and a sequence index can share it. The public
|
||||
type is opaque and currently non-comparable.
|
||||
|
||||
When this work is taken up, assess the traversal and record whether changes are
|
||||
worthwhile; a conclusion that no refactor is needed is valid. Make focused
|
||||
changes only where the maintenance benefit justifies the code movement. Keep
|
||||
cloning's typed-copy semantics distinct from projection's document-building
|
||||
semantics. Preserve detached values, nil/empty distinctions, physical inline
|
||||
fields, cycle/depth checks, and unresolved versus validated secret handling.
|
||||
Reuse existing ownership, codec, secret, and traversal tests, adding coverage for
|
||||
consequential gaps. No performance improvement is assumed.
|
||||
If consumers need comparison, consider a small exact-representation operation
|
||||
rather than exposing mutable segments or introducing a pattern language. A
|
||||
proposal must distinguish exact segment representation, including parsed versus
|
||||
explicit segments and invalid state, from resolution against a document. Pointer
|
||||
string equality must not be presented as semantic equality.
|
||||
|
||||
### Questions to resolve before implementation
|
||||
|
||||
- After projection and schema consolidation, which clone branches still mix
|
||||
responsibilities enough to benefit from extraction?
|
||||
- Would a proposed helper keep allocation, recursive copying, and cycle cleanup
|
||||
easier to follow together, or merely move a short loop out of sight?
|
||||
- Are any small shared operations semantically identical across traversals, or
|
||||
would sharing require special cases that obscure their different purposes?
|
||||
- Which ownership or special-value risks, if any, lack sufficient existing test
|
||||
coverage for the proposed changes?
|
||||
Keeping `Path` solely as an opaque query/building value remains an alternative.
|
||||
No current contract changes or implementation commitment follow from this entry;
|
||||
a comparison operation can be added compatibly later.
|
||||
|
||||
@@ -1,783 +0,0 @@
|
||||
# Consolidation Implementation Plan
|
||||
|
||||
Status: complete. Stages 1 through 20 are complete.
|
||||
The target executor is a Luna coding agent working through one end-to-end sprint.
|
||||
|
||||
## Authority, Baseline, And Execution
|
||||
|
||||
The [consolidation roadmap](consolidation.md) owns the target state, rationale,
|
||||
and settled scope. Start from `8a99e6107001c69117a41dc37bc6ea09d2910af9` plus the
|
||||
commit introducing this plan. Production code is unchanged from the audited
|
||||
`5959537d1adc2516e7f7e8881df72b707ec48c3f`; intervening commits consolidated the
|
||||
planning documents and settled the public-API and clone decisions.
|
||||
|
||||
Read [development.md](../development.md), [agents.md](../policy/agents.md),
|
||||
[architecture.md](../policy/architecture.md), [testing.md](../policy/testing.md),
|
||||
and [documentation.md](../policy/documentation.md) as applicable. Use the code
|
||||
graph for discovery and confirm source at the implementation baseline. Current
|
||||
public comments and [config.md](../config.md) define behavior; this plan does not
|
||||
add public obligations.
|
||||
|
||||
Implement only the selected consolidation. Retain the public `Load`,
|
||||
`LoadLayered`, options, inputs, callbacks, source/search descriptions, inspection,
|
||||
and sequence-registration APIs. Keep the root-to-engine-to-document/YAML
|
||||
dependency direction. Clone traversal review/refactoring remains deferred under
|
||||
[future.md](future.md#4-clone-traversal-review). Changes to shared descriptors must
|
||||
preserve existing cloning behavior. This sprint does not publish a release,
|
||||
update dependencies/toolchain requirements, change configuration semantics, or
|
||||
introduce complexity/performance gates.
|
||||
|
||||
Stages are bounded work assignments, not independently shippable commits.
|
||||
Intermediate stages may leave callers, tests, or compilation unfinished until a
|
||||
later stage. Proceed directly through those dependencies; do not build temporary
|
||||
compatibility layers solely to keep each stage compiling. Update affected tests
|
||||
with their callers, and run the focused checks below when that portion builds.
|
||||
Record any deferred verification accurately. The completed sprint must compile,
|
||||
pass its checks, and leave no superseded plumbing. Commit completed, reviewable
|
||||
units under the agent policy; stage boundaries do not require separate commits.
|
||||
|
||||
Preserve existing meaningful validation/processing order, diagnostics, ownership,
|
||||
and limits. Private helper order and representation are not new contracts. Do
|
||||
not add rejection rules, error recovery, caches, new merge strategies, another
|
||||
pipeline framework, or tests for incidental helper choreography. A correctness
|
||||
issue discovered during the refactor needs identifiable regression evidence and
|
||||
an explicit explanation, rather than an unexplained expectation change.
|
||||
|
||||
## Chosen Private Design
|
||||
|
||||
Use the existing `loadRequest[T]` as the normalized complete-load model and
|
||||
`loadInput` for each selected input. Keep generic application definitions and
|
||||
the final lifecycle in `executeLoadRequest[T]`; acquisition is non-generic.
|
||||
The following records are ordinary engine-private data, not extension points.
|
||||
Use these names to make migrations concrete; their fields describe the selected
|
||||
ownership model, not a requirement to duplicate data already available there.
|
||||
|
||||
| Record | Fields and ownership |
|
||||
| --- | --- |
|
||||
| `acquisitionPolicy` | Read-only operation snapshot of `importDefaults`, `sequencePolicy`, and `importLimits`. Snapshot caller-owned defaults at entry, preserving nil versus empty. |
|
||||
| `acquisitionState` | Borrowed pointers to the load's `importAccounting` and `sourceVersionState`. One instance per load; version state no longer transports import defaults. |
|
||||
| `tierAcquisitionState` | The tier's visited-resource map and shared host namespace token. Underlay groups each own a fresh instance. Existing group graph aggregation and namespace anchors retain their current owners. |
|
||||
| `inputAcquisitionRequest` | `input loadInput`, source-version role, root-directive context, applicable protection rule, deferred-assembly choice, and `ordinaryOverlay string`. The last field carries only the ordinary main input's overlay-dependent validation/context timing; it is empty for underlays and layered main. |
|
||||
| `importAcquisitionRequest` | The input request, parsed main source, and already computed source-version/directive processing result. It preserves the distinction between source-local processing and application-path protection. |
|
||||
| `resolvedFilesystemContext` | `native *nativeNamespace` (nil means host), `hostToken *namespaceToken` where supplied, and `rootPath`, `basePath`, `mainPath` strings. Native facts use slash-relative paths; host facts use the existing canonical host paths. The record contains no open root/file or acquisition access object. |
|
||||
|
||||
The main private entry is
|
||||
`acquireInput(request inputAcquisitionRequest, policy acquisitionPolicy, state *acquisitionState, tier *tierAcquisitionState) (assembledInput, []Diagnostic)`.
|
||||
Import acquisition uses the corresponding named request with the same borrowed
|
||||
policy/state/tier owners. An access-based worker receives a resolved filesystem
|
||||
description and an explicitly owned `*filesystemAccess`, rather than more
|
||||
positional paths and independent optional policy parameters. Retain
|
||||
`assembledInput`, `acquisitionScope`, and `acquiredContribution` as the structural
|
||||
handoffs; add an optional resolved-context field to `assembledInput` for reuse.
|
||||
|
||||
`importTraversal` borrows the policy and state references. Active routes, stack,
|
||||
current item scope/prefix/fallback, and the acquired inclusion graph remain
|
||||
traversal-local. Nested item traversal shares the active graph and tier repetition
|
||||
state; it does not create another tier. Reusing a file in a later tier remains
|
||||
valid. Do not collapse namespace identity, lexical candidate spelling, and stable
|
||||
scheduling keys into one key.
|
||||
|
||||
Context facts are obtained at the existing points where those facts are needed.
|
||||
There is no global eager context-validation pass or persistent live-handle owner.
|
||||
Directory checks, canonicalization, handle opening/closing, and passive view
|
||||
creation remain distinguishable. Reusing a description does not authorize
|
||||
earlier checks that bypass an existing earlier error or implicit loading activity
|
||||
after publication. Consumer-initiated passive filesystem operations retain their
|
||||
existing contract.
|
||||
|
||||
For inheritance, retain both scope adapters and one executor. Use private
|
||||
prepared-child/prepared-parent records for combination inputs, then an
|
||||
`inheritanceTemplateResult` containing the resolved value, origins, local policy,
|
||||
policy templates, and the controls/transfers/replacements/operations produced by
|
||||
that template. Sequence position evidence lives in the existing operation
|
||||
records. Merge callbacks mutate that local result, not the enclosing executor's
|
||||
accumulator. The executor appends the result and applies existing accounting.
|
||||
|
||||
For reflection, keep `valueOperation` and `schemaBuilder` as their existing state
|
||||
owners. Extract substantial value-kind and inline-field operations as methods;
|
||||
do not introduce a generic reflection visitor or merge the two schema views.
|
||||
|
||||
## Validation Groups
|
||||
|
||||
Use the installed compatible Go toolchain and already acquired dependencies as
|
||||
described in the development guide. Commands run from the repository root with
|
||||
`GOTOOLCHAIN=local` and `GOPROXY=off`. These are development checks for this
|
||||
refactor, not additions to the release procedure.
|
||||
|
||||
| Group | Commands and purpose |
|
||||
| --- | --- |
|
||||
| F: facade cleanup | `go test . -run 'SourceInfo|SourceVersionCheck|ImportSelection|Facade|PublicFacadeBoundary|DependencyImportBoundary'` checks useful forward evidence and the existing public boundary. |
|
||||
| A: acquisition integration | `go test ./internal/engine -run 'Load|Grouped|Source|Search|Discovery|Import|Glob|Filesystem|Native|Destination|Overlay|Resource|Version|Sequence|Collection'`; `go test . -run 'Facade|EquivalentSourceForms|Search|SourceContext|ImportDefaults|SourceVersion'`; `go test ./examples/consumers`. Covers normalization, ordinary/layered acquisition, selection, contexts, scopes, and consumer workflows. |
|
||||
| I: inheritance | `go test ./internal/engine -run 'Inheritance|Inherited|Sequence|Collection|Version|History|Explain|Sensitivity|Secret'`; `go test . -run 'Inheritance|Inherited|SequenceIdentity|FinalSequence'`. Covers shared adapters and their evidence. |
|
||||
| R: reflection | `go test ./internal/engine -run 'Schema|Decode|Projection|Clone|Inline|CompareDocument|Callback|Ownership|Independent|Secret'`. Covers descriptors, projection, shared cloning behavior, and ownership. |
|
||||
| Final | `./scripts/check.sh` followed by `go test -race ./...`. The first owns formatting, whitespace, ordinary tests/examples/doc links, vet, and build; race validation is appropriate because state and ownership handoffs change throughout this sprint. |
|
||||
|
||||
Run F after Stage 2, A after Stages 9 and 13, I after Stage 16, and R after
|
||||
Stage 18 when buildable. If earlier migrations still prevent compilation, repair
|
||||
them at the next integration point and run the deferred group there. Run the
|
||||
final checks once on the finished code/documentation; repeat only for subsequent
|
||||
changes affecting their evidence. No benchmark campaign, timing threshold,
|
||||
traversal-count test, extended fuzz campaign, CI wait, or manual platform check is
|
||||
required by this plan. Add narrow behavior tests only for consequential gaps
|
||||
identified below that existing tests do not already cover.
|
||||
|
||||
## Stage 1 — Establish The Starting Map (Complete)
|
||||
|
||||
**Work:**
|
||||
|
||||
1. Confirm the revision/worktree and preserve unrelated work. Read the roadmap's
|
||||
settled decisions and the selected owners above.
|
||||
2. Map the current callers of `selectOptions`, `assembleBaseInput...`, both
|
||||
`loadImported...` ladders, `contextsForAcquiredSources`, the selected-overlay
|
||||
loaders, and `applyInheritanceScopeTemplates`. Inspect test callers as well
|
||||
as production callers; some old entry variants exist mainly for tests.
|
||||
3. Identify the existing behavioral owners for repeated resources within/across
|
||||
tiers, enabled/disabled version checking, selected/absent searches, ordinary
|
||||
versus independent layered overlays, item-local helper scopes, inherited
|
||||
identity/history, inline fields, and detached records.
|
||||
4. Keep brief working notes or implementation progress in this plan. Reuse
|
||||
existing validation evidence where applicable; establish an implicated
|
||||
baseline only when needed to distinguish a pre-existing failure. Do not
|
||||
generate another broad audit or blanket characterization-test suite.
|
||||
|
||||
**Finish:** a concrete caller/coverage map and any actual coverage gaps. No
|
||||
production change or empty commit is needed for orientation alone.
|
||||
|
||||
Progress notes (2026-10-25): the worktree was clean at `8ab1568`, immediately
|
||||
after the planning baseline `8a99e61`; the audited production baseline
|
||||
`5959537` is present. `selectOptions` is called by the ordinary and grouped
|
||||
load entry points. The `assembleBaseInput...` wrappers converge on
|
||||
`assembleBaseInputTrackedForRole`, reached from ordinary loading, layered main
|
||||
loading, and underlay-group acquisition. The tracked import wrappers converge
|
||||
on `loadImportedDocumentsTrackedWithProcessing`; production callers are input
|
||||
acquisition and underlay-group acquisition, while older convenience variants
|
||||
also have direct test callers. `contextsForAcquiredSources` is shared by ordinary
|
||||
loading, layered assembly, input acquisition, and overlay application. Selected
|
||||
overlay loading is reached from ordinary overlay application and layered input
|
||||
assembly. Both inheritance adapters, for ordinary and identified sequences,
|
||||
feed `applyInheritanceScopeTemplates`.
|
||||
|
||||
Existing behavioral owners cover repeated/cyclic resources and cross-tier reuse
|
||||
(`internal/engine/imports_test.go`, `facade_equivalence_test.go`); default and
|
||||
enabled source-version modes (`internal/engine/source_version_test.go`,
|
||||
`facade_source_version_mode_test.go`); selected/absent discovery
|
||||
(`internal/engine/discovery_load_test.go`, `facade_discovery_test.go`); ordinary
|
||||
and layered overlays (`internal/engine/selected_overlay_test.go`,
|
||||
`facade_layered_test.go`); item-local helper scopes (`internal/engine/imports_test.go`,
|
||||
`facade_import_lifecycle_test.go`); inheritance identity/history
|
||||
(`internal/engine/inheritance_identity_history_test.go`,
|
||||
`internal/engine/inheritance_history_test.go`); inline fields
|
||||
(`internal/engine/schema_test.go`, `internal/engine/projection_test.go`); and
|
||||
detached results and callback ownership (`internal/engine/metadata_test.go`,
|
||||
`internal/engine/callback_ownership_test.go`, `facade_diff_test.go`). No specific
|
||||
coverage gap surfaced in this map; later focused checks remain assigned in the
|
||||
validation groups above.
|
||||
|
||||
## Stage 2 — Remove Unused Reverse Bridges And Share Real Facade Conversion (Complete)
|
||||
|
||||
**Owners:** root `conversion.go`, `records_test.go`, `lifecycle.go`, `layered.go`,
|
||||
and engine `records.go`.
|
||||
|
||||
1. Reconfirm callers, then delete `toEngineSourceInfo`, `toEngineOrigin`,
|
||||
`toEngineSourceVersionCheck`, and `toEngineImportSelections`, plus
|
||||
`SourceInfoWithPlacement`, `SourceInfoWithImportSelections`,
|
||||
`importSelectorFromString`, and `importSelectionOutcomeFromString` when their
|
||||
only remaining purpose is the deleted chain.
|
||||
2. Rewrite the three reverse-round-trip record tests using genuine engine output
|
||||
and forward conversion/public inspection. Retain placement, version-evidence,
|
||||
selection-evidence, nil/empty, and detached-copy protection. Engine test
|
||||
fixtures can use existing loading APIs; do not add production fixture setters.
|
||||
3. Extract one root `toEngineOverrides` helper and use it from both option
|
||||
converters. Reuse `toEngineInput`, filesystem, path, definition, and default
|
||||
conversion where already shared. Preserve slice ownership and nil/empty
|
||||
behavior; do not make conversion interpret/validate input values.
|
||||
4. Retain reverse diagnostic conversions used by `WriteDiagnostics`, forward
|
||||
source/origin inspection conversion, and all public named types.
|
||||
|
||||
**Finish:** the unnecessary cross-package inverse interface is gone and real
|
||||
conversion is shared without a new transport layer. Run F.
|
||||
|
||||
Progress notes (2026-10-25): removed the unused reverse source-record bridge
|
||||
and engine setters, replaced synthetic round-trip fixtures with engine loads,
|
||||
and shared override conversion across ordinary and layered options. Validation
|
||||
group F passed with `GOTOOLCHAIN=local GOPROXY=off go test . -run
|
||||
'SourceInfo|SourceVersionCheck|ImportSelection|Facade|PublicFacadeBoundary|DependencyImportBoundary' -count=1`.
|
||||
|
||||
## Stage 3 — Normalize Preflight Without Fabricated Options (Complete)
|
||||
|
||||
**Owners:** engine `options.go`, ordinary entry in `load.go`, grouped entry in
|
||||
`load_request.go`, and their option tests.
|
||||
|
||||
1. Extract non-generic input normalization/validation over `loadInput`, returning
|
||||
the normalized input plus diagnostics. Pass the ordinary overlay name only
|
||||
where its context requirements apply. Keep discovery-description validation,
|
||||
placement checks, normalized destinations, and filesystem option checks at
|
||||
their existing pre-acquisition boundary.
|
||||
2. Separate load-wide import-default validation, version-rule/mode validation,
|
||||
and sequence-registration compilation from per-input checks. Share their
|
||||
mechanisms between entries without adding a second interpretation of options.
|
||||
3. Keep the public/engine entry arity handling and each entry's existing error
|
||||
classification. Ordinary loading still translates its one selected options
|
||||
value; layered loading validates main, all underlay inputs, and its independent
|
||||
overlay context in the existing meaningful order. Preserve canonical diagnostic
|
||||
accumulation and early returns, including invalid options before reads.
|
||||
4. Replace the grouped entry's constructed `Options[T]` values with direct
|
||||
normalization of `loadInput`; set the effective protection rule from the
|
||||
normalized main placement. Leave typed application behavior in the definition.
|
||||
5. Remove `importDefaultsValidated` and preflight-only forwarding machinery when
|
||||
callers migrate. Update tests through the entry/helper that owns the behavior,
|
||||
rather than keeping `selectOptions` solely as a test compatibility surface.
|
||||
|
||||
**Finish:** both entries produce validated `loadRequest[T]` values without using
|
||||
the ordinary options type to validate grouped inputs. Acquisition may still use
|
||||
old options until Stage 6; do not broaden this stage into that migration.
|
||||
|
||||
Progress notes (2026-10-25): added shared normalization over `loadInput`, moved
|
||||
load-wide defaults, source-version, and sequence-registration validation into
|
||||
shared settings preparation, and removed `selectOptions` plus its validation
|
||||
flag from grouped preflight. `GOTOOLCHAIN=local GOPROXY=off go test ./...` passed.
|
||||
|
||||
## Stage 4 — Express Input Acquisition With Non-Generic Requests (Complete)
|
||||
|
||||
**Owners:** new engine `acquisition.go` and existing `load_input.go`.
|
||||
|
||||
1. Define `acquisitionPolicy`, `acquisitionState`, `tierAcquisitionState`,
|
||||
`inputAcquisitionRequest`, and `importAcquisitionRequest` as selected above.
|
||||
Reuse existing enums, policy records, and accounting; introduce no new public
|
||||
controls or duplicate budget counter.
|
||||
2. Replace the generic `assembleBaseInputTrackedForRole` implementation with
|
||||
`acquireInput`. Translate its references to source, imports, filesystem,
|
||||
placement, role, directive context, version rule, policy, and deferred
|
||||
assembly into explicit request/policy/state fields.
|
||||
3. Keep its phases visible: search selection; parse and failed-source evidence;
|
||||
disabled-directive checks; source-local version/directive processing; import
|
||||
acquisition or single-input placement; accounting/repetition registration;
|
||||
context handoff. Small helpers may isolate version/directive preparation.
|
||||
4. Preserve discovery resolution once per input consumption and the acquired
|
||||
concrete source on both success and failure. Keep mapping diagnostic
|
||||
destination projection and the ordinary-overlay context deferral unchanged.
|
||||
5. Retain `assembledInput`'s existing structural/evidence fields. Do not merge
|
||||
parsed/local payloads with placed documents or eagerly compose deferred input.
|
||||
|
||||
**Finish:** the shared input-acquisition implementation no longer depends on
|
||||
`Options[T]` or an application type. Call-site and import-worker migration is
|
||||
completed by Stages 5–6; temporary non-compilation is acceptable.
|
||||
|
||||
Progress notes (2026-10-25): introduced the non-generic acquisition request and
|
||||
policy/state/tier records, moved the input-acquisition implementation into
|
||||
`acquireInput`, and added the resolved-context handoff field. The focused engine
|
||||
test command currently cannot compile because the old assembly callers in
|
||||
`load_input.go` and `load_request.go` are scheduled to migrate in Stage 6; test
|
||||
verification is deferred until that integration.
|
||||
|
||||
## Stage 5 — Consolidate Import Requests And Default Ownership (Complete)
|
||||
|
||||
**Owners:** engine import entry points, `importTraversal`, `source_version.go`,
|
||||
and tests that directly invoke the old import variants.
|
||||
|
||||
1. Replace the `loadImportedDocumentsTracked...` ladder with
|
||||
`acquireImportedInput` using `importAcquisitionRequest` and the shared
|
||||
policy/state/tier owners. Replace the access-based ladder with one worker
|
||||
receiving the same request plus explicit access/resolved facts. Keep only
|
||||
genuinely distinct convenience entries, not one wrapper per optional field.
|
||||
2. Move `importDefaults` out of `sourceVersionState`. Resolve declarations from
|
||||
`acquisitionPolicy.importDefaults`, including nested item scopes. Import
|
||||
defaults must work independently of whether a version rule/state is active.
|
||||
Preserve enabled version processing and its separate local/effective rules.
|
||||
3. Make traversal references to aggregate accounting, version evidence, tier
|
||||
repetition, and immutable policies point to their existing operation owners.
|
||||
Keep active-route tracking, scope switching, and inclusion edges local to
|
||||
the traversal. Do not allocate a fresh visited map inside a nested item scope.
|
||||
4. Preserve nil version-state behavior used by lower-level operations and the
|
||||
existing default processing branch. Replace optional positional/variadic
|
||||
policy passing with explicit inputs for meaningful operation distinctions.
|
||||
Private test entry spellings do not need compatibility wrappers; preserve
|
||||
their relevant behavioral cases through the resulting operation.
|
||||
5. Migrate direct import tests to named inputs. Small test-only constructors may
|
||||
supply default state/policy values when this reduces repetitive fixtures.
|
||||
Keep their behavior assertions intact.
|
||||
|
||||
**Finish:** the import request surface has named cohesive inputs; defaults no
|
||||
longer travel through version processing; no forwarding ladder remains merely
|
||||
to supply another nil/default positional parameter.
|
||||
|
||||
Progress notes (2026-10-25): replaced the import entry ladders with named
|
||||
requests and one access worker, moved defaults to acquisition policy, and
|
||||
retargeted direct import tests. The focused engine test command cannot compile
|
||||
yet because the remaining ordinary and grouped assembly callers are scheduled
|
||||
for Stage 6; verification is deferred until that integration. `git diff --check`
|
||||
passed.
|
||||
|
||||
## Stage 6 — Wire Ordinary And Grouped Lifecycle To Acquisition (Complete)
|
||||
|
||||
**Owners:** engine `load.go`, `load_request.go`, `load_input.go`, related tests.
|
||||
|
||||
1. Construct the load's acquisition policy and mutable state once in
|
||||
`executeLoadRequest`. Keep the definition and schema/callback lifecycle there.
|
||||
2. Call `acquireInput` for main using its ordinary/layered directive context,
|
||||
main version role, current protection rule, and deferred assembly. Preserve
|
||||
the existing schema-build and version-check timing, including the different
|
||||
default/enabled source-version paths.
|
||||
3. Have `acquireUnderlayGroupScopes` use the same load policy/accounting/version
|
||||
state and one fresh tier state per group. Remove `Options[struct{}]`
|
||||
construction. Preserve explicit namespace anchors and graph reanchoring.
|
||||
4. Main acquisition still precedes underlays. Strict assembly still follows
|
||||
the existing dependency-ready scopes and lower-tier identity information.
|
||||
Keep group-local repetition separate from load-wide limits, and retain the
|
||||
existing selected-overlay duplicate-source checks.
|
||||
5. Replace the ordinary `applyInputLayers` options argument with the precise
|
||||
main/overlay/protection/policy inputs it uses. A named overlay request will
|
||||
complete that migration in Stage 9. Remove remaining production
|
||||
`assembleBaseInput...` variants and the acquisition-only `sequencePolicy`,
|
||||
`deferAssembly`, and `directiveContext` fields from `Options`. Retain the
|
||||
placement, destination, and import-default fields used by facade translation.
|
||||
6. Retain real lower-level test/benchmark capabilities with direct request
|
||||
construction or test-only adapters. Do not delete meaningful source/limit
|
||||
cases to make removed helpers disappear.
|
||||
|
||||
**Finish:** no executor or underlay caller reconstructs broad/generic options
|
||||
for acquisition. The shared path is wired end to end; context and import-flow
|
||||
extraction continue in later stages.
|
||||
|
||||
Progress notes (2026-10-25): `executeLoadRequest` now creates one acquisition
|
||||
policy/state and acquires the main input through `acquireInput`; underlay groups
|
||||
reuse those load owners with a fresh tier state. Removed the generic base-input
|
||||
assembly variants and the acquisition-only fields from `Options`, and adapted
|
||||
direct tests to construct named requests. A search-source regression was caught
|
||||
and fixed during verification. The focused engine selection, facade selection,
|
||||
consumer example tests, and `GOTOOLCHAIN=local GOPROXY=off go test ./...` passed.
|
||||
|
||||
## Stage 7 — Separate Resolved Filesystem Facts From Access (Complete)
|
||||
|
||||
**Owners:** new engine `filesystem_context.go`, `filesystem.go`,
|
||||
`filesystem_access.go`.
|
||||
|
||||
1. Add `resolvedFilesystemContext` with the fields selected above. Reuse native
|
||||
namespace/token identity; do not compare arbitrary `fs.FS` values or equate
|
||||
equal labels with equal namespaces.
|
||||
2. Extract host and native fact derivation from `importFilesystem` and
|
||||
`nativeFilesystemAccess`. Prefer a selected `SourceInfo.Path` when it already
|
||||
supplies canonical source identity. Preserve source-derived file bases,
|
||||
explicit in-memory roots/bases, native slash paths, and the root-only absent
|
||||
host default.
|
||||
3. Keep separate functions for the directory checks and opening access that
|
||||
currently occur at each operation. A returned description does not mean
|
||||
every possible check has run. Do not add validation-state flags or an
|
||||
all-purpose eager resolver with stronger acceptance rules.
|
||||
4. Adapt the import worker to consume resolved facts and explicitly own its
|
||||
access until the existing acquisition handoff closes it. Keep close-error
|
||||
reporting and primary-error precedence unchanged. Native access borrows its
|
||||
caller filesystem and must not close it.
|
||||
5. Keep any old derivation adapters only while genuine callers migrate in
|
||||
Stages 8–9; remove them at the end of that migration.
|
||||
|
||||
**Finish:** common context facts have one private representation while validation
|
||||
timing and live resource ownership remain explicit.
|
||||
|
||||
Progress notes (2026-10-25): added shared host/native filesystem descriptions,
|
||||
separated directory validation and access creation, and migrated import
|
||||
acquisition to pass resolved facts alongside its owned access. The prior
|
||||
derivation adapters remain for the validator and overlay migration. Focused
|
||||
acquisition tests, `GOTOOLCHAIN=local GOPROXY=off go test ./...`, and
|
||||
`git diff --check` passed.
|
||||
|
||||
## Stage 8 — Carry Context Facts Through Input And Passive Views (Complete)
|
||||
|
||||
**Owners:** engine `load_input.go`, `source_context.go`, `load_request.go`, and
|
||||
the ordinary context backfill in `load.go`.
|
||||
|
||||
1. Add the optional resolved-context field to `assembledInput`. Populate it
|
||||
from import acquisition or the existing non-import context validation point.
|
||||
When context work is deferred for an ordinary overlay, leave it deferred.
|
||||
2. Replace the source-kind derivation branches of `contextsForAcquiredSources`
|
||||
with `contextsForResolvedSources`: consume resolved facts and source records,
|
||||
construct the existing rooted host/native passive view, and compute each
|
||||
source's relative base using the appropriate path rules.
|
||||
3. Share relative-base/containment calculation where applicable; retain each
|
||||
acquired source's own path and source-ID association. Do not substitute the
|
||||
first source record for the selected main file or attach all values to main.
|
||||
4. Migrate normal input acquisition, underlay contexts, and ordinary context
|
||||
backfill. Resolve only still-missing facts at the same lifecycle point as
|
||||
before. Retained descriptions/views never capture `filesystemAccess` or
|
||||
`*os.Root`; the existing passive `ResourceContext` contract stays unchanged.
|
||||
5. Retain failure evidence, root-only absent host context, discovered native
|
||||
outcomes, and deferred search-base behavior. Update focused context tests
|
||||
only where private function signatures changed.
|
||||
|
||||
**Finish:** ordinary/underlay acquisition and passive views use the common facts;
|
||||
selected-overlay callers are the remaining migration in Stage 9.
|
||||
|
||||
Progress notes (2026-10-25): input acquisition now retains resolved filesystem
|
||||
facts and builds passive contexts from those facts for ordinary and underlay
|
||||
sources. Migrated the ordinary backfill and reordered-source context test. The
|
||||
selected-overlay adapters remain for Stage 9. Focused context/acquisition tests,
|
||||
`GOTOOLCHAIN=local GOPROXY=off go test ./...`, and `git diff --check` passed.
|
||||
|
||||
## Stage 9 — Migrate Selected Overlays Without Merging Their Semantics (Complete)
|
||||
|
||||
**Owners:** engine `selected_overlay.go`, ordinary `applyInputLayers`, and
|
||||
`assembleLayeredInput`.
|
||||
|
||||
1. Define a private `selectedOverlayRequest` containing the overlay name,
|
||||
resolved filesystem description, protection rule, sequence policy, and
|
||||
existing limits/version state. Derive context at the caller's existing
|
||||
lifecycle point. Pass assembly and source evidence explicitly to the worker.
|
||||
2. Route ordinary overlays through the main source's context; route layered
|
||||
overlays through their independent `OverlayFilesystem`. Use named operation
|
||||
inputs instead of fabricating a `sourceBytes`/empty parsed-main pair merely
|
||||
to select a context branch. Preserve the actual absent-host root behavior.
|
||||
3. Return or retain the resolved description used by overlay acquisition and
|
||||
build overlay passive context from it. Reuse main facts only when that
|
||||
ordinary operation previously used the same root. Context facts do not share
|
||||
a live handle between the main import traversal and the later overlay read.
|
||||
4. Keep the production collection-aware overlay path. Migrate meaningful
|
||||
lower-level tests to that path with explicit policy/limits, retaining their
|
||||
budget, version, and failure assertions. Delete the test-only legacy branch
|
||||
selected by missing variadic policy and its exclusive adapters, rather than
|
||||
treating optional argument count as a contract. The baseline production entry
|
||||
points all supply the policy argument.
|
||||
5. Keep overlay source IDs, duplicate detection, version protection, directory
|
||||
validation, read/close errors, aggregate accounting, and post-read application
|
||||
order. Remove superseded context derivation and overlay-forwarding adapters.
|
||||
|
||||
**Finish:** all acquisition/context callers share facts and named inputs while
|
||||
ordinary and independent layered overlay semantics remain distinct. Run A when
|
||||
buildable; carry any genuine remaining migration dependencies to the next
|
||||
integration opportunity instead of adding scaffolding or stopping the sprint.
|
||||
|
||||
Progress notes (2026-10-25): introduced the named overlay request and migrated
|
||||
ordinary and independent layered overlay acquisition to resolved facts plus
|
||||
explicit access. Replaced the variadic compatibility branch with the existing
|
||||
collection-aware path, removed old context adapters, and retained overlay
|
||||
validation timing, source evidence, budgets, version protection, and close
|
||||
reporting. Validation group A, consumer examples, the full Go suite, and
|
||||
`git diff --check` passed.
|
||||
|
||||
## Stage 10 — Give Import Responsibilities Focused Files (Complete)
|
||||
|
||||
**Owners:** engine `imports.go` and new `import_acquisition.go`,
|
||||
`import_scope.go`, `import_projection.go`.
|
||||
|
||||
1. Put the request entry/access worker and traversal admission/orchestration in
|
||||
`import_acquisition.go`. Keep `imports.go` as the small common owner for
|
||||
limits, accounting/route/traversal data, and shared import diagnostics.
|
||||
2. Move `composeAcquiredInputWithPolicies`, `composeAcquiredScopeWithPolicy`,
|
||||
pending-item composition, placement reservation, and their item-scope
|
||||
assembly helpers into `import_scope.go`.
|
||||
3. Move nested-origin/policy/evidence projection, acquired source destinations,
|
||||
version-evidence projection, sequence-prefix helpers, and diagnostic-path
|
||||
projection into `import_projection.go`. Scheduling and graph identity remain
|
||||
with their existing graph/scheduler owners.
|
||||
4. Make the acquisition-to-closed-resources-to-assembly handoff visible. Keep
|
||||
evidence projection based on existing composition applications/position maps;
|
||||
file movement does not authorize another merge, full-tree replay, or scan of
|
||||
every unrelated policy for each record.
|
||||
|
||||
**Finish:** maintainers can locate acquisition, local strict assembly, and evidence
|
||||
work separately. Keep this stage structural; parse/admission extraction follows.
|
||||
|
||||
Progress notes (2026-10-25): moved request acquisition and traversal admission to
|
||||
`import_acquisition.go`, local strict composition and item-scope assembly to
|
||||
`import_scope.go`, and origin, destination, version-evidence, and diagnostic-path
|
||||
projection to `import_projection.go`. Shared limits, traversal data, and import
|
||||
diagnostics remain in `imports.go`. `./scripts/check.sh` passed.
|
||||
|
||||
## Stage 11 — Extract Import Declaration Parsing Operations (Complete)
|
||||
|
||||
**Owner:** engine `import_spec.go`.
|
||||
|
||||
1. Retain the outer mapping check, exactly-one-selector check, source-order field
|
||||
walk, applicability checks, and final selector parsing in
|
||||
`parseImportDeclarationSpec`.
|
||||
2. Extract boolean assignment, wildcard-set parsing/validation (shared by
|
||||
`pattern` and `exclude`), placement parsing, and selector-value parsing into
|
||||
ordinary private helpers. Reuse existing `parseStringSet`, authored-member
|
||||
sites, and destination/depth parsers.
|
||||
3. Preserve unspecified pointers and set flags, especially nil versus empty
|
||||
pattern lists. Keep error paths/locations tied to the authored selector or
|
||||
list member. Do not reorder consequential checks or replace the selector
|
||||
matrix with a configurable parser registry.
|
||||
|
||||
**Finish:** field decoding is readable without changing declaration syntax,
|
||||
selector rules, default resolution, or diagnostics. Keep behavior tests as the
|
||||
owner; no helper-by-helper assertion inventory is needed.
|
||||
|
||||
Progress notes (2026-10-25): extracted boolean assignment, shared wildcard set
|
||||
assignment, placement parsing, and selector-value parsing into private helpers.
|
||||
The declaration parser retains its validation ordering, authored member sites,
|
||||
and nil-versus-empty presence behavior. Import-focused tests and
|
||||
`./scripts/check.sh` passed.
|
||||
|
||||
## Stage 12 — Extract Directory Entry Admission And Descent (Complete)
|
||||
|
||||
**Owner:** engine `import_selection.go`, using existing directory/link helpers.
|
||||
|
||||
1. Keep root normalization/deduplication, root ordering, the explicit traversal
|
||||
stack, candidate union, sorting, exclusion, and required-empty decision visible
|
||||
in `selectDirectoryImportCandidates`.
|
||||
2. Extract coherent entry inspection and descent preparation. Return a small
|
||||
result identifying skip, admitted file, or directory frame to enter, plus a
|
||||
diagnostic when appropriate. Keep stack/active-directory mutation with the
|
||||
traversal so its lifetime is visible; do not invent a general visitor API.
|
||||
3. Preserve hidden/follow/depth behavior, distinct lexical candidates versus
|
||||
canonical access identity, resource-entry accounting, root-error handling,
|
||||
and active-path cycle detection. A later non-active alias remains eligible;
|
||||
exclusions still do not suppress discovery errors.
|
||||
4. Keep globs and first-match main-file discovery on their existing mechanisms.
|
||||
Share a helper only where its existing admission semantics actually coincide.
|
||||
|
||||
**Finish:** substantial entry/descent policy is separated from stack orchestration
|
||||
without changing the selection algorithm or adding repeated root walks.
|
||||
|
||||
Progress notes (2026-10-25): extracted entry inspection and file/directory
|
||||
admission into a result-returning helper. The selector retains root handling,
|
||||
stack and active-path mutation, candidate union, sorting, exclusions, and the
|
||||
required-empty decision. Directory-focused tests and `./scripts/check.sh` passed.
|
||||
|
||||
## Stage 13 — Extract Imported-Source Admission From Recursion (Complete)
|
||||
|
||||
**Owner:** engine `import_acquisition.go` after Stage 10.
|
||||
|
||||
1. Keep `importTraversal.visitPath` as the visible coordinator of containment,
|
||||
canonical identity, cycle/repetition checks, read/parse, directive/version
|
||||
processing, placement, and recursive declaration traversal.
|
||||
2. Extract source admission/read-and-process results into a private
|
||||
`admittedImport` carrying canonical resource/node identity, source record,
|
||||
processed payload/declarations, and processing evidence. Return an explicit
|
||||
optional-missing outcome; do not represent a read failure as absence.
|
||||
3. Preserve when source records are appended, rolled back for optional missing
|
||||
input, and retained for failures; preserve source IDs, accounting increments,
|
||||
version-callback ordering, and inclusion-edge creation on repetition/cycles.
|
||||
4. Pass the admitted record to mapping/item placement helpers rather than
|
||||
repeating its payload, declarations, and processing evidence as parallel
|
||||
arguments to `visitItemSource`.
|
||||
5. Factor route push/pop and item-scope save/restore only where it makes their
|
||||
shared lifetime clearer. Keep active graph state across item scopes and
|
||||
reserve placement nodes once. Item helpers complete before outer placement;
|
||||
they are strict local peers, not a precedence layer.
|
||||
|
||||
**Finish:** admission decisions and recursion have clear handoffs, with one
|
||||
read/process path and the original scheduling/evidence algorithms. Run A again
|
||||
because Stages 10–13 changed the import implementation after the first checkpoint.
|
||||
|
||||
Progress notes (2026-10-25): introduced an explicit admitted-import record and
|
||||
optional-missing outcome for source read/parse/version processing. `visitPath`
|
||||
retains containment, identity/cycle checks, graph updates, and recursive
|
||||
placement coordination; item placement receives the admitted record directly.
|
||||
Validation group A and `./scripts/check.sh` passed.
|
||||
|
||||
## Stage 14 — Extract Inheritance Child And Parent Preparation (Complete)
|
||||
|
||||
**Owners:** engine `inheritance_schedule.go`, new `inheritance_prepare.go`, and
|
||||
existing scope/policy helpers.
|
||||
|
||||
1. Extract preparation of the child's directive-free payload, direct origins,
|
||||
local policy templates, and optional control/control-origin evidence.
|
||||
2. Extract parent preparation: clone/project policies, lower them against the
|
||||
child's claims, exclude parent identity from the inherited payload, and
|
||||
prepare the separate parent history snapshot where currently required.
|
||||
Keep effective policy templates and transfer construction available to the
|
||||
combination result rather than reconstructing them afterward.
|
||||
3. Use focused prepared-child/prepared-parent records of these related values.
|
||||
Borrow authored/resolved parent inputs read-only; retain existing clone points
|
||||
for mutable payloads and history. Keep the current preliminary lower-bound
|
||||
budget decisions at the executor's existing point before materialization.
|
||||
4. Keep mapping and identified-sequence adapters, the stable Kahn scheduler, and
|
||||
the parentless/cancelled path semantics intact. Do not flatten nested directives
|
||||
before their destination-local evaluation.
|
||||
|
||||
**Finish:** the executor no longer embeds child/control and parent/policy
|
||||
preparation. Actual recursive combination/evidence moves in Stage 15.
|
||||
|
||||
Progress notes (2026-10-25): extracted child/control and parent/policy preparation
|
||||
into focused records in `inheritance_prepare.go`. The executor retains its
|
||||
preliminary budget checks, scheduler, and combination path, while history and
|
||||
transfer inputs stay available to combination. Engine inheritance coverage and
|
||||
facade inheritance tests passed.
|
||||
|
||||
## Stage 15 — Return One Inheritance Combination Result (Complete)
|
||||
|
||||
**Owners:** new engine `inheritance_combine.go`, `inheritance_collection.go`,
|
||||
and the shared executor.
|
||||
|
||||
1. Add `inheritanceTemplateResult` as described above and extract the existing
|
||||
parent/child combination into a helper receiving prepared inputs and the
|
||||
application policy. Parentless templates return the same result shape.
|
||||
2. Keep `mergeRecursiveMappings` and `combineSequence` as the combination
|
||||
mechanisms. Their callbacks update local origins/policy and local replacement
|
||||
evidence. Have `applyInheritanceSequenceOrigins` return its replacement slice
|
||||
instead of taking the whole enclosing scope result; append that slice to the
|
||||
local template result while retaining its updates to the supplied origin maps.
|
||||
3. Derive sequence history, transferred origins, retired/replaced policies,
|
||||
identity preservation, and the transfer operation from that same combination.
|
||||
Preserve `historyParent` and all policy/identity fields on the existing
|
||||
`inheritanceOperation`; do not rerun a merge or project values to recover them.
|
||||
4. Keep sequence validation, diagnostic canonicalization, location assignment,
|
||||
replacement order, and no-parent cloning behavior at their established
|
||||
semantic points. Keep child identity absent from inherited-parent history but
|
||||
present with its own origin in the effective child.
|
||||
|
||||
**Finish:** merge callbacks no longer mutate the executor accumulator. The
|
||||
resolved template and all evidence travel together for Stage 16 integration.
|
||||
|
||||
Progress notes (2026-10-25): added `inheritanceTemplateResult` and moved the
|
||||
parent/child merge and evidence construction into `combineInheritanceTemplate`.
|
||||
Callbacks now update the local origins, policies, and replacement evidence;
|
||||
sequence-origin handling returns replacement records to the template result.
|
||||
Parentless templates use the same result shape. Engine and facade inheritance
|
||||
tests passed.
|
||||
|
||||
## Stage 16 — Finish The Inheritance Executor And Accounting (Complete)
|
||||
|
||||
**Owner:** engine `inheritance_schedule.go` and focused finalization helpers.
|
||||
|
||||
1. Reduce the executor to preparation, budget precheck, combination, accumulation,
|
||||
and final rebuild. Append each template's control before its transfer/history
|
||||
in the same order as before, and update the indexed resolved value/origin/
|
||||
policy/template state once per template.
|
||||
2. Extract retained-history charging for one template's returned operations and
|
||||
replacements, reusing the same `retainedNodeCounter` across the scope as
|
||||
before. Preserve pointer-based shared-node accounting, lower-bound checks,
|
||||
scope-slot overhead, final expansion check, limits, and diagnostics.
|
||||
3. Extract final rebuilding and scope policy/origin projection where it clarifies
|
||||
the coordinator. Reuse `newInheritancePolicyIndex` once for scope localization;
|
||||
do not return to scanning all policy entries for every slot.
|
||||
4. Confirm both adapters and nested walking consume the same output. Retain
|
||||
source-version movement, sensitivity, independent snapshots, assembled order,
|
||||
cancellation, and policy retirement on actual replacement.
|
||||
|
||||
**Finish:** one readable executor coordinates explicit value/policy/evidence
|
||||
handoffs, with no parallel inheritance implementation. Run I.
|
||||
|
||||
Progress notes (2026-10-25): extracted per-template retained-history charging and
|
||||
scope rebuild/origin/policy projection into `inheritance_finalize.go`. The shared
|
||||
counter, budget checks, final expansion check, and one policy index remain intact.
|
||||
Both mapping and identified-sequence adapters still use the shared executor.
|
||||
Validation group I passed.
|
||||
|
||||
## Stage 17 — Extract Substantial Projection Handlers (Complete)
|
||||
|
||||
**Owners:** engine `projection.go`, optionally new `projection_collection.go`.
|
||||
|
||||
1. Keep `projectConfiguration`'s schema/type guard and one `valueOperation`.
|
||||
Keep `project`'s depth check, pointer/dynamic dispatch, and scalar conversion
|
||||
choices visible; delegate structs, maps, and sequences to named methods.
|
||||
2. Move struct member/inline flattening, collision checks, mapping-key charging,
|
||||
and flattened-field/inline-map secret classification into `projectStruct`.
|
||||
Preserve declaration order and the current decrement for a flattened
|
||||
container that is not retained in the output.
|
||||
3. Move deterministic sorted-map traversal into `projectMap`; keep its cycle
|
||||
entry active until all recursive children complete. Move slice/array
|
||||
construction into `projectSequence`, preserving nil-slice handling and the
|
||||
slice-only cycle guard.
|
||||
4. All recursive children call the same operation's dispatcher. Avoid duplicate
|
||||
node charging/depth increments or early cycle release introduced by extraction.
|
||||
Preserve float/text/duration/secret scalar handling and error paths.
|
||||
|
||||
**Finish:** dispatch and substantial handlers are separately readable, using the
|
||||
original operation state. Do not refactor `clone`, `decodeValue`, or
|
||||
`valueBuilder.build` as an additional cleanup project.
|
||||
|
||||
Progress notes (2026-10-25): delegated struct, map, and sequence projection to
|
||||
methods on the existing `valueOperation`. The dispatcher retains depth checks,
|
||||
pointer/dynamic handling, and scalar conversion; struct order, map sorting,
|
||||
flattened-node charging, nil slices, and cycle guards remain in their original
|
||||
paths. Validation group R passed.
|
||||
|
||||
## Stage 18 — Separate Schema Field Discovery And Assembly (Complete)
|
||||
|
||||
**Owner:** engine `schema.go`, optionally new `schema_fields.go`.
|
||||
|
||||
1. Keep `describeFields` as declaration-order enumeration, exported/ignored/tag
|
||||
decisions, and dispatch to ordinary or inline handling.
|
||||
2. Extract inline pointer-chain inspection and inline-struct/map assembly,
|
||||
retaining recursion/building-state checks and the existing codec-versus-
|
||||
structural distinction. Reuse the one builder/cache.
|
||||
3. Make ordinary registration and inline flattening visibly maintain both
|
||||
`members` and flattened `fields`/`fieldByName`/`inlineMap`. Preserve index
|
||||
paths, nested secret propagation, ambiguity paths, and the single-inline-map
|
||||
rule. Share registration helpers only for genuinely identical updates.
|
||||
4. Preserve descriptor reuse and the physical-member view used by typed cloning.
|
||||
Do not unify these views or add alternate field/tag policy. Existing schema,
|
||||
projection, decoding, and cloning expectations remain the behavior baseline.
|
||||
|
||||
Progress notes (2026-10-25): kept declaration-order enumeration, exported and
|
||||
ignored-field decisions, and tag parsing in `describeFields`; moved ordinary
|
||||
registration, inline pointer inspection, and inline struct/map assembly into
|
||||
focused helpers in `schema_fields.go`. Physical members remain distinct from
|
||||
flattened fields and inline-map metadata. Validation group R passed.
|
||||
|
||||
**Finish:** field discovery and descriptor assembly have explicit responsibilities
|
||||
without a new registration framework. Run R for both Stages 17–18.
|
||||
|
||||
## Stage 19 — Update Consumer And Internal Navigation (Complete)
|
||||
|
||||
**Owners:** `docs/consumers/README.md`, `docs/internal/overview.md`,
|
||||
`docs/internal/imports.md`, and affected links/test-owner entries in
|
||||
`docs/development.md`.
|
||||
|
||||
1. Add a concise consumer choice guide: ordinary `Load` for one selected source,
|
||||
explicit discovery as a source usable by either entry, and `LoadLayered` for
|
||||
underlay groups. Distinguish input-owned placement/import/filesystem choices
|
||||
from load-wide definition/default/version/override choices and independent
|
||||
layered overlay context. Link current API/config owners for exact rules.
|
||||
2. Link the existing simple README example, bundled-file/layered examples,
|
||||
import-focused prompt catalog, and discovered sequence catalog. Keep the
|
||||
README's simple path simple; do not duplicate complete examples or rewrite
|
||||
consumer domain rules to showcase new internal types.
|
||||
3. Update implementation ownership and flow for normalized requests, acquisition
|
||||
policy/state/tier lifetimes, resolved context versus live handles, import file
|
||||
owners, and explicit inheritance/projection/schema handoffs. Fix moved-symbol
|
||||
and behavioral-test navigation and stale comments affected by the refactor.
|
||||
4. Preserve public contract definitions and accepted ADR decisions. Keep deferred
|
||||
clone work in `future.md`. No policy change or new ADR is needed for the
|
||||
selected private refactor.
|
||||
|
||||
Progress notes (2026-10-25): added a consumer entry-point and ownership guide,
|
||||
linked the existing adoption examples, and documented normalized requests,
|
||||
acquisition/context lifetimes, import owners, and inheritance/projection/schema
|
||||
handoffs. Updated test navigation and retained clone review as deferred.
|
||||
Documentation links and repository boundaries passed.
|
||||
|
||||
**Finish:** current documentation describes implemented structure, with the
|
||||
roadmap/plan retaining future work and progress until release cleanup.
|
||||
|
||||
## Stage 20 — Verify And Close The Combined Sprint (Complete)
|
||||
|
||||
1. Search for obsolete import/input forwarding ladders, synthetic `Options`
|
||||
acquisition construction, `sourceVersionState.importDefaults`, discarded
|
||||
context derivation branches, unused inverse bridges, and temporary scaffolds.
|
||||
Remove leftovers along with exclusive test scaffolding. Preserve distinct
|
||||
operations and real diagnostic conversions; do not delete private code solely
|
||||
because it resembles another helper.
|
||||
2. Review the combined call flow: one normalization model, non-generic acquisition,
|
||||
load versus tier versus traversal state, library-owned acquisition handles
|
||||
closed before composition/publication, one combination outcome for inheritance
|
||||
evidence, and one state owner per reflective operation. Review for added whole-tree
|
||||
traversals, policy scans, or ownership copies without requiring measurements.
|
||||
3. Account for every selected finding using the table below. Confirm any new
|
||||
handoff coverage addresses a concrete gap. Existing correctness/algorithmic
|
||||
regressions, public-boundary tests, and meaningful assertions stay effective.
|
||||
4. Run the Final validation group, resolve failures, inspect the staged diff and
|
||||
workspace, and commit the completed remaining work with plain-English
|
||||
messages. Report changes, checks, limitations, and any unrelated work retained.
|
||||
Do not tag, push, publish, or update consumer repositories in this sprint.
|
||||
|
||||
Progress notes (2026-10-25): reviewed the combined request/acquisition/tier and
|
||||
traversal flow, the inheritance result handoff, and the shared reflection state
|
||||
owners. The named obsolete source-version field, synthetic acquisition-options
|
||||
construction, and inverse conversion symbols are absent; no additional cleanup
|
||||
was needed. The final development check and race-enabled full test suite passed.
|
||||
|
||||
| Audit finding | Completion evidence |
|
||||
| --- | --- |
|
||||
| 1: imports | Stages 3–6 and 10–13 remove options reconstruction/default coupling and forwarding ladders; acquisition/scope/evidence owners and admission operations are clear. A and final checks pass. |
|
||||
| 2: inheritance | Stages 14–16 return explicit template results, keep one executor, and preserve accounting and value/history/identity agreement. I and final checks pass. |
|
||||
| 3: projection | Stage 17 extracts substantial handlers sharing operation state. R and final checks pass. Clone review remains deferred, not implicitly completed. |
|
||||
| 4: schema | Stage 18 clarifies ordinary/inline assembly while preserving both descriptor views. R and final checks pass. |
|
||||
| 5: contexts | Stages 7–9 share resolved facts across input/import/overlay/passive contexts without retaining handles or changing validation timing. A and final checks pass. |
|
||||
| 6: facade | Stage 2 removes unused inverse conversions/adapters and shares real conversion. F and final boundary/consumer checks pass. |
|
||||
|
||||
**Finish:** the selected roadmap target is implemented, validated, documented,
|
||||
and reviewable as one complete sprint. No unresolved product/scope decisions are
|
||||
needed to begin these stages.
|
||||
@@ -1,77 +0,0 @@
|
||||
# Consolidation Review Findings
|
||||
|
||||
Reviewed revision: `557e006a4ab98b666735a71b3845418c702f7cec`.
|
||||
Comparison baseline: `8ab1568` (the committed implementation plan).
|
||||
|
||||
The review compared the [consolidation target](consolidation.md) and
|
||||
[implementation plan](implementation.md) with the completed sprint, including
|
||||
public/private conversions, preflight and acquisition, import selection and
|
||||
evidence, filesystem contexts, inheritance, projection, schema construction,
|
||||
changed tests, and documentation. The development check (`./scripts/check.sh`)
|
||||
and full race suite (`go test -race ./...`) passed with Go 1.26.10 on Linux.
|
||||
|
||||
## 1. Complete The Non-Import Resolved-Context Handoff
|
||||
|
||||
Status: open. Priority: medium. Category: implementation gap and maintainability.
|
||||
No user-facing correctness regression has been established for this finding.
|
||||
|
||||
**Target.** Roadmap internal target 2 and implementation Stages 7–9 select one
|
||||
resolved filesystem description shared by validation/acquisition and passive
|
||||
context construction. Stage 8 explicitly requires populating the description
|
||||
at the existing non-import validation point and resolving only facts that are
|
||||
still missing later.
|
||||
|
||||
**Evidence.** In [acquisition.go](../../internal/engine/acquisition.go),
|
||||
`acquireInput` calls `validateFilesystemContext` at line 181 for inputs without
|
||||
import traversal and with an applicable explicit context. That validator returns
|
||||
only a diagnostic. Its host/native branches derive a resolved description but
|
||||
discard it. At lines 220–226, acquisition sees `input.resolvedFilesystem == nil`
|
||||
and calls `resolveFilesystemContext` again before building passive views.
|
||||
|
||||
For example, an ordinary `Load` of a byte or mapping source with explicit host
|
||||
`FilesystemContext` and imports disabled resolves/canonicalizes the base and
|
||||
root during validation, then derives those same facts again for its source
|
||||
context. The retained description therefore does not complete the intended
|
||||
validation-to-context handoff.
|
||||
|
||||
Additionally, [filesystem.go](../../internal/engine/filesystem.go) still contains
|
||||
`validateAbsentHostFilesystemContext` (line 40), with its own root/base
|
||||
canonicalization, root-defaulting, and containment logic. That derivation
|
||||
duplicates `resolveAbsentFilesystemContext` in
|
||||
[filesystem_context.go](../../internal/engine/filesystem_context.go) (line 29).
|
||||
The two paths already maintain separate containment-diagnostic messages. Graph
|
||||
caller inspection confirms that the old validator is reachable from
|
||||
`acquireInput`; it is not unused migration scaffolding.
|
||||
|
||||
**Why address it.** The new model exists, but one important path still discards
|
||||
and reconstructs its results, while absent-host context derivation has two
|
||||
production implementations. This leaves part of audit finding 5 unresolved and
|
||||
preserves the drift risk the consolidation was intended to remove. This is a
|
||||
structural completion issue, not a claim of measured performance loss or a new
|
||||
single-lookup guarantee.
|
||||
|
||||
**Required revision.**
|
||||
|
||||
- Have non-import context resolution/validation return or populate the resolved
|
||||
description, and retain it on `assembledInput` at the existing validation
|
||||
point. Later passive-view construction and ordinary-overlay handling should
|
||||
reuse those applicable facts.
|
||||
- Route absent-host derivation through the common resolved-context owner and
|
||||
remove the duplicate derivation from the old validator. Keep the existing
|
||||
directory/access checks and close-error handling at their appropriate points.
|
||||
- Preserve deferred discovery/ordinary-overlay checks, root-only absent host
|
||||
context, host/native distinctions, diagnostics, and borrowed filesystem
|
||||
ownership. Independently required validation must not disappear merely to
|
||||
reduce filesystem calls.
|
||||
- Keep internal documentation aligned with the completed handoff. Describe
|
||||
aggregate accounting as load-wide and repetition/namespace state as
|
||||
tier-scoped; the new internal overview/import prose currently calls the latter
|
||||
per-tier accounting despite its distinct role.
|
||||
|
||||
**Verification.** Review the non-import call path to confirm that applicable
|
||||
resolved facts are carried forward and absent-host derivation has one owner.
|
||||
Reuse the existing host/native, discovery selected/absent, ordinary/layered
|
||||
overlay, and passive-resource-context tests. Add a focused behavioral regression
|
||||
only for a consequential gap exposed by the change; do not assert incidental
|
||||
canonicalization/stat call counts. Run the development check and applicable
|
||||
ownership race checks after remediation.
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func ExampleLoad() {
|
||||
@@ -26,7 +26,7 @@ func ExampleLoad_sequenceDeclaration() {
|
||||
type Config struct {
|
||||
Items []Item `yaml:"items"`
|
||||
}
|
||||
result, err := configkit.Load[Config](configkit.Bytes("catalog.yml", []byte(`configkit:
|
||||
result, err := configkit.Load[Config](configkit.Bytes("catalog.yml", []byte(`config:
|
||||
sequences:
|
||||
- destination: [items]
|
||||
key: id
|
||||
@@ -186,7 +186,8 @@ func ExampleOverride() {
|
||||
},
|
||||
Overrides: overrides,
|
||||
})
|
||||
fmt.Println(result.Config.Workers, result.Sources()[0].Label, err)
|
||||
source, _ := result.Source("source-1")
|
||||
fmt.Println(result.Config.Workers, source.Label, err)
|
||||
// Output: 0 --workers <nil>
|
||||
}
|
||||
|
||||
|
||||
26
examples/consumers/config_controls_test.go
Normal file
26
examples/consumers/config_controls_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package consumers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func Example_documentRootConfig() {
|
||||
type app struct {
|
||||
Config struct {
|
||||
Timeout string `yaml:"timeout"`
|
||||
} `yaml:"config"`
|
||||
}
|
||||
files := configkit.NewFilesystem("bundle", fstest.MapFS{
|
||||
"main.yml": {Data: []byte("config: {imports: [{path: settings.yml, destination: [config]}]}\n")},
|
||||
"settings.yml": {Data: []byte("timeout: 10s\n")},
|
||||
})
|
||||
result, err := configkit.Load[app](files.File("main.yml"), configkit.Options[app]{Imports: true})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(result.Config.Config.Timeout)
|
||||
// Output: 10s
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
// Artifact is a Narratio-shaped generated output declaration.
|
||||
@@ -167,7 +167,7 @@ func LoadNotariusPaths(path, invocationDir string, overrides ...configkit.Overri
|
||||
resolveNotariusReference(configuration, resolution, invocationDir)
|
||||
return nil
|
||||
},
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/sensitive" },
|
||||
Sensitive: func(path configkit.Path) bool { return path.String() == "/sensitive" },
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -177,7 +177,7 @@ func resolveNotariusReference(configuration *NotariusPathConfig, resolution *con
|
||||
if configuration.Reference == "" || filepath.IsAbs(configuration.Reference) {
|
||||
return
|
||||
}
|
||||
info, found := resolution.InputInfo(path)
|
||||
info, found := resolution.EntryInfo(path)
|
||||
if !found {
|
||||
resolution.Warning(path, "relative reference has no entry provenance")
|
||||
return
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func Example_narratio() {
|
||||
@@ -82,7 +82,7 @@ func Example_weatherStartup() {
|
||||
defer os.RemoveAll(directory)
|
||||
_ = os.WriteFile(filepath.Join(directory, blockedName), []byte("unused-example-value\n"), 0o600)
|
||||
_ = os.WriteFile(filepath.Join(directory, tokenName), []byte("disposable-example-credential\n"), 0o600)
|
||||
report, bootstrapErr := configkit.LoadSecretsDir(directory)
|
||||
report, bootstrapErr := configkit.LoadSecretsIntoEnvironment(directory)
|
||||
var reportOutput bytes.Buffer
|
||||
_, reportWriteErr := report.WriteTo(&reportOutput)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"io/fs"
|
||||
"path"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
// CollectionBundleConfig is a representative application-owned collection
|
||||
@@ -86,7 +86,7 @@ func resolveCollectionResources(configuration *CollectionBundleConfig, resolutio
|
||||
continue
|
||||
}
|
||||
contentPath := configkit.Keys("collections", collectionID, "prompts", promptID, "content_file")
|
||||
info, found := resolution.InputInfo(contentPath)
|
||||
info, found := resolution.EntryInfo(contentPath)
|
||||
if !found || len(info.Origin.Locations) != 1 {
|
||||
return fmt.Errorf("prompt resource has no single declaring source")
|
||||
}
|
||||
@@ -104,9 +104,9 @@ func resolveCollectionResources(configuration *CollectionBundleConfig, resolutio
|
||||
}
|
||||
prompt.ResourceText = string(data)
|
||||
collection.Prompts[promptID] = prompt
|
||||
resolution.Record(
|
||||
resolution.RecordNormalization(
|
||||
configkit.Keys("collections", collectionID, "prompts", promptID, "resource_text"),
|
||||
configkit.ChangeNormalization, "read source-relative prompt content", contentPath,
|
||||
"read source-relative prompt content", contentPath,
|
||||
)
|
||||
}
|
||||
configuration.Collections[collectionID] = collection
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func Example_inputDestination() {
|
||||
@@ -60,7 +60,7 @@ func TestCollectionBundleAssemblesMountedPeersAndResources(t *testing.T) {
|
||||
}
|
||||
|
||||
versionInfo, ok := result.Metadata().Lookup(configkit.Keys("version"))
|
||||
if !ok || !versionInfo.Explicit || versionInfo.Origin.Layer != configkit.LayerBase || len(versionInfo.Origin.Locations) != 1 {
|
||||
if !ok || !versionInfo.Explicit || versionInfo.Origin.Layer != configkit.LayerMain || len(versionInfo.Origin.Locations) != 1 {
|
||||
t.Fatalf("main-owned version metadata = %#v, %v", versionInfo, ok)
|
||||
}
|
||||
collectionVersionPath := configkit.Keys("collections", "weatherreporter", "version")
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
// FileComparisonConfig is a small application-owned shape for comparing
|
||||
@@ -101,8 +101,8 @@ func resolveRollout(configuration *RolloutConfig, resolution *configkit.Resoluti
|
||||
url := fmt.Sprintf("https://%s:%d", endpoint.Host, endpoint.Port)
|
||||
if endpoint.URL != url {
|
||||
endpoint.URL = url
|
||||
resolution.Record(configkit.Keys("service", "endpoint", "url"),
|
||||
configkit.ChangeNormalization, "derived endpoint URL",
|
||||
resolution.RecordNormalization(configkit.Keys("service", "endpoint", "url"),
|
||||
"derived endpoint URL",
|
||||
configkit.Keys("service", "endpoint", "host"),
|
||||
configkit.Keys("service", "endpoint", "port"))
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func ExampleCompareFiles() {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
type workingDirectoryPresetConfig struct {
|
||||
@@ -40,6 +40,7 @@ func Example_workingDirectoryPreset() {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(result.Config.Name, filepath.Base(result.Sources()[0].Path))
|
||||
source, _ := result.Source("source-1")
|
||||
fmt.Println(result.Config.Name, filepath.Base(source.Path))
|
||||
// Output: example service.yml
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package consumers
|
||||
|
||||
import configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
import configkit "go.maximumdirect.net/configkit"
|
||||
|
||||
// PresetCatalog is ordinary application data; no inheritance fields or
|
||||
// registration hooks are required in the typed configuration.
|
||||
|
||||
@@ -3,8 +3,8 @@ package consumers_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func TestPresetCatalogOneLoadAndDerivation(t *testing.T) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"io/fs"
|
||||
"path"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
//go:embed testdata/native-source testdata/layered-bundle testdata/collection-bundle testdata/ordinary-imports testdata/source-version
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func Example_nativeFilesystem() {
|
||||
@@ -26,7 +26,7 @@ func Example_rootRelativeImports() {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
source := result.Sources()[2]
|
||||
source, _ := result.Source("source-3")
|
||||
fmt.Println(result.Config.Name, result.Config.Version, result.Config.Prompts["brief"].Summary, source.Destination.String(), err)
|
||||
// Output: Root import example 1.0 A root-loaded prompt. /prompts <nil>
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func Example_sourceVersionAdoption() {
|
||||
}
|
||||
checks, redacted := 0, true
|
||||
for _, source := range result.Sources() {
|
||||
if check := source.VersionCheck; check != nil {
|
||||
if check := source.VersionEvidence; check != nil {
|
||||
checks++
|
||||
redacted = redacted && check.Redacted && check.Version.Text == ""
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func TestVersionedServiceModesAndMountedProtection(t *testing.T) {
|
||||
t.Fatalf("versioned service sources = %#v", sources)
|
||||
}
|
||||
for _, source := range sources {
|
||||
check := source.VersionCheck
|
||||
check := source.VersionEvidence
|
||||
if check == nil || check.Path.String() != "/version" || !check.Version.Present ||
|
||||
!check.Redacted || check.Version.Text != "" {
|
||||
t.Fatalf("safe source version evidence = %#v", sources)
|
||||
@@ -119,7 +119,7 @@ func TestCatalogRetainsPresenceAndSourceContext(t *testing.T) {
|
||||
}
|
||||
for _, key := range []string{"enabled", "workers"} {
|
||||
info, found := result.Metadata().Lookup(configkit.Keys(key))
|
||||
if !found || !info.Explicit || info.Origin.Layer != configkit.LayerBase {
|
||||
if !found || !info.Explicit || info.Origin.Layer != configkit.LayerMain {
|
||||
t.Fatalf("%s presence = %#v, %v", key, info, found)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package consumers
|
||||
import (
|
||||
"strings"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
// PromptCatalogConfig is an application-owned prompt collection shape.
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func Example_identifiedPromptCatalog() {
|
||||
@@ -24,8 +24,8 @@ func Example_identifiedPromptCatalog() {
|
||||
}
|
||||
emptySearches := 0
|
||||
for _, source := range result.Sources() {
|
||||
for _, selection := range source.ImportSelections {
|
||||
if selection.Selector == configkit.ImportSelectorDirectory && selection.Outcome == configkit.ImportSelectionNone {
|
||||
for _, selection := range source.EmptyImportSelections {
|
||||
if selection.Selector == configkit.ImportSelectorDirectory {
|
||||
emptySearches++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
// SequenceCatalogConfig is a source-backed prompt catalog with an application
|
||||
@@ -43,7 +43,7 @@ func LoadSequenceCatalog(mainPath, overlayName, runLabel string) (*configkit.Res
|
||||
filesystem := configkit.NewFilesystem("sequence catalog", os.DirFS(root))
|
||||
filesystemContext := &configkit.FilesystemContext{BaseDir: ".", Root: ".", FS: &filesystem}
|
||||
mainSearch := configkit.SearchConfig("", configkit.Searches(
|
||||
configkit.Paths(absoluteMain),
|
||||
configkit.SearchPaths(absoluteMain),
|
||||
configkit.SearchDirectory(filepath.Join(root, "fallback"), configkit.DirectorySearchOptions{
|
||||
Filenames: []configkit.FilenameGroup{configkit.CustomFilenames(filepath.Base(absoluteMain))},
|
||||
}),
|
||||
@@ -54,7 +54,7 @@ func LoadSequenceCatalog(mainPath, overlayName, runLabel string) (*configkit.Res
|
||||
},
|
||||
Resolve: func(configuration *SequenceCatalogConfig, resolution *configkit.Resolution) error {
|
||||
configuration.ResolvedLabel = "for:" + configuration.RunLabel
|
||||
resolution.Record(configkit.Keys("resolved_label"), configkit.ChangeNormalization,
|
||||
resolution.RecordNormalization(configkit.Keys("resolved_label"),
|
||||
"derive the resolved label from the selected run label", configkit.Keys("run_label"))
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func Example_sequenceCatalog() {
|
||||
@@ -171,7 +171,7 @@ collections:
|
||||
|
||||
func Example_yamlDeclaredIdentityOnly() {
|
||||
result, err := configkit.Load(configkit.Bytes("YAML identity only", []byte(`
|
||||
configkit:
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
@@ -193,7 +193,7 @@ prompts:
|
||||
|
||||
func TestYAMLDeclaredIdentityDoesNotRegisterFinalTypedConstraint(t *testing.T) {
|
||||
result, err := configkit.Load(configkit.Bytes("YAML identity only", []byte(`
|
||||
configkit:
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
// VersionedServiceConfig demonstrates a mounted main input whose source-local
|
||||
@@ -47,7 +47,7 @@ func LoadVersionedService(mode configkit.SourceVersionMode, overrides ...configk
|
||||
return nil
|
||||
},
|
||||
},
|
||||
Secret: func(path configkit.Path) bool {
|
||||
Sensitive: func(path configkit.Path) bool {
|
||||
return path.String() == "/service/version"
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: metadata.yml
|
||||
destination: []
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: version.yml
|
||||
description: Imported collection metadata.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: compact.yml
|
||||
- path: weather.yml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: brief.yml
|
||||
- path: custom.yml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: ../shared.yml
|
||||
enabled: true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: artifacts.yml
|
||||
poll_interval: 1m
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: declarations/path.yml
|
||||
sensitive: PATH_REFERENCE_SECRET_CANARY
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: metadata.yml
|
||||
- path: prompts/brief.yml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: helpers/curated-id.yml
|
||||
name: Curated summary
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: prompts/required.yml
|
||||
required: true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
run_label: source-default
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- dir: fragments
|
||||
pattern: '*.yml'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,7 +23,7 @@ var (
|
||||
|
||||
func TestFacadeValuesRemainRootOwned(t *testing.T) {
|
||||
t.Parallel()
|
||||
const packagePath = "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
const packagePath = "go.maximumdirect.net/configkit"
|
||||
for _, candidate := range []reflect.Type{
|
||||
reflect.TypeFor[configkit.Path](), reflect.TypeFor[configkit.Source](),
|
||||
reflect.TypeFor[configkit.Secret](), reflect.TypeFor[configkit.Diagnostic](),
|
||||
@@ -140,7 +140,7 @@ func TestFacadeSecretsBootstrapUsesFacadeRecords(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
report, err := configkit.LoadSecretsDir(directory)
|
||||
report, err := configkit.LoadSecretsIntoEnvironment(directory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -164,7 +164,7 @@ func TestFacadeSecretsBootstrapUsesFacadeRecords(t *testing.T) {
|
||||
t.Fatalf("report rendering = %q, %v", rendered.String(), err)
|
||||
}
|
||||
|
||||
failedReport, err := configkit.LoadSecretsDir("")
|
||||
failedReport, err := configkit.LoadSecretsIntoEnvironment("")
|
||||
var secretsError *configkit.SecretsError
|
||||
if failedReport == nil || !errors.As(err, &secretsError) || len(secretsError.Diagnostics()) != 1 || secretsError.Diagnostics()[0].Code != configkit.CodeSecretsInvalidPath {
|
||||
t.Fatalf("bootstrap failure = %#v / %#v", failedReport, err)
|
||||
@@ -203,7 +203,7 @@ func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
|
||||
if !resolution.IsExplicit(configkit.Keys("input")) {
|
||||
resolution.Error(configkit.Keys("input"), "input is required")
|
||||
}
|
||||
input, ok := resolution.InputInfo(configkit.Keys("input"))
|
||||
input, ok := resolution.EntryInfo(configkit.Keys("input"))
|
||||
if !ok || !input.Explicit {
|
||||
resolution.Error(configkit.Keys("input"), "missing input provenance")
|
||||
}
|
||||
@@ -245,6 +245,8 @@ func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
|
||||
}
|
||||
for _, context := range []func(){
|
||||
func() { retainedResolution.IsExplicit(configkit.Keys("input")) },
|
||||
func() { retainedResolution.RecordDerivedDefault(configkit.Keys("input"), "too late") },
|
||||
func() { retainedResolution.RecordNormalization(configkit.Keys("input"), "too late") },
|
||||
func() { retainedValidation.Warning(configkit.Keys("input"), "too late") },
|
||||
} {
|
||||
func() {
|
||||
|
||||
@@ -5,10 +5,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -27,23 +26,6 @@ type facadeCompatibilityConfig struct {
|
||||
func TestFacadeCompatibility(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const packagePath = "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
for _, candidate := range []struct {
|
||||
name string
|
||||
type_ reflect.Type
|
||||
}{
|
||||
{"Path", reflect.TypeFor[configkit.Path]()},
|
||||
{"Source", reflect.TypeFor[configkit.Source]()},
|
||||
{"Secret", reflect.TypeFor[configkit.Secret]()},
|
||||
{"Definition", reflect.TypeFor[configkit.Definition[facadeCompatibilityConfig]]()},
|
||||
{"Options", reflect.TypeFor[configkit.Options[facadeCompatibilityConfig]]()},
|
||||
{"Result", reflect.TypeFor[configkit.Result[facadeCompatibilityConfig]]()},
|
||||
} {
|
||||
if candidate.type_.PkgPath() != packagePath {
|
||||
t.Errorf("%s belongs to %q, want %q", candidate.name, candidate.type_.PkgPath(), packagePath)
|
||||
}
|
||||
}
|
||||
|
||||
constructed := configkit.Options[facadeCompatibilityConfig]{
|
||||
Definition: configkit.Definition[facadeCompatibilityConfig]{},
|
||||
Filesystem: &configkit.FilesystemContext{BaseDir: "configs", Root: "configs"},
|
||||
@@ -56,11 +38,6 @@ func TestFacadeCompatibility(t *testing.T) {
|
||||
if got := constructed.Overrides[0].Path.String(); got != "/value" {
|
||||
t.Fatalf("constructed override path = %q, want /value", got)
|
||||
}
|
||||
publicResult := configkit.Result[facadeCompatibilityConfig]{Config: facadeCompatibilityConfig{Value: "constructed"}}
|
||||
if publicResult.Config.Value != "constructed" {
|
||||
t.Fatal("constructed result lost its public configuration")
|
||||
}
|
||||
|
||||
result, err := configkit.Load(configkit.Bytes("facade.yml", []byte("value: input\n")), configkit.Options[facadeCompatibilityConfig]{
|
||||
Definition: configkit.Definition[facadeCompatibilityConfig]{
|
||||
Resolve: func(configuration *facadeCompatibilityConfig, resolution *configkit.Resolution) error {
|
||||
|
||||
108
facade_config_directives_test.go
Normal file
108
facade_config_directives_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package configkit_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestDocumentRootControlsAndNestedApplicationConfig(t *testing.T) {
|
||||
type settings struct {
|
||||
Imports string `yaml:"imports"`
|
||||
}
|
||||
type app struct {
|
||||
Service struct {
|
||||
Config settings `yaml:"config"`
|
||||
} `yaml:"service"`
|
||||
}
|
||||
result, err := configkit.Load[app](configkit.Bytes("main", []byte("config: {sequences: []}\nservice: {config: {imports: ordinary}}\n")))
|
||||
if err != nil || result.Config.Service.Config.Imports != "ordinary" {
|
||||
t.Fatalf("result = %#v, error = %v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountedConfigDataKeepsControlEvidenceAndHiding(t *testing.T) {
|
||||
type settings struct {
|
||||
Imports string `yaml:"imports"`
|
||||
Token string `yaml:"token" configkit:"secret"`
|
||||
}
|
||||
type app struct {
|
||||
Config settings `yaml:"config"`
|
||||
}
|
||||
fs := configkit.NewFilesystem("bundle", fstest.MapFS{
|
||||
"main.yml": {Data: []byte("config: {imports: [{path: missing.yml}, {path: data.yml, destination: [config]}]}\n")},
|
||||
"data.yml": {Data: []byte("imports: ordinary\ntoken: disposable-canary\n")},
|
||||
})
|
||||
result, err := configkit.Load[app](fs.File("main.yml"), configkit.Options[app]{Imports: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Config.Config.Imports != "ordinary" {
|
||||
t.Fatalf("config = %#v", result.Config)
|
||||
}
|
||||
sources := result.Sources()
|
||||
if len(sources) != 2 || len(sources[0].EmptyImportSelections) != 1 || sources[0].EmptyImportSelections[0].Path.String() != "/config/imports/0" {
|
||||
t.Fatalf("sources = %#v", sources)
|
||||
}
|
||||
if _, ok := result.Metadata().Lookup(configkit.Keys("config", "imports")); !ok {
|
||||
t.Fatal("missing application value")
|
||||
}
|
||||
if _, ok := result.Explain(configkit.Keys("config", "imports")); !ok {
|
||||
t.Fatal("missing explanation")
|
||||
}
|
||||
var output strings.Builder
|
||||
if err := result.WriteEffective(&output); err != nil || strings.Contains(output.String(), "disposable-canary") || !strings.Contains(output.String(), "ordinary") {
|
||||
t.Fatalf("effective output = %q, %v", output.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOldControlSpellingsAreOrdinaryData(t *testing.T) {
|
||||
body := "configkit: {imports: [{path: nonexistent.yml}]}\n_configkit: ordinary\n"
|
||||
result, err := configkit.Load[map[string]any](configkit.Bytes("old", []byte(body)))
|
||||
if err != nil || result.Config["configkit"] == nil || result.Config["_configkit"] != "ordinary" {
|
||||
t.Fatalf("result = %#v, error = %v", result, err)
|
||||
}
|
||||
if _, err := configkit.Load[struct{}](configkit.Bytes("strict", []byte(body))); err == nil {
|
||||
t.Fatal("strict schema accepted unknown data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultsOverridesAndResolutionAtConfigRoot(t *testing.T) {
|
||||
type settings struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Count int `yaml:"count"`
|
||||
}
|
||||
type app struct {
|
||||
Config settings `yaml:"config"`
|
||||
}
|
||||
entryExplicit := false
|
||||
entryCountExplicit := false
|
||||
result, err := configkit.Load[app](configkit.NoSource(), configkit.Options[app]{
|
||||
Definition: configkit.Definition[app]{
|
||||
Defaults: func() app { return app{Config: settings{Enabled: true, Count: 2}} },
|
||||
Resolve: func(value *app, r *configkit.Resolution) error {
|
||||
info, ok := r.EntryInfo(configkit.Keys("config", "enabled"))
|
||||
entryExplicit = ok && info.Explicit && info.Origin.Layer == configkit.LayerCLI
|
||||
entryCountExplicit = r.IsExplicit(configkit.Keys("config", "count"))
|
||||
value.Config.Count = 7
|
||||
r.RecordNormalization(configkit.Keys("config", "count"), "normalize count")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
Overrides: []configkit.Override{{Path: configkit.Keys("config", "enabled"), Value: false, Flag: "--enabled"}},
|
||||
})
|
||||
if err != nil || result == nil || !entryExplicit || entryCountExplicit || result.Config.Config.Enabled || result.Config.Config.Count != 7 {
|
||||
t.Fatalf("result = %#v, entry explicit = %t, error = %v", result, entryExplicit, err)
|
||||
}
|
||||
if info, ok := result.Metadata().Lookup(configkit.Keys("config", "enabled")); !ok || !info.Explicit || info.Origin.Layer != configkit.LayerCLI {
|
||||
t.Fatalf("final enabled info = %#v, %t", info, ok)
|
||||
}
|
||||
if info, ok := result.Metadata().Lookup(configkit.Keys("config", "count")); !ok || info.Origin.Layer != configkit.LayerResolution {
|
||||
t.Fatalf("final count info = %#v, %t", info, ok)
|
||||
}
|
||||
if _, ok := result.Explain(configkit.Keys("config", "count")); !ok {
|
||||
t.Fatal("missing resolution explanation")
|
||||
}
|
||||
}
|
||||
41
facade_container_origin_test.go
Normal file
41
facade_container_origin_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package configkit_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestOverlayExplanationUsesQueryScopedOrigin(t *testing.T) {
|
||||
t.Parallel()
|
||||
filesystem := configkit.NewFilesystem("origin", fstest.MapFS{
|
||||
"main.yml": {Data: []byte("a: base\ngroup: {x: X, y: Y}\n")},
|
||||
"overlays/dev.yml": {Data: []byte("a: overlay\ngroup: {x: O}\n")},
|
||||
})
|
||||
result, err := configkit.Load[map[string]any](filesystem.File("main.yml"), configkit.Options[map[string]any]{Overlay: "dev"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
path configkit.Path
|
||||
line, column int
|
||||
}{
|
||||
{configkit.Keys(), 1, 1},
|
||||
{configkit.Keys("group"), 2, 8},
|
||||
{configkit.Keys("group", "x"), 2, 12},
|
||||
} {
|
||||
explanation, found := result.Explain(test.path)
|
||||
if !found || len(explanation.History) == 0 {
|
||||
t.Fatalf("missing explanation for %s", test.path)
|
||||
}
|
||||
origin := explanation.History[len(explanation.History)-1].Origin
|
||||
if origin.Layer != configkit.LayerOverlay || len(origin.Locations) != 1 {
|
||||
t.Fatalf("overlay origin for %s = %#v", test.path, origin)
|
||||
}
|
||||
location := origin.Locations[0]
|
||||
if location.SourceID != "source-2" || location.Line != test.line || location.Column != test.column {
|
||||
t.Fatalf("overlay location for %s = %#v; want line %d column %d", test.path, location, test.line, test.column)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package configkit_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFacadeInactiveContextsPreserveClosedFailure(t *testing.T) {
|
||||
@@ -24,10 +24,11 @@ func TestFacadeInactiveContextsPreserveClosedFailure(t *testing.T) {
|
||||
call func()
|
||||
}{
|
||||
{"IsExplicit", func() { state.resolution.IsExplicit(path) }},
|
||||
{"InputInfo", func() { state.resolution.InputInfo(path) }},
|
||||
{"EntryInfo", func() { state.resolution.EntryInfo(path) }},
|
||||
{"Source", func() { state.resolution.Source("source") }},
|
||||
{"SourceContext", func() { state.resolution.SourceContext("source") }},
|
||||
{"Record", func() { state.resolution.Record(path, configkit.ChangeNormalization, "reason") }},
|
||||
{"RecordDerivedDefault", func() { state.resolution.RecordDerivedDefault(path, "reason") }},
|
||||
{"RecordNormalization", func() { state.resolution.RecordNormalization(path, "reason") }},
|
||||
{"Resolution.Error", func() { state.resolution.Error(path, "message") }},
|
||||
{"Resolution.Warning", func() { state.resolution.Warning(path, "message") }},
|
||||
{"Default", func() { configkit.Default(state.resolution, path, &target, 1, "reason") }},
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFacadeInputDestinationsAcrossSourceKinds(t *testing.T) {
|
||||
@@ -109,7 +109,7 @@ func TestFacadeDestinationRootEmptyMappingNoSourceAndInvalidPaths(t *testing.T)
|
||||
wantReads int
|
||||
}{
|
||||
{name: "index segment", source: configkit.Reader("unread", reader), destination: configkit.Keys("catalog").Index(0)},
|
||||
{name: "reserved configkit key", source: configkit.NoSource(), destination: configkit.Keys("configkit")},
|
||||
{name: "reserved extends key", source: configkit.NoSource(), destination: configkit.Keys("extends")},
|
||||
{name: "reserved extends key", source: configkit.NoSource(), destination: configkit.Keys("catalog", "extends")},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@@ -129,7 +129,7 @@ func TestFacadeImportedSourcesRetainInputDestination(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := filepath.Join(directory, "main.yml")
|
||||
fragmentPath := filepath.Join(directory, "fragment.yml")
|
||||
if err := os.WriteFile(mainPath, []byte("configkit: {imports: [{path: fragment.yml}]}\nmain: present\n"), 0o600); err != nil {
|
||||
if err := os.WriteFile(mainPath, []byte("config: {imports: [{path: fragment.yml}]}\nmain: present\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(fragmentPath, []byte("imported: present\n"), 0o600); err != nil {
|
||||
@@ -169,7 +169,7 @@ func TestFacadeRelativeImportDestinationsForRootAndMountedLoads(t *testing.T) {
|
||||
return filename
|
||||
}
|
||||
|
||||
mainPath := write("main.yml", `configkit:
|
||||
mainPath := write("main.yml", `config:
|
||||
imports:
|
||||
- path: metadata.yml
|
||||
destination: [metadata]
|
||||
@@ -184,7 +184,7 @@ root: main
|
||||
write("metadata.yml", "name: imported\n")
|
||||
write("prompts/a.yml", "a: {name: alpha}\n")
|
||||
write("prompts/b.yml", "b: {name: beta}\n")
|
||||
write("entries/main.yml", `configkit:
|
||||
write("entries/main.yml", `config:
|
||||
imports:
|
||||
- path: nested/details.yml
|
||||
destination: [details]
|
||||
@@ -352,7 +352,7 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
|
||||
Resolve: func(configuration *map[string]any, resolution *configkit.Resolution) error {
|
||||
callbacks.resolve++
|
||||
resourcePath := configkit.Keys("catalog", "entries", "item", "resource")
|
||||
input, ok := resolution.InputInfo(resourcePath)
|
||||
input, ok := resolution.EntryInfo(resourcePath)
|
||||
if !ok || len(input.Origin.Locations) != 1 {
|
||||
return fmt.Errorf("mounted resource input has no single source origin")
|
||||
}
|
||||
@@ -370,11 +370,11 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
|
||||
}
|
||||
entry := facadeDestinationMap(t, facadeDestinationMap(t, facadeDestinationMap(t, *configuration, "catalog"), "entries"), "item")
|
||||
entry["resource_text"] = string(data)
|
||||
resolution.Record(configkit.Keys("catalog", "entries", "item", "resource_text"), configkit.ChangeNormalization, "read source-relative summary", resourcePath)
|
||||
resolution.RecordNormalization(configkit.Keys("catalog", "entries", "item", "resource_text"), "read source-relative summary", resourcePath)
|
||||
return nil
|
||||
},
|
||||
Validate: func(*map[string]any, *configkit.Validation) { callbacks.validate++ },
|
||||
Secret: func(candidate configkit.Path) bool { return candidate.String() == tokenPath.String() },
|
||||
Validate: func(*map[string]any, *configkit.Validation) { callbacks.validate++ },
|
||||
Sensitive: func(candidate configkit.Path) bool { return candidate.String() == tokenPath.String() },
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -405,7 +405,7 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
|
||||
if err != nil || string(data) != "source-relative summary" {
|
||||
t.Fatalf("completed relative resource = %q, %v", data, err)
|
||||
}
|
||||
if info, ok := result.Metadata().Lookup(titlePath); !ok || info.Origin.Layer != configkit.LayerBase {
|
||||
if info, ok := result.Metadata().Lookup(titlePath); !ok || info.Origin.Layer != configkit.LayerMain {
|
||||
t.Fatalf("mounted title metadata = %#v, %v", info, ok)
|
||||
}
|
||||
if history := result.Metadata().History(titlePath); len(history) < 2 {
|
||||
@@ -505,7 +505,7 @@ func TestFacadeRelativeImportFailuresRetainSourceRecordsAndCoordinates(t *testin
|
||||
}
|
||||
|
||||
t.Run("imported parse failure retains effective source destination", func(t *testing.T) {
|
||||
mainPath := write("parse/main.yml", "configkit:\n imports:\n - path: child.yml\n destination: [tenant]\n")
|
||||
mainPath := write("parse/main.yml", "config:\n imports:\n - path: child.yml\n destination: [tenant]\n")
|
||||
childPath := write("parse/child.yml", "count: [unterminated\n")
|
||||
_, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
||||
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
||||
@@ -525,8 +525,8 @@ func TestFacadeRelativeImportFailuresRetainSourceRecordsAndCoordinates(t *testin
|
||||
})
|
||||
|
||||
t.Run("directive failure stays source local", func(t *testing.T) {
|
||||
mainPath := write("directive/main.yml", "configkit:\n imports:\n - path: parent.yml\n destination: [tenant]\n")
|
||||
parentPath := write("directive/parent.yml", "configkit:\n imports:\n - path: details.yml\n destination: [3]\n")
|
||||
mainPath := write("directive/main.yml", "config:\n imports:\n - path: parent.yml\n destination: [tenant]\n")
|
||||
parentPath := write("directive/parent.yml", "config:\n imports:\n - path: details.yml\n destination: [3]\n")
|
||||
_, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
||||
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
||||
})
|
||||
@@ -535,7 +535,7 @@ func TestFacadeRelativeImportFailuresRetainSourceRecordsAndCoordinates(t *testin
|
||||
t.Fatalf("load error = %T %v", err, err)
|
||||
}
|
||||
diagnostics := loadError.Diagnostics()
|
||||
wantPath := configkit.Keys("configkit", "imports").Index(0).Key("destination").Index(0)
|
||||
wantPath := configkit.Keys("config", "imports").Index(0).Key("destination").Index(0)
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeImportInvalid || !reflect.DeepEqual(diagnostics[0].Path, wantPath) || diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "source-2" {
|
||||
t.Fatalf("import directive diagnostic = %#v", diagnostics)
|
||||
}
|
||||
@@ -546,7 +546,7 @@ func TestFacadeRelativeImportFailuresRetainSourceRecordsAndCoordinates(t *testin
|
||||
})
|
||||
|
||||
t.Run("composition failure reports effective path and all sources", func(t *testing.T) {
|
||||
mainPath := write("compose/main.yml", "configkit:\n imports:\n - path: left.yml\n destination: [tenant]\n - path: right.yml\n destination: [tenant]\n")
|
||||
mainPath := write("compose/main.yml", "config:\n imports:\n - path: left.yml\n destination: [tenant]\n - path: right.yml\n destination: [tenant]\n")
|
||||
write("compose/left.yml", "value: same\n")
|
||||
write("compose/right.yml", "value: same\n")
|
||||
_, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
||||
@@ -567,7 +567,7 @@ func TestFacadeRelativeImportFailuresRetainSourceRecordsAndCoordinates(t *testin
|
||||
})
|
||||
|
||||
t.Run("version failure reports effective path and imported source", func(t *testing.T) {
|
||||
mainPath := write("version/main.yml", "configkit:\n imports:\n - path: child.yml\n destination: [tenant]\n")
|
||||
mainPath := write("version/main.yml", "config:\n imports:\n - path: child.yml\n destination: [tenant]\n")
|
||||
childPath := write("version/child.yml", "version: forbidden\n")
|
||||
_, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
||||
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
||||
@@ -599,7 +599,7 @@ func TestFacadeRelativeImportFailuresRetainSourceRecordsAndCoordinates(t *testin
|
||||
} `yaml:"tenant"`
|
||||
} `yaml:"mount"`
|
||||
}
|
||||
mainPath := write("typed/main.yml", "configkit:\n imports:\n - path: child.yml\n destination: [tenant]\n")
|
||||
mainPath := write("typed/main.yml", "config:\n imports:\n - path: child.yml\n destination: [tenant]\n")
|
||||
childPath := write("typed/child.yml", "count: invalid\n")
|
||||
_, err := configkit.LoadLayered[configuration](configkit.Input{
|
||||
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
||||
|
||||
95
facade_diagnostic_order_test.go
Normal file
95
facade_diagnostic_order_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package configkit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFacadeCompositionConflictKeepsClaimantOrder(t *testing.T) {
|
||||
filesystem := configkit.NewFilesystem("bundle", fstest.MapFS{
|
||||
"a.yml": &fstest.MapFile{Data: []byte(`config:
|
||||
imports:
|
||||
- path: z.yml
|
||||
`)},
|
||||
"b.yml": &fstest.MapFile{Data: []byte("x: b\n")},
|
||||
"c.yml": &fstest.MapFile{Data: []byte("x: c\n")},
|
||||
"z.yml": &fstest.MapFile{Data: []byte("x: z\n")},
|
||||
})
|
||||
main := configkit.Mapping("main", map[string]any{
|
||||
"config": map[string]any{"imports": []any{
|
||||
map[string]any{"path": "a.yml"},
|
||||
map[string]any{"path": "b.yml"},
|
||||
map[string]any{"path": "c.yml"},
|
||||
}},
|
||||
})
|
||||
_, err := configkit.Load[map[string]any](main, configkit.Options[map[string]any]{
|
||||
Imports: true,
|
||||
Filesystem: &configkit.FilesystemContext{
|
||||
FS: &filesystem, Root: ".", BaseDir: ".",
|
||||
},
|
||||
})
|
||||
var loadError *configkit.LoadError
|
||||
if !errors.As(err, &loadError) {
|
||||
t.Fatalf("Load error = %v, want composition LoadError", err)
|
||||
}
|
||||
diagnostics := loadError.Diagnostics()
|
||||
if len(diagnostics) != 1 {
|
||||
t.Fatalf("diagnostics = %#v, want one composition conflict", diagnostics)
|
||||
}
|
||||
diagnostic := diagnostics[0]
|
||||
if diagnostic.Code != configkit.CodeComposeConflict || diagnostic.Path.String() != "/x" || diagnostic.Primary == nil {
|
||||
t.Fatalf("composition diagnostic = %#v", diagnostic)
|
||||
}
|
||||
|
||||
pathsByID := make(map[string]string)
|
||||
var acquisitionOrder []string
|
||||
for _, source := range loadError.Sources() {
|
||||
pathsByID[source.ID] = source.Path
|
||||
for _, name := range []string{"z.yml", "b.yml", "c.yml"} {
|
||||
if source.Path == name || strings.HasSuffix(source.Path, "/"+name) {
|
||||
acquisitionOrder = append(acquisitionOrder, strings.TrimSuffix(name, ".yml"))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Join(acquisitionOrder, ",") != "z,b,c" {
|
||||
t.Fatalf("claimant acquisition order = %v, want z,b,c", acquisitionOrder)
|
||||
}
|
||||
claimant := func(location configkit.Location) string {
|
||||
for _, name := range []string{"b.yml", "c.yml", "z.yml"} {
|
||||
if pathsByID[location.SourceID] == name || strings.HasSuffix(pathsByID[location.SourceID], "/"+name) {
|
||||
return strings.TrimSuffix(name, ".yml")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if got := claimant(*diagnostic.Primary); got != "b" {
|
||||
t.Fatalf("primary claimant = %q, want b (sources: %#v)", got, pathsByID)
|
||||
}
|
||||
if len(diagnostic.Related) != 2 || claimant(diagnostic.Related[0]) != "c" || claimant(diagnostic.Related[1]) != "z" {
|
||||
t.Fatalf("related claimants = %#v, want c then z", diagnostic.Related)
|
||||
}
|
||||
|
||||
var output bytes.Buffer
|
||||
if err := configkit.WriteDiagnostics(&output, diagnostics); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
positions := []int{
|
||||
strings.Index(output.String(), diagnostic.Primary.SourceID),
|
||||
strings.Index(output.String(), diagnostic.Related[0].SourceID),
|
||||
strings.Index(output.String(), diagnostic.Related[1].SourceID),
|
||||
}
|
||||
if positions[0] < 0 || positions[1] <= positions[0] || positions[2] <= positions[1] {
|
||||
t.Fatalf("rendered claimant order = %v in %q", positions, output.String())
|
||||
}
|
||||
|
||||
diagnostics[0].Related[0].SourceID = "mutated"
|
||||
if got := claimant(loadError.Diagnostics()[0].Related[0]); got != "c" {
|
||||
t.Fatalf("LoadError.Diagnostics retained caller mutation; first claimant = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
type facadeDiffConfig struct {
|
||||
@@ -29,10 +29,10 @@ func TestFacadeDiffKeepsPublicSequenceChangesWithHistoricalDescendants(t *testin
|
||||
t.Helper()
|
||||
result, err := configkit.Load(configkit.Bytes("historical-sequence.yml", []byte("items: [{secret: disposable-canary}, "+public+"]\n")),
|
||||
configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/items/0/secret" },
|
||||
Sensitive: func(path configkit.Path) bool { return path.String() == "/items/0/secret" },
|
||||
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
|
||||
current.Items[0] = "public scalar"
|
||||
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "normalize first item")
|
||||
resolution.RecordNormalization(configkit.Keys("items"), "normalize first item")
|
||||
return nil
|
||||
},
|
||||
}})
|
||||
@@ -71,10 +71,10 @@ func TestFacadeDiffKeepsPublicScalarAcrossHistoricalAbsence(t *testing.T) {
|
||||
Dynamic map[string]any `yaml:"dynamic"`
|
||||
}
|
||||
options := configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/dynamic/entry/token" },
|
||||
Sensitive: func(path configkit.Path) bool { return path.String() == "/dynamic/entry/token" },
|
||||
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
|
||||
delete(current.Dynamic, "entry")
|
||||
resolution.Record(configkit.Keys("dynamic", "entry"), configkit.ChangeNormalization, "remove entry")
|
||||
resolution.RecordNormalization(configkit.Keys("dynamic", "entry"), "remove entry")
|
||||
return nil
|
||||
},
|
||||
}}
|
||||
@@ -221,7 +221,7 @@ func TestFacadeDiffIgnoresSourceAndResolutionHistory(t *testing.T) {
|
||||
resolve := func(value string) configkit.Options[config] {
|
||||
return configkit.Options[config]{Definition: configkit.Definition[config]{Resolve: func(current *config, resolution *configkit.Resolution) error {
|
||||
current.Count = len(value) + 8
|
||||
resolution.Record(configkit.Keys("count"), configkit.ChangeNormalization, "set effective count")
|
||||
resolution.RecordNormalization(configkit.Keys("count"), "set effective count")
|
||||
return nil
|
||||
}}}
|
||||
}
|
||||
@@ -281,7 +281,7 @@ func TestFacadeDiffUsesFrozenFilesAndRunsCallbacksOnlyDuringLoad(t *testing.T) {
|
||||
|
||||
func TestFacadeDiffCombinesSecretSensitivityAndLoadErrors(t *testing.T) {
|
||||
t.Setenv("FACADE_DIFF_SECRET", "credential-one")
|
||||
secretOptions := configkit.Options[facadeDiffSecretConfig]{Definition: configkit.Definition[facadeDiffSecretConfig]{Secret: func(path configkit.Path) bool {
|
||||
secretOptions := configkit.Options[facadeDiffSecretConfig]{Definition: configkit.Definition[facadeDiffSecretConfig]{Sensitive: func(path configkit.Path) bool {
|
||||
return path.String() == "/secret"
|
||||
}}}
|
||||
left, err := configkit.Load[facadeDiffSecretConfig](configkit.Bytes("left.yml", []byte("secret: FACADE_DIFF_SECRET\n")), secretOptions)
|
||||
|
||||
@@ -93,14 +93,14 @@ func TestFacadeSearchOrderingRequirednessAndSnapshots(t *testing.T) {
|
||||
names := []string{"config.yml"}
|
||||
groups := []FilenameGroup{CustomFilenames(names...)}
|
||||
directoryOptions := []DirectorySearchOptions{{Filenames: groups}}
|
||||
search := Searches(Paths(paths...), SearchDirectory(root, directoryOptions...), Paths(laterPath))
|
||||
search := Searches(SearchPaths(paths...), SearchDirectory(root, directoryOptions...), SearchPaths(laterPath))
|
||||
source := SearchConfig("", search, SearchOptions{Required: true})
|
||||
|
||||
paths[0] = laterPath
|
||||
names[0] = "later.yml"
|
||||
groups[0] = CustomFilenames("later.yml")
|
||||
directoryOptions[0].Filenames[0] = CustomFilenames("later.yml")
|
||||
search = Paths(laterPath)
|
||||
search = SearchPaths(laterPath)
|
||||
|
||||
result, err := Load[facadeDiscoveryConfig](source)
|
||||
if err != nil || result.Config.Value != "selected" {
|
||||
@@ -110,7 +110,7 @@ func TestFacadeSearchOrderingRequirednessAndSnapshots(t *testing.T) {
|
||||
t.Fatalf("ordered search sources = %#v", sources)
|
||||
}
|
||||
|
||||
_, err = Load[facadeDiscoveryConfig](SearchConfig("", Paths(filepath.Join(root, "missing.yml")), SearchOptions{Required: true}))
|
||||
_, err = Load[facadeDiscoveryConfig](SearchConfig("", SearchPaths(filepath.Join(root, "missing.yml")), SearchOptions{Required: true}))
|
||||
var loadErr *LoadError
|
||||
if !errors.As(err, &loadErr) {
|
||||
t.Fatalf("required exhausted search error = %v, want *LoadError", err)
|
||||
@@ -131,7 +131,7 @@ func TestFacadeSearchWorksForLayeredMainAndUnderlay(t *testing.T) {
|
||||
mainPath := writeFacadeImportFile(t, root, "main.yml", "settings: {main: value}\n")
|
||||
underlayPath := writeFacadeImportFile(t, root, "underlay/base.yml", "settings: {underlay: value}\n")
|
||||
result, err := LoadLayered(
|
||||
Input{Source: SearchConfig("", Paths(mainPath))},
|
||||
Input{Source: SearchConfig("", SearchPaths(mainPath))},
|
||||
LayeredOptions[facadeDiscoveryConfig]{Underlays: []Underlay{{Inputs: []Input{{
|
||||
Source: SearchConfig("", SearchDirectory(filepath.Dir(underlayPath), DirectorySearchOptions{
|
||||
Filenames: []FilenameGroup{CustomFilenames("base.yml")},
|
||||
@@ -142,7 +142,7 @@ func TestFacadeSearchWorksForLayeredMainAndUnderlay(t *testing.T) {
|
||||
t.Fatalf("layered search = %#v, %v", result, err)
|
||||
}
|
||||
sources := result.Sources()
|
||||
if len(sources) != 2 || sources[0].Path != canonicalFacadePath(t, mainPath) || sources[0].Layer != LayerBase ||
|
||||
if len(sources) != 2 || sources[0].Path != canonicalFacadePath(t, mainPath) || sources[0].Layer != LayerMain ||
|
||||
sources[1].Path != canonicalFacadePath(t, underlayPath) || sources[1].Layer != LayerUnderlay {
|
||||
t.Fatalf("layered search sources = %#v", sources)
|
||||
}
|
||||
@@ -157,7 +157,7 @@ func TestFacadeSearchSelectedAndAbsentUseRootOnlyContext(t *testing.T) {
|
||||
filesystem := &FilesystemContext{Root: root}
|
||||
options := Options[facadeDiscoveryConfig]{Overlay: "testing", Filesystem: filesystem}
|
||||
|
||||
selected, err := Load(SearchConfig("", Paths(mainPath)), options)
|
||||
selected, err := Load(SearchConfig("", SearchPaths(mainPath)), options)
|
||||
if err != nil || selected.Config.Value != "overlay" {
|
||||
t.Fatalf("selected search with root-only context = %#v, %v", selected, err)
|
||||
}
|
||||
@@ -171,7 +171,7 @@ func TestFacadeSearchSelectedAndAbsentUseRootOnlyContext(t *testing.T) {
|
||||
t.Fatalf("selected file base context = %#v, %v", mainContext, ok)
|
||||
}
|
||||
|
||||
missing := SearchConfig("", Paths(filepath.Join(root, "missing.yml")))
|
||||
missing := SearchConfig("", SearchPaths(filepath.Join(root, "missing.yml")))
|
||||
searchedAbsent, err := Load(missing, options)
|
||||
if err != nil {
|
||||
t.Fatalf("exhausted search with root-only overlay context: %v", err)
|
||||
@@ -201,7 +201,7 @@ func TestFacadeSearchRejectsSelectedContextWithoutFallback(t *testing.T) {
|
||||
inside := writeFacadeImportFile(t, root, "inside.yml", "value: fallback\n")
|
||||
outsideRoot := t.TempDir()
|
||||
outside := writeFacadeImportFile(t, outsideRoot, "outside.yml", "value: selected\n")
|
||||
search := SearchConfig("", Paths(outside, inside))
|
||||
search := SearchConfig("", SearchPaths(outside, inside))
|
||||
|
||||
_, err := Load(search, Options[facadeDiscoveryConfig]{Filesystem: &FilesystemContext{Root: root}})
|
||||
var loadErr *LoadError
|
||||
@@ -228,7 +228,7 @@ func TestFacadeSearchRunsCallbacksOnceAndKeepsInspectionWithSelectedFile(t *test
|
||||
selectedPath := writeFacadeImportFile(t, root, "selected.yml", "value: selected\n")
|
||||
writeFacadeImportFile(t, root, "later.yml", "value: later\n")
|
||||
defaultsCalls, resolveCalls, validateCalls := 0, 0, 0
|
||||
result, err := Load(SearchConfig("", Paths(selectedPath, filepath.Join(root, "later.yml"))), Options[facadeDiscoveryLifecycleConfig]{
|
||||
result, err := Load(SearchConfig("", SearchPaths(selectedPath, filepath.Join(root, "later.yml"))), Options[facadeDiscoveryLifecycleConfig]{
|
||||
Definition: Definition[facadeDiscoveryLifecycleConfig]{
|
||||
Defaults: func() facadeDiscoveryLifecycleConfig {
|
||||
defaultsCalls++
|
||||
@@ -237,7 +237,7 @@ func TestFacadeSearchRunsCallbacksOnceAndKeepsInspectionWithSelectedFile(t *test
|
||||
Resolve: func(configuration *facadeDiscoveryLifecycleConfig, resolution *Resolution) error {
|
||||
resolveCalls++
|
||||
configuration.Resolved = "from:" + configuration.Value
|
||||
resolution.Record(Keys("resolved"), ChangeNormalization, "derive from selected value", Keys("value"))
|
||||
resolution.RecordNormalization(Keys("resolved"), "derive from selected value", Keys("value"))
|
||||
return nil
|
||||
},
|
||||
Validate: func(_ *facadeDiscoveryLifecycleConfig, _ *Validation) { validateCalls++ },
|
||||
@@ -272,7 +272,7 @@ func TestFacadeSearchSequenceItemAndAbsentInputPlacement(t *testing.T) {
|
||||
Destination: Keys("items"),
|
||||
PlacementMode: PlacementModeSequenceItem,
|
||||
}
|
||||
selected, err := Load(SearchConfig("", Paths(selectedPath)), options)
|
||||
selected, err := Load(SearchConfig("", SearchPaths(selectedPath)), options)
|
||||
if err != nil || len(selected.Config.Items) != 1 || selected.Config.Items[0].Name != "selected" {
|
||||
t.Fatalf("selected sequence item = %#v, %v", selected, err)
|
||||
}
|
||||
@@ -283,7 +283,7 @@ func TestFacadeSearchSequenceItemAndAbsentInputPlacement(t *testing.T) {
|
||||
defaults := func() facadeDiscoveryPlacementConfig {
|
||||
return facadeDiscoveryPlacementConfig{Items: []facadeDiscoveryPlacementItem{}}
|
||||
}
|
||||
absent, err := Load(SearchConfig("", Paths(filepath.Join(root, "missing.yml"))), Options[facadeDiscoveryPlacementConfig]{
|
||||
absent, err := Load(SearchConfig("", SearchPaths(filepath.Join(root, "missing.yml"))), Options[facadeDiscoveryPlacementConfig]{
|
||||
Definition: Definition[facadeDiscoveryPlacementConfig]{Defaults: defaults},
|
||||
Destination: options.Destination,
|
||||
PlacementMode: options.PlacementMode,
|
||||
|
||||
53
facade_dynamic_redaction_test.go
Normal file
53
facade_dynamic_redaction_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package configkit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFinalDynamicTagRedactsEqualValuedResolution(t *testing.T) {
|
||||
t.Parallel()
|
||||
type classified struct {
|
||||
Token string `yaml:"token" configkit:"secret"`
|
||||
}
|
||||
type configuration struct {
|
||||
Value any `yaml:"value"`
|
||||
}
|
||||
const canary = "DYNAMIC-TAG-CANARY"
|
||||
result, err := configkit.Load(configkit.Bytes("input", []byte("value: {token: "+canary+"}\n")), configkit.Options[configuration]{
|
||||
Definition: configkit.Definition[configuration]{
|
||||
Resolve: func(c *configuration, _ *configkit.Resolution) error {
|
||||
// No canonical value changes, so no normalization record is needed.
|
||||
c.Value = classified{Token: canary}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var effective bytes.Buffer
|
||||
if err := result.WriteEffective(&effective); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(effective.String(), canary) || !strings.Contains(effective.String(), "[REDACTED]") {
|
||||
t.Fatal("effective rendering did not preserve the final dynamic tag")
|
||||
}
|
||||
explanation, found := result.Explain(configkit.Keys("value", "token"))
|
||||
if !found || !explanation.Effective.Redacted || strings.Contains(explanation.Effective.YAML, canary) {
|
||||
t.Fatal("effective explanation did not preserve the final dynamic tag")
|
||||
}
|
||||
if len(explanation.History) == 0 {
|
||||
t.Fatal("source history is absent")
|
||||
}
|
||||
for _, change := range explanation.History {
|
||||
for _, value := range []configkit.ValueSnapshot{change.Before, change.After} {
|
||||
if value.Present && (!value.Redacted || strings.Contains(value.YAML, canary)) {
|
||||
t.Fatal("historical explanation did not preserve the final dynamic tag")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
type equivalentConfig struct {
|
||||
@@ -23,6 +23,9 @@ type equivalentConfig struct {
|
||||
|
||||
func TestEquivalentSourceFormsPreserveInputSemantics(t *testing.T) {
|
||||
const yaml = "version: 1\nname: retained\nenabled: false\ncount: 0\nitems: []\noptional: null\n"
|
||||
bytesData := []byte(yaml)
|
||||
bytesSource := configkit.Bytes("memory", bytesData)
|
||||
bytesData[0] = 'X'
|
||||
hostPath := filepath.Join(t.TempDir(), "main.yml")
|
||||
if err := os.WriteFile(hostPath, []byte(yaml), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -37,7 +40,7 @@ func TestEquivalentSourceFormsPreserveInputSemantics(t *testing.T) {
|
||||
}{
|
||||
{"host", configkit.File(hostPath), nil, configkit.SourceKindFile},
|
||||
{"native", namespace.File("main.yml"), nil, configkit.SourceKindFilesystem},
|
||||
{"bytes", configkit.Bytes("memory", []byte(yaml)), context, configkit.SourceKindBytes},
|
||||
{"bytes", bytesSource, context, configkit.SourceKindBytes},
|
||||
{"reader", configkit.Reader("stream", strings.NewReader(yaml)), context, configkit.SourceKindReader},
|
||||
{"mapping", configkit.Mapping("programmatic", map[string]any{
|
||||
"version": uint64(1), "name": "retained", "enabled": false, "count": 0,
|
||||
@@ -66,7 +69,7 @@ func TestEquivalentSourceFormsPreserveInputSemantics(t *testing.T) {
|
||||
}
|
||||
for _, field := range []string{"version", "name", "enabled", "count", "items", "optional"} {
|
||||
info, ok := result.Metadata().Lookup(configkit.Keys(field))
|
||||
if !ok || !info.Explicit || info.Origin.Layer != configkit.LayerBase || info.Null != (field == "optional") {
|
||||
if !ok || !info.Explicit || info.Origin.Layer != configkit.LayerMain || info.Null != (field == "optional") {
|
||||
t.Errorf("%s input info = %#v, %v", field, info, ok)
|
||||
}
|
||||
}
|
||||
@@ -84,8 +87,8 @@ func TestEquivalentSourceFormsPreserveInputSemantics(t *testing.T) {
|
||||
|
||||
func TestRepeatedImportsAcrossTiersAndReaderConsumption(t *testing.T) {
|
||||
namespace := configkit.NewFilesystem("bundle", fstest.MapFS{
|
||||
"left.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: shared.yml}]}\n")},
|
||||
"right.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: shared.yml}]}\n")},
|
||||
"left.yml": &fstest.MapFile{Data: []byte("config: {imports: [{path: shared.yml}]}\n")},
|
||||
"right.yml": &fstest.MapFile{Data: []byte("config: {imports: [{path: shared.yml}]}\n")},
|
||||
"shared.yml": &fstest.MapFile{Data: []byte("name: retained\n")},
|
||||
})
|
||||
type config struct {
|
||||
@@ -112,6 +115,15 @@ func TestRepeatedImportsAcrossTiersAndReaderConsumption(t *testing.T) {
|
||||
if got := layeredErrorCode(t, err); got != configkit.CodeSourceReaderUsed {
|
||||
t.Fatalf("reader re-consumption = %s", got)
|
||||
}
|
||||
copiedReader := configkit.Reader("copied-single-use", strings.NewReader("name: retained\n"))
|
||||
readerCopy := copiedReader
|
||||
if _, err := configkit.Load[config](readerCopy, configkit.Options[config]{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = configkit.Load[config](copiedReader, configkit.Options[config]{})
|
||||
if got := layeredErrorCode(t, err); got != configkit.CodeSourceReaderUsed {
|
||||
t.Fatalf("copied reader re-consumption = %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplacedSecretRemainsAbsentFromSafeOutput(t *testing.T) {
|
||||
@@ -120,7 +132,7 @@ func TestReplacedSecretRemainsAbsentFromSafeOutput(t *testing.T) {
|
||||
}
|
||||
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte("secret: new-secret-canary\n"))}, configkit.LayeredOptions[config]{
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{"secret": "old-secret-canary"})}}}},
|
||||
Definition: configkit.Definition[config]{Secret: func(path configkit.Path) bool { return path.String() == "/secret" }},
|
||||
Definition: configkit.Definition[config]{Sensitive: func(path configkit.Path) bool { return path.String() == "/secret" }},
|
||||
})
|
||||
if err != nil || result.Config.Secret != "new-secret-canary" {
|
||||
t.Fatalf("secret load = %#v, %v", result, err)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
//go:embed testdata/filesystem
|
||||
@@ -57,7 +57,7 @@ func TestFacadeFilesystemSourceImportsOverlayAndReuse(t *testing.T) {
|
||||
|
||||
func TestFacadeFilesystemContextsAndIdentity(t *testing.T) {
|
||||
first := configkit.NewFilesystem("same", fstest.MapFS{
|
||||
"root/config/main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: ../shared.yml}]}\n")},
|
||||
"root/config/main.yml": &fstest.MapFile{Data: []byte("config: {imports: [{path: ../shared.yml}]}\n")},
|
||||
"root/shared.yml": &fstest.MapFile{Data: []byte("items: {value: first}\n")},
|
||||
"root/overlays/testing.yml": &fstest.MapFile{Data: []byte("items: {overlay: first}\n")},
|
||||
})
|
||||
@@ -82,8 +82,8 @@ func TestFacadeFilesystemContextsAndIdentity(t *testing.T) {
|
||||
source configkit.Source
|
||||
kind configkit.SourceKind
|
||||
}{
|
||||
{"bytes", configkit.Bytes("memory", []byte("configkit: {imports: [{path: shared.yml}]}\n")), configkit.SourceKindBytes},
|
||||
{"reader", configkit.Reader("stream", strings.NewReader("configkit: {imports: [{path: shared.yml}]}\n")), configkit.SourceKindReader},
|
||||
{"bytes", configkit.Bytes("memory", []byte("config: {imports: [{path: shared.yml}]}\n")), configkit.SourceKindBytes},
|
||||
{"reader", configkit.Reader("stream", strings.NewReader("config: {imports: [{path: shared.yml}]}\n")), configkit.SourceKindReader},
|
||||
} {
|
||||
t.Run(input.name, func(t *testing.T) {
|
||||
result, err := configkit.Load(input.source, configkit.Options[filesystemFacadeConfig]{Imports: true, Filesystem: &configkit.FilesystemContext{Root: "root", BaseDir: "root", FS: &first}})
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFinalSequenceIdentityTreatsLateExtendsFieldsAsPayload(t *testing.T) {
|
||||
@@ -29,7 +29,7 @@ func TestFinalSequenceIdentityTreatsLateExtendsFieldsAsPayload(t *testing.T) {
|
||||
if phase == "resolution" {
|
||||
definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
|
||||
(*value)["items"] = items
|
||||
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "supply application payload")
|
||||
resolution.RecordNormalization(configkit.Keys("items"), "supply application payload")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func TestFinalSequenceIdentityValidatesLateValues(t *testing.T) {
|
||||
case "resolution":
|
||||
options.Definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
|
||||
(*value)["items"] = items
|
||||
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "create effective records")
|
||||
resolution.RecordNormalization(configkit.Keys("items"), "create effective records")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ func TestFinalSequenceIdentityMatchesLateDynamicNamespaces(t *testing.T) {
|
||||
case "resolution":
|
||||
options.Definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
|
||||
(*value)["collections"] = map[string]any{name: map[string]any{"items": items}}
|
||||
resolution.Record(configkit.Keys("collections"), configkit.ChangeNormalization, "create namespaced records")
|
||||
resolution.RecordNormalization(configkit.Keys("collections"), "create namespaced records")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
120
facade_historical_paths_test.go
Normal file
120
facade_historical_paths_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package configkit_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestPublicHistoricalPathsEnumerateContextualShapes(t *testing.T) {
|
||||
loaded, err := configkit.Load[map[string]any](configkit.Bytes("main", []byte(
|
||||
"retired: {child: 1}\nshape: {'0': base}\n'a/b~': {'': 2}\n",
|
||||
)), configkit.Options[map[string]any]{Overrides: []configkit.Override{
|
||||
{Path: configkit.Keys("retired"), Value: nil, Flag: "--retired"},
|
||||
{Path: configkit.Keys("shape"), Value: []any{"replacement"}, Flag: "--shape"},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
|
||||
metadata := loaded.Metadata()
|
||||
paths := metadata.HistoricalPaths()
|
||||
got := make([]string, len(paths))
|
||||
for index, path := range paths {
|
||||
got[index] = path.String()
|
||||
if len(metadata.History(path)) == 0 {
|
||||
t.Errorf("enumerated path %s has no history", path)
|
||||
}
|
||||
}
|
||||
want := []string{"", "/a~1b~0", "/a~1b~0/", "/retired", "/retired/child", "/shape", "/shape/0"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("historical paths = %v, want sorted unique pointers %v", got, want)
|
||||
}
|
||||
if _, ok := metadata.Lookup(paths[4]); ok {
|
||||
t.Fatal("removed descendant remains effective")
|
||||
}
|
||||
if _, ok := metadata.Lookup(paths[6]); !ok {
|
||||
t.Fatal("enumerated numeric path did not follow the effective sequence shape")
|
||||
}
|
||||
if len(metadata.History(paths[6])) == 0 {
|
||||
t.Fatal("enumerated numeric path lost history")
|
||||
}
|
||||
assertPublicShapeHistory(t, metadata, configkit.LayerCLI)
|
||||
if _, ok := metadata.Lookup(configkit.Keys("shape", "0")); ok {
|
||||
t.Fatal("mapping-key path resolved against the effective sequence")
|
||||
}
|
||||
if len((configkit.Metadata{}).HistoricalPaths()) != 0 {
|
||||
t.Fatal("zero metadata contains historical paths")
|
||||
}
|
||||
if (configkit.Metadata{}).History(configkit.Keys("shape")) != nil {
|
||||
t.Fatal("zero metadata contains history")
|
||||
}
|
||||
|
||||
paths[1] = configkit.Keys("mutated")
|
||||
if again := metadata.HistoricalPaths(); !reflect.DeepEqual(pathStrings(again), want) {
|
||||
t.Fatalf("returned path slice mutated retained metadata: %v", pathStrings(again))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicHistoryUsesResolutionShapeAttribution(t *testing.T) {
|
||||
loaded, err := configkit.Load[map[string]any](configkit.Bytes("main", []byte("shape: {'0': base}\n")), configkit.Options[map[string]any]{
|
||||
Definition: configkit.Definition[map[string]any]{
|
||||
Resolve: func(configuration *map[string]any, resolution *configkit.Resolution) error {
|
||||
(*configuration)["shape"] = []any{"replacement"}
|
||||
resolution.RecordNormalization(configkit.Keys("shape"), "replace numeric mapping key")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
assertPublicShapeHistory(t, loaded.Metadata(), configkit.LayerResolution)
|
||||
}
|
||||
|
||||
func assertPublicShapeHistory(t *testing.T, metadata configkit.Metadata, operationLayer configkit.Layer) {
|
||||
t.Helper()
|
||||
|
||||
keyPath := configkit.Keys("shape", "0")
|
||||
indexPath := configkit.Keys("shape").Index(0)
|
||||
keyHistory := metadata.History(keyPath)
|
||||
indexHistory := metadata.History(indexPath)
|
||||
if len(keyHistory) == 0 || len(indexHistory) == 0 {
|
||||
t.Fatalf("explicit shape histories = key %#v, index %#v", keyHistory, indexHistory)
|
||||
}
|
||||
var keyDefinition, keyReplacement, indexReplacement bool
|
||||
for _, change := range keyHistory {
|
||||
keyDefinition = keyDefinition || change.Origin.Layer == configkit.LayerMain
|
||||
keyReplacement = keyReplacement || change.Origin.Layer == operationLayer && change.Path.String() == "/shape"
|
||||
}
|
||||
for _, change := range indexHistory {
|
||||
if change.Origin.Layer == configkit.LayerMain {
|
||||
t.Fatalf("introduced index inherited mapping-key history: %#v", indexHistory)
|
||||
}
|
||||
indexReplacement = indexReplacement || change.Origin.Layer == operationLayer && change.Path.String() == "/shape"
|
||||
for _, previous := range change.Previous {
|
||||
if previous.Layer == configkit.LayerMain {
|
||||
t.Fatalf("introduced index inherited mapping-key previous origin: %#v", change)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !keyDefinition || !keyReplacement || !indexReplacement {
|
||||
t.Fatalf("shape histories missed applicable definitions or replacement: key %#v, index %#v", keyHistory, indexHistory)
|
||||
}
|
||||
parsed, err := configkit.ParsePath("/shape/0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if parsedHistory := metadata.History(parsed); len(parsedHistory) == 0 {
|
||||
t.Fatal("parsed numeric query lost contextual history")
|
||||
}
|
||||
}
|
||||
|
||||
func pathStrings(paths []configkit.Path) []string {
|
||||
result := make([]string, len(paths))
|
||||
for index, path := range paths {
|
||||
result[index] = path.String()
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFacadeNativeDiscoveryPreservesAliasIdentity(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for name, contents := range map[string]string{
|
||||
"data/item.yml": "configkit:\n imports:\n - path: helper.yml\nvalue: item\n",
|
||||
"data/item.yml": "config:\n imports:\n - path: helper.yml\nvalue: item\n",
|
||||
"data/helper.yml": "owner: target\nresource: resource.txt\n",
|
||||
"aliases/helper.yml": "owner: alias\nresource: resource.txt\n",
|
||||
"data/resource.txt": "target resource",
|
||||
@@ -50,7 +50,7 @@ func TestFacadeNativeDiscoveryPreservesAliasIdentity(t *testing.T) {
|
||||
{"directory/glob", "glob: 'lin*/ite*.yml'", "linked/item.yml", "linked/helper.yml", "target", "target resource"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := configkit.Load(configkit.Bytes("main", []byte("configkit:\n imports:\n - "+test.declaration+"\n")), configkit.Options[configuration]{
|
||||
result, err := configkit.Load(configkit.Bytes("main", []byte("config:\n imports:\n - "+test.declaration+"\n")), configkit.Options[configuration]{
|
||||
Imports: true, Filesystem: &configkit.FilesystemContext{FS: &filesystem, Root: ".", BaseDir: "."},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -82,14 +82,14 @@ func TestFacadeNativeDiscoveryPreservesAliasIdentity(t *testing.T) {
|
||||
// The same native path is still a repeat when separately included. An alias
|
||||
// to a leaf file remains a different native namespace path, as for path imports.
|
||||
for _, selector := range []string{"path: aliases/item.yml", "dir: aliases\n pattern: item.yml", "glob: 'aliases/ite*.yml'"} {
|
||||
_, err := configkit.Load(configkit.Bytes("main", []byte("configkit:\n imports:\n - "+selector+"\n - path: aliases/item.yml\n")), configkit.Options[configuration]{
|
||||
_, err := configkit.Load(configkit.Bytes("main", []byte("config:\n imports:\n - "+selector+"\n - path: aliases/item.yml\n")), configkit.Options[configuration]{
|
||||
Imports: true, Filesystem: &configkit.FilesystemContext{FS: &filesystem, Root: ".", BaseDir: "."},
|
||||
})
|
||||
var loadError *configkit.LoadError
|
||||
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != configkit.CodeImportRepeated {
|
||||
t.Fatalf("repeated source: %v", err)
|
||||
}
|
||||
_, err = configkit.Load(configkit.Bytes("main", []byte("configkit:\n imports:\n - "+selector+"\n - path: data/item.yml\n")), configkit.Options[configuration]{
|
||||
_, err = configkit.Load(configkit.Bytes("main", []byte("config:\n imports:\n - "+selector+"\n - path: data/item.yml\n")), configkit.Options[configuration]{
|
||||
Imports: true, Filesystem: &configkit.FilesystemContext{FS: &filesystem, Root: ".", BaseDir: "."},
|
||||
})
|
||||
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != configkit.CodeComposeConflict {
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
)
|
||||
|
||||
func TestImportDepthChecksSuffixAfterSaturation(t *testing.T) {
|
||||
@@ -21,7 +19,7 @@ func TestImportDepthChecksSuffixAfterSaturation(t *testing.T) {
|
||||
{"-0", true},
|
||||
} {
|
||||
t.Run(test.depth, func(t *testing.T) {
|
||||
main := writeFacadeImportFile(t, t.TempDir(), "main.yml", "value: true\nconfigkit:\n imports:\n - dir: absent\n recursive: true\n depth: "+test.depth+"\n")
|
||||
main := writeFacadeImportFile(t, t.TempDir(), "main.yml", "value: true\nconfig:\n imports:\n - dir: absent\n recursive: true\n depth: "+test.depth+"\n")
|
||||
result, err := Load[map[string]any](File(main), Options[map[string]any]{Imports: true})
|
||||
if test.valid {
|
||||
if err != nil || result.Config["value"] != true {
|
||||
@@ -33,7 +31,7 @@ func TestImportDepthChecksSuffixAfterSaturation(t *testing.T) {
|
||||
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 {
|
||||
t.Fatalf("invalid depth accepted: %v", err)
|
||||
}
|
||||
if diagnostic := loadError.Diagnostics()[0]; diagnostic.Code != CodeImportInvalid || diagnostic.Path.String() != "/configkit/imports/0/depth" {
|
||||
if diagnostic := loadError.Diagnostics()[0]; diagnostic.Code != CodeImportInvalid || diagnostic.Path.String() != "/config/imports/0/depth" {
|
||||
t.Fatalf("invalid depth diagnostic = %#v", diagnostic)
|
||||
}
|
||||
})
|
||||
@@ -49,12 +47,12 @@ func TestImportDefaultsApplyToTransitiveAndUnderlayImports(t *testing.T) {
|
||||
IgnoredDeep string `yaml:"ignored_deep"`
|
||||
}
|
||||
directory := t.TempDir()
|
||||
mainPath := writeFacadeImportFile(t, directory, "main.yml", `configkit:
|
||||
mainPath := writeFacadeImportFile(t, directory, "main.yml", `config:
|
||||
imports:
|
||||
- path: parent/parent.yml
|
||||
main: main
|
||||
`)
|
||||
writeFacadeImportFile(t, directory, "parent/parent.yml", `configkit:
|
||||
writeFacadeImportFile(t, directory, "parent/parent.yml", `config:
|
||||
imports:
|
||||
- dir: generated
|
||||
- dir: explicit
|
||||
@@ -66,7 +64,7 @@ main: main
|
||||
explicitPath := writeFacadeImportFile(t, directory, "parent/explicit/exception.yaml", "exception: declaration\n")
|
||||
writeFacadeImportFile(t, directory, "parent/explicit/deep/ignored.yaml", "ignored_deep: wrong-depth\n")
|
||||
writeFacadeImportFile(t, directory, "parent/explicit/ignored.cfg", "ignored_deep: wrong-explicit-pattern\n")
|
||||
underlayPath := writeFacadeImportFile(t, directory, "underlay.yml", `configkit:
|
||||
underlayPath := writeFacadeImportFile(t, directory, "underlay.yml", `config:
|
||||
imports:
|
||||
- dir: underlay-files
|
||||
`)
|
||||
@@ -95,25 +93,32 @@ main: main
|
||||
|
||||
func TestImportDefaultsPreserveEmptyPatternAndRequiredness(t *testing.T) {
|
||||
type config struct {
|
||||
Value string `yaml:"value"`
|
||||
Value string `yaml:"value"`
|
||||
Hidden string `yaml:"hidden"`
|
||||
}
|
||||
directory := t.TempDir()
|
||||
mainPath := writeFacadeImportFile(t, directory, "main.yml", `configkit:
|
||||
mainPath := writeFacadeImportFile(t, directory, "main.yml", `config:
|
||||
imports:
|
||||
- dir: snippets
|
||||
`)
|
||||
writeFacadeImportFile(t, directory, "snippets/value.cfg", "value: imported\n")
|
||||
writeFacadeImportFile(t, directory, "snippets/.hidden.cfg", "hidden: imported\n")
|
||||
|
||||
unrestricted, err := Load(File(mainPath), Options[config]{Imports: true})
|
||||
if err != nil || unrestricted.Config.Value != "imported" {
|
||||
t.Fatalf("unspecified pattern = %#v, %v", unrestricted, err)
|
||||
}
|
||||
includeHidden := false
|
||||
visibleOnly, err := Load(File(mainPath), Options[config]{Imports: true, ImportDefaults: ImportDefaults{IncludeHidden: &includeHidden}})
|
||||
if err != nil || visibleOnly.Config.Value != "imported" || visibleOnly.Config.Hidden != "" {
|
||||
t.Fatalf("hidden-file default = %#v, %v", visibleOnly, err)
|
||||
}
|
||||
empty, err := Load(File(mainPath), Options[config]{Imports: true, ImportDefaults: ImportDefaults{Pattern: []string{}}})
|
||||
if err != nil || empty.Config.Value != "" {
|
||||
t.Fatalf("empty pattern = %#v, %v", empty, err)
|
||||
}
|
||||
emptySources := empty.Sources()
|
||||
if len(emptySources) != 1 || len(emptySources[0].ImportSelections) != 1 || emptySources[0].ImportSelections[0].Path.String() != "/configkit/imports/0" || emptySources[0].ImportSelections[0].Selector != ImportSelectorDirectory || emptySources[0].ImportSelections[0].Outcome != ImportSelectionNone {
|
||||
if len(emptySources) != 1 || len(emptySources[0].EmptyImportSelections) != 1 || emptySources[0].EmptyImportSelections[0].Path.String() != "/config/imports/0" || emptySources[0].EmptyImportSelections[0].Selector != ImportSelectorDirectory {
|
||||
t.Fatalf("empty-pattern evidence = %#v", emptySources)
|
||||
}
|
||||
required := true
|
||||
@@ -122,23 +127,64 @@ func TestImportDefaultsPreserveEmptyPatternAndRequiredness(t *testing.T) {
|
||||
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != CodeImportMissing {
|
||||
t.Fatalf("required empty selection error = %T %v", err, err)
|
||||
}
|
||||
if sources := loadError.Sources(); len(sources) != 1 || len(sources[0].ImportSelections) != 0 {
|
||||
if sources := loadError.Sources(); len(sources) != 1 || len(sources[0].EmptyImportSelections) != 0 {
|
||||
t.Fatalf("required empty selection was recorded as optional: %#v", sources)
|
||||
}
|
||||
}
|
||||
|
||||
recursiveMain := writeFacadeImportFile(t, directory, "recursive.yml", `configkit:
|
||||
func TestImportDefaultsAreSnapshottedBeforeOrdinaryAndLayeredCallbacks(t *testing.T) {
|
||||
type config struct {
|
||||
Version string `yaml:"version"`
|
||||
Imported string `yaml:"imported"`
|
||||
}
|
||||
for _, layered := range []bool{false, true} {
|
||||
t.Run(map[bool]string{false: "ordinary", true: "layered"}[layered], func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeFacadeImportFile(t, directory, "main.yml", `version: v1
|
||||
config:
|
||||
imports:
|
||||
- dir: recursive-files
|
||||
- dir: parts
|
||||
`)
|
||||
writeFacadeImportFile(t, directory, "recursive-files/nested/value.cfg", "value: selected\n")
|
||||
recursive := true
|
||||
patterns := []string{"*.cfg"}
|
||||
engineOptions := toEngineOptions(Options[config]{Imports: true, ImportDefaults: ImportDefaults{Recursive: &recursive, Pattern: patterns}})
|
||||
recursive = false
|
||||
patterns[0] = "["
|
||||
loadedEngine, err := engine.Load(File(recursiveMain).toEngine(), secretBinding(), engineOptions)
|
||||
if err != nil || loadedEngine.Config.Value != "selected" {
|
||||
t.Fatalf("load after mutating caller defaults = %#v, %v", loadedEngine, err)
|
||||
writeFacadeImportFile(t, directory, "parts/nested/value.cfg", "imported: selected\n")
|
||||
|
||||
recursive := true
|
||||
patterns := []string{"*.cfg"}
|
||||
definition := Definition[config]{Version: &VersionRule{
|
||||
Path: Keys("version"),
|
||||
Check: func(version Version, _ *Validation) error {
|
||||
if version != (Version{Present: true, Text: "v1"}) {
|
||||
t.Errorf("version callback received %#v", version)
|
||||
}
|
||||
recursive = false
|
||||
patterns[0] = "*.skip"
|
||||
return nil
|
||||
},
|
||||
}}
|
||||
load := func() (*Result[config], error) {
|
||||
defaults := ImportDefaults{Recursive: &recursive, Pattern: patterns}
|
||||
if layered {
|
||||
return LoadLayered(Input{Source: File(mainPath), Imports: true}, LayeredOptions[config]{
|
||||
Definition: definition, SourceVersionMode: SourceVersionModeIfPresent, ImportDefaults: defaults,
|
||||
})
|
||||
}
|
||||
return Load(File(mainPath), Options[config]{
|
||||
Definition: definition, SourceVersionMode: SourceVersionModeIfPresent, Imports: true, ImportDefaults: defaults,
|
||||
})
|
||||
}
|
||||
|
||||
first, err := load()
|
||||
if err != nil || first.Config.Imported != "selected" {
|
||||
t.Fatalf("load with pre-callback defaults = %#v, %v", first, err)
|
||||
}
|
||||
if recursive || patterns[0] != "*.skip" {
|
||||
t.Fatalf("version callback did not mutate caller defaults: recursive=%v pattern=%q", recursive, patterns[0])
|
||||
}
|
||||
|
||||
second, err := load()
|
||||
if err != nil || second.Config.Imported != "" {
|
||||
t.Fatalf("subsequent load reused earlier defaults: %#v, %v", second, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +229,7 @@ func TestPublicImportSelectionSnapshotsAreDetachedAcrossResultErrorAndResolution
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
directory := t.TempDir()
|
||||
mainPath := writeFacadeImportFile(t, directory, "main.yml", `configkit:
|
||||
mainPath := writeFacadeImportFile(t, directory, "main.yml", `config:
|
||||
imports:
|
||||
- dir: absent
|
||||
value: loaded
|
||||
@@ -191,33 +237,33 @@ value: loaded
|
||||
callbackPath := ""
|
||||
loaded, err := Load(File(mainPath), Options[config]{Imports: true, Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||
first, ok := resolution.Source("source-1")
|
||||
if !ok || len(first.ImportSelections) != 1 {
|
||||
if !ok || len(first.EmptyImportSelections) != 1 {
|
||||
t.Fatalf("resolution source = %#v, %v", first, ok)
|
||||
}
|
||||
first.ImportSelections[0].Path = Keys("callback mutation")
|
||||
first.EmptyImportSelections[0].Path = Keys("callback mutation")
|
||||
second, ok := resolution.Source("source-1")
|
||||
if !ok || second.ImportSelections[0].Path.String() != "/configkit/imports/0" {
|
||||
if !ok || second.EmptyImportSelections[0].Path.String() != "/config/imports/0" {
|
||||
t.Fatalf("resolution snapshot changed after mutation: %#v", second)
|
||||
}
|
||||
callbackPath = second.ImportSelections[0].Path.String()
|
||||
callbackPath = second.EmptyImportSelections[0].Path.String()
|
||||
return nil
|
||||
}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if callbackPath != "/configkit/imports/0" {
|
||||
if callbackPath != "/config/imports/0" {
|
||||
t.Fatalf("callback path = %q", callbackPath)
|
||||
}
|
||||
sources := loaded.Sources()
|
||||
if len(sources) != 1 || len(sources[0].ImportSelections) != 1 || sources[0].ImportSelections[0].Location.SourceID != sources[0].ID || sources[0].ImportSelections[0].Selector != ImportSelectorDirectory {
|
||||
if len(sources) != 1 || len(sources[0].EmptyImportSelections) != 1 || sources[0].EmptyImportSelections[0].Location.SourceID != sources[0].ID || sources[0].EmptyImportSelections[0].Selector != ImportSelectorDirectory {
|
||||
t.Fatalf("result source evidence = %#v", sources)
|
||||
}
|
||||
sources[0].ImportSelections[0].Path = Keys("result mutation")
|
||||
if got := loaded.Sources()[0].ImportSelections[0].Path.String(); got != "/configkit/imports/0" {
|
||||
sources[0].EmptyImportSelections[0].Path = Keys("result mutation")
|
||||
if got := loaded.Sources()[0].EmptyImportSelections[0].Path.String(); got != "/config/imports/0" {
|
||||
t.Fatalf("result snapshot changed after mutation: %q", got)
|
||||
}
|
||||
|
||||
failurePath := writeFacadeImportFile(t, directory, "failure.yml", `configkit:
|
||||
failurePath := writeFacadeImportFile(t, directory, "failure.yml", `config:
|
||||
imports:
|
||||
- dir: absent
|
||||
- path: required-missing.yml
|
||||
@@ -229,11 +275,11 @@ value: loaded
|
||||
t.Fatalf("failed load error = %T %v", err, err)
|
||||
}
|
||||
errorSources := loadError.Sources()
|
||||
if len(errorSources) != 1 || len(errorSources[0].ImportSelections) != 1 || errorSources[0].ImportSelections[0].Path.String() != "/configkit/imports/0" {
|
||||
if len(errorSources) != 1 || len(errorSources[0].EmptyImportSelections) != 1 || errorSources[0].EmptyImportSelections[0].Path.String() != "/config/imports/0" {
|
||||
t.Fatalf("error source evidence = %#v", errorSources)
|
||||
}
|
||||
errorSources[0].ImportSelections[0].Path = Keys("error mutation")
|
||||
if got := loadError.Sources()[0].ImportSelections[0].Path.String(); got != "/configkit/imports/0" {
|
||||
errorSources[0].EmptyImportSelections[0].Path = Keys("error mutation")
|
||||
if got := loadError.Sources()[0].EmptyImportSelections[0].Path.String(); got != "/config/imports/0" {
|
||||
t.Fatalf("error snapshot changed after mutation: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func TestDiscoveredItemHelpersPreserveNestedInheritedResourceEvidence(t *testing.T) {
|
||||
const canary = "PRIVATE-CONSUMED-HEADER-CANARY"
|
||||
filesystem := NewFilesystem("bundle", fstest.MapFS{
|
||||
"bundle/config/main.yml": {Data: []byte(`configkit:
|
||||
"bundle/config/main.yml": {Data: []byte(`config:
|
||||
imports:
|
||||
- dir: records
|
||||
pattern: '*-item.yml'
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
- id: authored
|
||||
version: authored
|
||||
`)},
|
||||
"bundle/config/records/a-item.yml": {Data: []byte(`configkit:
|
||||
"bundle/config/records/a-item.yml": {Data: []byte(`config:
|
||||
imports:
|
||||
- path: helper.yml
|
||||
version: ` + canary + `
|
||||
@@ -42,7 +42,7 @@ entries:
|
||||
Definition: Definition[map[string]any]{
|
||||
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
|
||||
SequenceIdentities: []SequenceIdentity{SequenceIdentityAt(Keys("services", "records"), "id")},
|
||||
Secret: func(candidate Path) bool {
|
||||
Sensitive: func(candidate Path) bool {
|
||||
return candidate.String() == "/services/records/0/version"
|
||||
},
|
||||
},
|
||||
@@ -67,6 +67,14 @@ entries:
|
||||
}
|
||||
|
||||
sources := result.Sources()
|
||||
if len(sources) < 3 || sources[0].Layer != LayerMain || string(sources[0].Layer) != "main" {
|
||||
t.Fatalf("main source attribution = %#v", sources)
|
||||
}
|
||||
for _, source := range sources[1:] {
|
||||
if source.ImportedFrom != nil && (source.Layer != LayerMain || string(source.Layer) != "main") {
|
||||
t.Errorf("imported main source attribution = %#v", source)
|
||||
}
|
||||
}
|
||||
filesByID := make(map[string]string, len(sources))
|
||||
for _, source := range sources {
|
||||
filesByID[source.ID] = source.Path
|
||||
@@ -77,8 +85,8 @@ entries:
|
||||
itemSource = source
|
||||
}
|
||||
}
|
||||
if itemSource.VersionCheck == nil || !itemSource.VersionCheck.Version.Present || !itemSource.VersionCheck.Redacted || itemSource.VersionCheck.Version.Text != "" {
|
||||
t.Fatalf("consumed header inspection = %#v", itemSource.VersionCheck)
|
||||
if itemSource.VersionEvidence == nil || !itemSource.VersionEvidence.Version.Present || !itemSource.VersionEvidence.Redacted || itemSource.VersionEvidence.Version.Text != "" {
|
||||
t.Fatalf("consumed header inspection = %#v", itemSource.VersionEvidence)
|
||||
}
|
||||
idInfo, ok := result.Metadata().Lookup(Keys("services", "records").Index(0).Key("id"))
|
||||
if !ok || len(idInfo.Origin.Locations) != 1 || filesByID[idInfo.Origin.Locations[0].SourceID] != "bundle/config/records/helper.yml" {
|
||||
@@ -107,7 +115,7 @@ entries:
|
||||
Definition: Definition[map[string]any]{
|
||||
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
|
||||
SequenceIdentities: []SequenceIdentity{SequenceIdentityAt(Keys("services", "records"), "version")},
|
||||
Secret: func(candidate Path) bool {
|
||||
Sensitive: func(candidate Path) bool {
|
||||
return candidate.String() == "/services/records/0/version"
|
||||
},
|
||||
},
|
||||
@@ -129,7 +137,7 @@ entries:
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundFailedItem || failedItemSource.VersionCheck == nil || failedItemSource.VersionCheck.Version.Text != "" || !failedItemSource.VersionCheck.Redacted {
|
||||
if !foundFailedItem || failedItemSource.VersionEvidence == nil || failedItemSource.VersionEvidence.Version.Text != "" || !failedItemSource.VersionEvidence.Redacted {
|
||||
t.Fatalf("failed-load source inspection exposed or omitted header evidence: %#v", failedItemSource)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFacadeDirectoryRootBelowContainedAbsoluteSymlink(t *testing.T) {
|
||||
@@ -29,7 +29,7 @@ func TestFacadeDirectoryRootBelowContainedAbsoluteSymlink(t *testing.T) {
|
||||
for _, follow := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("follow=%t", follow), func(t *testing.T) {
|
||||
mainPath := filepath.Join(root, "main.yml")
|
||||
contents := fmt.Sprintf("configkit:\n imports:\n - dir: absolute/parts\n pattern: '*.yml'\n follow: %t\n", follow)
|
||||
contents := fmt.Sprintf("config:\n imports:\n - dir: absolute/parts\n pattern: '*.yml'\n follow: %t\n", follow)
|
||||
if err := os.WriteFile(mainPath, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func TestFacadeNativeDiscoveryWithNestedRoot(t *testing.T) {
|
||||
"dir: parts", "glob: 'parts/*.yml'", "dir: linked", "glob: 'lin*/*.yml'",
|
||||
} {
|
||||
t.Run(selector, func(t *testing.T) {
|
||||
result, err := configkit.Load(configkit.Bytes("main", []byte("configkit:\n imports:\n - "+selector+"\n")), configkit.Options[map[string]any]{Imports: true, Filesystem: context})
|
||||
result, err := configkit.Load(configkit.Bytes("main", []byte("config:\n imports:\n - "+selector+"\n")), configkit.Options[map[string]any]{Imports: true, Filesystem: context})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func TestFacadeNativeDiscoveryWithNestedRoot(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, selector := range []string{"dir: parts", "glob: 'parts/*.yml'"} {
|
||||
_, err := configkit.Load(configkit.Bytes("main", []byte("configkit:\n imports:\n - "+selector+"\n")), configkit.Options[map[string]any]{Imports: true, Filesystem: context})
|
||||
_, err := configkit.Load(configkit.Bytes("main", []byte("config:\n imports:\n - "+selector+"\n")), configkit.Options[map[string]any]{Imports: true, Filesystem: context})
|
||||
var loadError *configkit.LoadError
|
||||
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != configkit.CodeImportOutsideRoot {
|
||||
t.Fatalf("escaping %s: %v", selector, err)
|
||||
@@ -107,7 +107,7 @@ func TestFacadeDiscoveryAllowsFiniteLinkReuse(t *testing.T) {
|
||||
if native {
|
||||
context = &configkit.FilesystemContext{FS: &filesystem, Root: ".", BaseDir: "."}
|
||||
}
|
||||
result, err := configkit.Load(configkit.Bytes("main", []byte("configkit:\n imports:\n - "+selector+"\n")), configkit.Options[map[string]any]{Imports: true, Filesystem: context})
|
||||
result, err := configkit.Load(configkit.Bytes("main", []byte("config:\n imports:\n - "+selector+"\n")), configkit.Options[map[string]any]{Imports: true, Filesystem: context})
|
||||
if err != nil {
|
||||
t.Fatalf("native=%v selector=%s: %v", native, selector, err)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
type inheritedEntry struct {
|
||||
@@ -42,7 +42,7 @@ func TestFacadeInheritancePreservesPresenceOriginsAndHistory(t *testing.T) {
|
||||
if !resolution.IsExplicit(configkit.Keys("entries", "child", "enabled")) {
|
||||
t.Error("inherited false is not explicit")
|
||||
}
|
||||
info, found := resolution.InputInfo(configkit.Keys("entries", "child", "enabled"))
|
||||
info, found := resolution.EntryInfo(configkit.Keys("entries", "child", "enabled"))
|
||||
if !found || len(info.Origin.Inheritance) != 1 {
|
||||
t.Errorf("resolver inherited origin = %#v, %v", info, found)
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func TestFacadeIdentifiedInheritanceResolvesChainsAndNestedCollections(t *testin
|
||||
result, err := configkit.Load(filesystem.File("config/identified.yml"), configkit.Options[configuration]{
|
||||
Definition: configkit.Definition[configuration]{
|
||||
SequenceIdentities: registrations,
|
||||
Secret: func(path configkit.Path) bool {
|
||||
Sensitive: func(path configkit.Path) bool {
|
||||
return path.String() == "/jobs/0/credential" || path.String() == "/jobs/1/credential" || path.String() == "/jobs/2/credential"
|
||||
},
|
||||
},
|
||||
@@ -221,7 +221,7 @@ func TestFacadeIdentifiedInheritanceFailureDoesNotRevealSecretCanary(t *testing.
|
||||
_, err := configkit.Load(configkit.Bytes("identified-failure.yml", input), configkit.Options[configuration]{
|
||||
Definition: configkit.Definition[configuration]{
|
||||
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("jobs"), "id")},
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/jobs/0/credential" },
|
||||
Sensitive: func(path configkit.Path) bool { return path.String() == "/jobs/0/credential" },
|
||||
},
|
||||
})
|
||||
if err == nil || strings.Contains(err.Error(), canary) {
|
||||
@@ -231,7 +231,7 @@ func TestFacadeIdentifiedInheritanceFailureDoesNotRevealSecretCanary(t *testing.
|
||||
|
||||
func TestFacadeLayeredIdentifiedInheritanceUsesSurvivingRecords(t *testing.T) {
|
||||
policy := func(mode string) string {
|
||||
return "configkit:\n sequences:\n - destination: [jobs]\n key: id\n mode: " + mode + "\n"
|
||||
return "config:\n sequences:\n - destination: [jobs]\n key: id\n mode: " + mode + "\n"
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -416,7 +416,7 @@ func TestFacadeInheritanceFailureStopsLaterCallbacks(t *testing.T) {
|
||||
|
||||
func TestFacadeCLIOverridesCannotChangeInheritanceRelationship(t *testing.T) {
|
||||
filesystem := configkit.NewFilesystem("bundle", fstest.MapFS{
|
||||
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: parent.yml}]}\nentries: {child: {extends: first}}\n")},
|
||||
"main.yml": &fstest.MapFile{Data: []byte("config: {imports: [{path: parent.yml}]}\nentries: {child: {extends: first}}\n")},
|
||||
"parent.yml": &fstest.MapFile{Data: []byte("entries: {first: {count: 1}, second: {count: 2}}\n")},
|
||||
})
|
||||
_, err := configkit.Load(filesystem.File("main.yml"), configkit.Options[inheritedConfiguration]{
|
||||
@@ -427,7 +427,7 @@ func TestFacadeCLIOverridesCannotChangeInheritanceRelationship(t *testing.T) {
|
||||
t.Fatalf("CLI inheritance relationship change = %v", err)
|
||||
}
|
||||
conflicting := configkit.NewFilesystem("bundle", fstest.MapFS{
|
||||
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: parent.yml}]}\nentries: {child: {extends: first}, first: {count: 3}}\n")},
|
||||
"main.yml": &fstest.MapFile{Data: []byte("config: {imports: [{path: parent.yml}]}\nentries: {child: {extends: first}, first: {count: 3}}\n")},
|
||||
"parent.yml": &fstest.MapFile{Data: []byte("entries: {first: {count: 1}}\n")},
|
||||
})
|
||||
_, err = configkit.Load[inheritedConfiguration](conflicting.File("main.yml"), configkit.Options[inheritedConfiguration]{Imports: true})
|
||||
@@ -468,7 +468,7 @@ func TestFacadeInheritedSecretsStayRedactedAfterLookup(t *testing.T) {
|
||||
Entries map[string]entry `yaml:"entries"`
|
||||
}
|
||||
result, err := configkit.Load[configuration](configkit.Bytes("secret.yml", []byte("entries: {parent: {credential: CONFIGKIT_INHERITED_TEST_SECRET, plain: visible}, child: {extends: parent}}\n")), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/entries/parent/credential" },
|
||||
Sensitive: func(path configkit.Path) bool { return path.String() == "/entries/parent/credential" },
|
||||
}})
|
||||
if err != nil || result.Config.Entries["child"].Credential.Value() != canary {
|
||||
t.Fatalf("inherited credential = %#v, %v", result, err)
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
)
|
||||
|
||||
func TestWrappedSourcesPreserveEngineOwnership(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type config struct {
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
data := []byte("value: retained\n")
|
||||
source := Bytes("snapshot.yml", data)
|
||||
data[7] = 'X'
|
||||
result, err := engine.Load[config](source.toEngine(), engine.SecretBinding{})
|
||||
if err != nil || result.Config.Value != "retained" {
|
||||
t.Fatalf("snapshotted source = %#v, %v", result, err)
|
||||
}
|
||||
|
||||
readerSource := Reader("reader.yml", strings.NewReader("value: once\n"))
|
||||
first := readerSource
|
||||
if _, err := engine.Load[config](first.toEngine(), engine.SecretBinding{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = engine.Load[config](readerSource.toEngine(), engine.SecretBinding{})
|
||||
var loadError *engine.LoadError
|
||||
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != engine.CodeSourceReaderUsed {
|
||||
t.Fatalf("copied reader source error = %#v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrappedPathsRetainEngineSegmentIntent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
parsed, err := ParsePath("/0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keyed, indexed := Keys("0"), Path{}.Index(0)
|
||||
if parsed.String() != "/0" || keyed.String() != "/0" || indexed.String() != "/0" {
|
||||
t.Fatalf("path spellings = %q, %q, %q", parsed, keyed, indexed)
|
||||
}
|
||||
if reflect.DeepEqual(parsed.value, keyed.value) || reflect.DeepEqual(parsed.value, indexed.value) || reflect.DeepEqual(keyed.value, indexed.value) {
|
||||
t.Fatal("path wrappers collapsed parsed, mapping-key, and index segments")
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFacadeItemScopesDoNotReinterpretGlobalRegistrations(t *testing.T) {
|
||||
@@ -22,14 +22,14 @@ func TestFacadeItemScopesDoNotReinterpretGlobalRegistrations(t *testing.T) {
|
||||
configkit.SequenceLiteralKey("catalog"), configkit.SequenceAnyMappingKey(), configkit.SequenceLiteralKey("prompts"),
|
||||
}, "id")
|
||||
}
|
||||
item := "configkit: {imports: [{path: helper.yml}, {path: child.yml, destination: [children], placement: sequence-item}]}\n"
|
||||
item := "config: {imports: [{path: helper.yml}, {path: child.yml, destination: [children], placement: sequence-item}]}\n"
|
||||
if !missingOuterID {
|
||||
item += "id: outer\n"
|
||||
}
|
||||
files := configkit.NewFilesystem("items", fstest.MapFS{
|
||||
"main.yml": {Data: []byte("configkit: {imports: [{path: item.yml, destination: [items], placement: sequence-item}]}\n")},
|
||||
"main.yml": {Data: []byte("config: {imports: [{path: item.yml, destination: [items], placement: sequence-item}]}\n")},
|
||||
"item.yml": {Data: []byte(item)}, "helper.yml": {Data: []byte(payload)},
|
||||
"child.yml": {Data: []byte("id: child\nconfigkit: {imports: [{path: child-helper.yml}]}\n")},
|
||||
"child.yml": {Data: []byte("id: child\nconfig: {imports: [{path: child-helper.yml}]}\n")},
|
||||
"child-helper.yml": {Data: []byte(payload)},
|
||||
})
|
||||
definition := configkit.Definition[map[string]any]{SequenceIdentities: []configkit.SequenceIdentity{
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFacadeItemDomainVersionsKeepEffectiveScope(t *testing.T) {
|
||||
@@ -13,18 +13,18 @@ func TestFacadeItemDomainVersionsKeepEffectiveScope(t *testing.T) {
|
||||
underlay, mainItem bool
|
||||
}{
|
||||
{name: "item", item: "id: item\nversion: domain\n"},
|
||||
{name: "helper", item: "id: item\nconfigkit: {imports: [{path: helper.yml}]}\n", helper: "version: domain\n"},
|
||||
{name: "nested", item: "id: parent\nconfigkit: {imports: [{path: child.yml, destination: [children], placement: sequence-item}]}\n", child: "id: child\nversion: domain\n"},
|
||||
{name: "helper", item: "id: item\nconfig: {imports: [{path: helper.yml}]}\n", helper: "version: domain\n"},
|
||||
{name: "nested", item: "id: parent\nconfig: {imports: [{path: child.yml, destination: [children], placement: sequence-item}]}\n", child: "id: child\nversion: domain\n"},
|
||||
{name: "underlay", item: "id: item\nversion: domain\n", underlay: true},
|
||||
{name: "main-item", item: "id: parent\nconfigkit: {imports: [{path: child.yml, destination: [children], placement: sequence-item}]}\n", child: "id: child\nversion: domain\n", mainItem: true},
|
||||
{name: "main-item", item: "id: parent\nconfig: {imports: [{path: child.yml, destination: [children], placement: sequence-item}]}\n", child: "id: child\nversion: domain\n", mainItem: true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
files := fstest.MapFS{
|
||||
"main.yml": {Data: []byte("version: 1\nconfigkit: {imports: [{path: item.yml, destination: [items], placement: sequence-item}]}\n")},
|
||||
"main.yml": {Data: []byte("version: 1\nconfig: {imports: [{path: item.yml, destination: [items], placement: sequence-item}]}\n")},
|
||||
"item.yml": {Data: []byte(test.item)},
|
||||
"child.yml": {Data: []byte(test.child)},
|
||||
"helper.yml": {Data: []byte(test.helper)},
|
||||
"underlay.yml": {Data: []byte("configkit: {imports: [{path: item.yml, destination: [items], placement: sequence-item}]}\n")},
|
||||
"underlay.yml": {Data: []byte("config: {imports: [{path: item.yml, destination: [items], placement: sequence-item}]}\n")},
|
||||
}
|
||||
filesystem := configkit.NewFilesystem("items", files)
|
||||
definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
type layeredFacadeConfig struct {
|
||||
@@ -28,7 +28,7 @@ func TestFacadeLayeredMixedSourcesAndInspection(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lower := configkit.NewFilesystem("bundle", fstest.MapFS{
|
||||
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: defaults.yml}]}\nsettings: {shared: lower, native: retained, secret: old-canary}\nlist: [lower]\n")},
|
||||
"main.yml": &fstest.MapFile{Data: []byte("config: {imports: [{path: defaults.yml}]}\nsettings: {shared: lower, native: retained, secret: old-canary}\nlist: [lower]\n")},
|
||||
"defaults.yml": &fstest.MapFile{Data: []byte("settings: {imported: retained}\n")},
|
||||
"overlays/testing.yml": &fstest.MapFile{Data: []byte("settings: {from_lower_namespace: retained}\n")},
|
||||
})
|
||||
@@ -59,8 +59,8 @@ func TestFacadeLayeredMixedSourcesAndInspection(t *testing.T) {
|
||||
configkit.Default(resolution, configkit.Keys("derived"), &config.Derived, config.Settings["native"], "derive from native", configkit.Keys("settings", "native"))
|
||||
return nil
|
||||
},
|
||||
Validate: func(config *layeredFacadeConfig, validation *configkit.Validation) { checks.validation++ },
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/settings/secret" },
|
||||
Validate: func(config *layeredFacadeConfig, validation *configkit.Validation) { checks.validation++ },
|
||||
Sensitive: func(path configkit.Path) bool { return path.String() == "/settings/secret" },
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -80,7 +80,7 @@ func TestFacadeLayeredMixedSourcesAndInspection(t *testing.T) {
|
||||
if len(sources) != 8 {
|
||||
t.Fatalf("sources = %#v", sources)
|
||||
}
|
||||
wantLayers := []configkit.Layer{configkit.LayerBase, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerOverlay, configkit.LayerCLI}
|
||||
wantLayers := []configkit.Layer{configkit.LayerMain, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerOverlay, configkit.LayerCLI}
|
||||
wantIndexes := []int{0, 1, 1, 1, 1, 2, 0, 0}
|
||||
wantKinds := []configkit.SourceKind{configkit.SourceKindFile, configkit.SourceKindFilesystem, configkit.SourceKindFilesystem, configkit.SourceKindMapping, configkit.SourceKindFilesystem, configkit.SourceKindMapping, configkit.SourceKindFilesystem, configkit.SourceKindCLI}
|
||||
for index, source := range sources {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
type mappingFacadeConfig struct {
|
||||
@@ -36,7 +36,7 @@ func TestFacadeMappingSnapshotsAndMatchesYAML(t *testing.T) {
|
||||
if !reflect.DeepEqual(result.Config, want) {
|
||||
t.Fatalf("mapping config = %#v, want %#v", result.Config, want)
|
||||
}
|
||||
if sources := result.Sources(); len(sources) != 1 || sources[0].ID != "source-1" || sources[0].Kind != configkit.SourceKindMapping || sources[0].Layer != configkit.LayerBase || sources[0].Path != "" {
|
||||
if sources := result.Sources(); len(sources) != 1 || sources[0].ID != "source-1" || sources[0].Kind != configkit.SourceKindMapping || sources[0].Layer != configkit.LayerMain || sources[0].Path != "" {
|
||||
t.Fatalf("mapping sources = %#v", sources)
|
||||
}
|
||||
info, ok := result.Metadata().Lookup(configkit.Keys("name"))
|
||||
@@ -80,8 +80,8 @@ func TestFacadeMappingImportsUseExplicitContext(t *testing.T) {
|
||||
"root/shared.yml": &fstest.MapFile{Data: []byte("name: imported\n")},
|
||||
})
|
||||
source := configkit.Mapping("programmatic", map[string]any{
|
||||
"configkit": map[string]any{"imports": []any{map[string]any{"path": "shared.yml"}}},
|
||||
"enabled": true,
|
||||
"config": map[string]any{"imports": []any{map[string]any{"path": "shared.yml"}}},
|
||||
"enabled": true,
|
||||
})
|
||||
result, err := configkit.Load(source, configkit.Options[mappingFacadeConfig]{
|
||||
Imports: true, Filesystem: &configkit.FilesystemContext{Root: "root", BaseDir: "root", FS: &namespace},
|
||||
@@ -89,7 +89,7 @@ func TestFacadeMappingImportsUseExplicitContext(t *testing.T) {
|
||||
if err != nil || result.Config.Name != "imported" || !result.Config.Enabled {
|
||||
t.Fatalf("mapping import = %#v, %v", result, err)
|
||||
}
|
||||
if sources := result.Sources(); len(sources) != 2 || sources[0].Kind != configkit.SourceKindMapping || sources[1].Kind != configkit.SourceKindFilesystem || sources[1].Layer != configkit.LayerBase {
|
||||
if sources := result.Sources(); len(sources) != 2 || sources[0].Kind != configkit.SourceKindMapping || sources[1].Kind != configkit.SourceKindFilesystem || sources[1].Layer != configkit.LayerMain {
|
||||
t.Fatalf("mapping import sources = %#v", sources)
|
||||
}
|
||||
checkMappingCode(t, source, configkit.Options[mappingFacadeConfig]{Imports: true}, configkit.CodeOptionsInvalid)
|
||||
|
||||
63
facade_resolution_presence_test.go
Normal file
63
facade_resolution_presence_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package configkit_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFacadeResolutionPresenceUsesDynamicEntryShape(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type configuration struct {
|
||||
Shape any `yaml:"shape"`
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
keyPresent bool
|
||||
}{
|
||||
{name: "mapping key", input: "shape: {'0': input}\n", keyPresent: true},
|
||||
{name: "sequence index", input: "shape: [input]\n", keyPresent: false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
parsed, err := configkit.ParsePath("/shape/0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = configkit.Load(configkit.Bytes("dynamic.yml", []byte(test.input)), configkit.Options[configuration]{
|
||||
Definition: configkit.Definition[configuration]{
|
||||
Resolve: func(_ *configuration, resolution *configkit.Resolution) error {
|
||||
check := func(path configkit.Path, want bool) error {
|
||||
info, found := resolution.EntryInfo(path)
|
||||
explicit := resolution.IsExplicit(path)
|
||||
if found != want || explicit != want || (found && info.Explicit != explicit) {
|
||||
return errors.New("resolution presence disagrees with dynamic entry shape")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, checkPath := range []struct {
|
||||
path configkit.Path
|
||||
want bool
|
||||
}{
|
||||
{path: configkit.Keys("shape", "0"), want: test.keyPresent},
|
||||
{path: configkit.Keys("shape").Index(0), want: !test.keyPresent},
|
||||
{path: parsed, want: true},
|
||||
{path: configkit.Keys("shape", "missing"), want: false},
|
||||
} {
|
||||
if err := check(checkPath.path, checkPath.want); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
43
facade_resolution_recording_test.go
Normal file
43
facade_resolution_recording_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package configkit
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFacadeResolutionRecordingIntents(t *testing.T) {
|
||||
type configuration struct {
|
||||
Derived string `yaml:"derived"`
|
||||
Normalized string `yaml:"normalized"`
|
||||
}
|
||||
|
||||
result, err := Load(Bytes("recording.yml", []byte("derived: explicit\nnormalized: before\n")), Options[configuration]{
|
||||
Definition: Definition[configuration]{Resolve: func(value *configuration, resolution *Resolution) error {
|
||||
value.Derived = "changed explicit input"
|
||||
resolution.RecordDerivedDefault(Keys("derived"), "derive explicit input")
|
||||
value.Normalized = "after"
|
||||
resolution.RecordNormalization(Keys("normalized"), "normalize configured input")
|
||||
return nil
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
path Path
|
||||
kind ChangeKind
|
||||
}{
|
||||
{path: Keys("derived"), kind: ChangeDerivedDefault},
|
||||
{path: Keys("normalized"), kind: ChangeNormalization},
|
||||
} {
|
||||
info, ok := result.Metadata().Lookup(test.path)
|
||||
if !ok || info.Origin.Operation != test.kind {
|
||||
t.Errorf("metadata %s = %#v, %v; want operation %q", test.path, info, ok, test.kind)
|
||||
}
|
||||
history := result.Metadata().History(test.path)
|
||||
if len(history) == 0 || history[len(history)-1].Origin.Operation != test.kind {
|
||||
t.Errorf("history %s = %#v; want final operation %q", test.path, history, test.kind)
|
||||
}
|
||||
}
|
||||
if !result.Metadata().IsExplicit(Keys("derived")) {
|
||||
t.Fatal("recording a derived default suppressed explicit-input provenance")
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
type observedResourceFS struct {
|
||||
@@ -45,7 +45,7 @@ func TestFacadeSourceContextsFollowOriginsWithoutQueryIO(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
borrowed := &observedResourceFS{files: fstest.MapFS{
|
||||
"bundle/config/defaults.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: ../shared.yml}]}\nnative: icon.txt\n")},
|
||||
"bundle/config/defaults.yml": &fstest.MapFile{Data: []byte("config: {imports: [{path: ../shared.yml}]}\nnative: icon.txt\n")},
|
||||
"bundle/shared.yml": &fstest.MapFile{Data: []byte("shared: shared.txt\n")},
|
||||
"bundle/overlays/test.yml": &fstest.MapFile{Data: []byte("selected: overlay.txt\n")},
|
||||
"bundle/config/icon.txt": &fstest.MapFile{Data: []byte("icon-v1")},
|
||||
@@ -224,8 +224,8 @@ func TestFacadeSyntheticSourceContextsAndContextlessSources(t *testing.T) {
|
||||
|
||||
func TestFacadeRelativeImportDestinationsPreserveNativeResourceContexts(t *testing.T) {
|
||||
borrowed := &observedResourceFS{files: fstest.MapFS{
|
||||
"bundle/config/main.yml": &fstest.MapFile{Data: []byte("configkit:\n imports:\n - path: entries/parent.yml\n destination: [logical]\n")},
|
||||
"bundle/config/entries/parent.yml": &fstest.MapFile{Data: []byte("configkit:\n imports:\n - path: nested/details.yml\n destination: [details]\nresource: icon.txt\n")},
|
||||
"bundle/config/main.yml": &fstest.MapFile{Data: []byte("config:\n imports:\n - path: entries/parent.yml\n destination: [logical]\n")},
|
||||
"bundle/config/entries/parent.yml": &fstest.MapFile{Data: []byte("config:\n imports:\n - path: nested/details.yml\n destination: [details]\nresource: icon.txt\n")},
|
||||
"bundle/config/entries/nested/details.yml": &fstest.MapFile{Data: []byte("resource: detail.txt\n")},
|
||||
"bundle/config/entries/icon.txt": &fstest.MapFile{Data: []byte("parent-resource")},
|
||||
"bundle/config/entries/nested/detail.txt": &fstest.MapFile{Data: []byte("nested-resource")},
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFacadeSequenceIdentitySnapshotsSelectorsAndFields(t *testing.T) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFacadeNoSourcePreservesAbsenceWithSequencePolicy(t *testing.T) {
|
||||
@@ -54,12 +54,12 @@ func TestFacadeNoSourcePreservesAbsenceWithSequencePolicy(t *testing.T) {
|
||||
func TestFacadeItemHelperHistoryPreservesScheduledPlacement(t *testing.T) {
|
||||
t.Parallel()
|
||||
filesystem := configkit.NewFilesystem("history", fstest.MapFS{
|
||||
"main.yml": {Data: []byte("records: [{id: authored}]\nconfigkit: {imports: [{path: one.yml, destination: [records], placement: sequence-item}, {path: two.yml, destination: [records], placement: sequence-item}]}\n")},
|
||||
"one.yml": {Data: []byte("id: one\nconfigkit: {imports: [{path: helper-one.yml}, {path: child.yml, destination: [children], placement: sequence-item}]}\n")},
|
||||
"two.yml": {Data: []byte("id: two\nconfigkit: {imports: [{path: helper-two.yml}]}\n")},
|
||||
"main.yml": {Data: []byte("records: [{id: authored}]\nconfig: {imports: [{path: one.yml, destination: [records], placement: sequence-item}, {path: two.yml, destination: [records], placement: sequence-item}]}\n")},
|
||||
"one.yml": {Data: []byte("id: one\nconfig: {imports: [{path: helper-one.yml}, {path: child.yml, destination: [children], placement: sequence-item}]}\n")},
|
||||
"two.yml": {Data: []byte("id: two\nconfig: {imports: [{path: helper-two.yml}]}\n")},
|
||||
"helper-one.yml": {Data: []byte("text: first helper\n")},
|
||||
"helper-two.yml": {Data: []byte("text: second helper\n")},
|
||||
"child.yml": {Data: []byte("id: child\nconfigkit: {imports: [{path: child-helper.yml}]}\n")},
|
||||
"child.yml": {Data: []byte("id: child\nconfig: {imports: [{path: child-helper.yml}]}\n")},
|
||||
"child-helper.yml": {Data: []byte("text: nested helper\n")},
|
||||
})
|
||||
result, err := configkit.Load(filesystem.File("main.yml"), configkit.Options[map[string]any]{
|
||||
@@ -107,7 +107,7 @@ func TestFacadeRegisteredReplacementExplainsRemovedDescendants(t *testing.T) {
|
||||
{"removed field", []any{map[string]any{"id": "main"}}, configkit.Keys("records").Index(0).Key("text")},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
mainYAML := "configkit:\n sequences:\n - destination: [records]\n key: id\n mode: replace\n"
|
||||
mainYAML := "config:\n sequences:\n - destination: [records]\n key: id\n mode: replace\n"
|
||||
if len(test.upper) == 0 {
|
||||
mainYAML += "records: []\n"
|
||||
} else {
|
||||
@@ -125,7 +125,7 @@ func TestFacadeRegisteredReplacementExplainsRemovedDescendants(t *testing.T) {
|
||||
t.Fatalf("removal not explained: %#v", explanation)
|
||||
}
|
||||
last := explanation.History[len(explanation.History)-1]
|
||||
if !last.Before.Present || last.After.Present || last.Origin.Layer != configkit.LayerBase || len(last.Origin.Locations) != 1 || last.Origin.Locations[0].SourceID != result.Sources()[0].ID {
|
||||
if !last.Before.Present || last.After.Present || last.Origin.Layer != configkit.LayerMain || len(last.Origin.Locations) != 1 || last.Origin.Locations[0].SourceID != result.Sources()[0].ID {
|
||||
t.Fatalf("wrong removal transition/origin: %#v", last)
|
||||
}
|
||||
})
|
||||
@@ -135,12 +135,12 @@ func TestFacadeRegisteredReplacementExplainsRemovedDescendants(t *testing.T) {
|
||||
func TestFacadeItemConflictDiagnosticsFollowEnclosingIndexes(t *testing.T) {
|
||||
t.Parallel()
|
||||
filesystem := configkit.NewFilesystem("conflicts", fstest.MapFS{
|
||||
"main.yml": {Data: []byte("records: [{id: authored}]\nconfigkit: {imports: [{path: good.yml, destination: [records], placement: sequence-item}, {path: bad.yml, destination: [records], placement: sequence-item}]}\n")},
|
||||
"group.yml": {Data: []byte("records: [{id: authored}]\nconfigkit: {imports: [{path: bad.yml, destination: [records], placement: sequence-item}]}\n")},
|
||||
"nested.yml": {Data: []byte("records: [{id: authored}]\nconfigkit: {imports: [{path: parent.yml, destination: [records], placement: sequence-item}]}\n")},
|
||||
"parent.yml": {Data: []byte("id: parent\nchildren: [{id: authored-child}]\nconfigkit: {imports: [{path: bad.yml, destination: [children], placement: sequence-item}]}\n")},
|
||||
"main.yml": {Data: []byte("records: [{id: authored}]\nconfig: {imports: [{path: good.yml, destination: [records], placement: sequence-item}, {path: bad.yml, destination: [records], placement: sequence-item}]}\n")},
|
||||
"group.yml": {Data: []byte("records: [{id: authored}]\nconfig: {imports: [{path: bad.yml, destination: [records], placement: sequence-item}]}\n")},
|
||||
"nested.yml": {Data: []byte("records: [{id: authored}]\nconfig: {imports: [{path: parent.yml, destination: [records], placement: sequence-item}]}\n")},
|
||||
"parent.yml": {Data: []byte("id: parent\nchildren: [{id: authored-child}]\nconfig: {imports: [{path: bad.yml, destination: [children], placement: sequence-item}]}\n")},
|
||||
"good.yml": {Data: []byte("id: good\n")},
|
||||
"bad.yml": {Data: []byte("id: bad\nconfigkit: {imports: [{path: helper.yml}]}\n")},
|
||||
"bad.yml": {Data: []byte("id: bad\nconfig: {imports: [{path: helper.yml}]}\n")},
|
||||
"helper.yml": {Data: []byte("id: duplicate-field\n")},
|
||||
})
|
||||
definition := configkit.Definition[map[string]any]{SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("records"), "id")}}
|
||||
@@ -211,7 +211,7 @@ func TestFacadeSequencePlacementControlsAcrossLoadEntryPoints(t *testing.T) {
|
||||
if got := ordinary.Config["entries"].([]any); len(got) != 1 || got[0].(map[string]any)["id"] != "one" {
|
||||
t.Fatalf("ordinary item placement = %#v", ordinary.Config)
|
||||
}
|
||||
if sources := ordinary.Sources(); len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeSequenceItem || sources[0].Destination.String() != "/entries" || sources[0].DestinationUnresolved {
|
||||
if sources := ordinary.Sources(); len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeSequenceItem || sources[0].Destination.String() != "/entries" || sources[0].DestinationIsFallback {
|
||||
t.Fatalf("ordinary item source = %#v", sources)
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ func TestFacadeSequencePlacementControlsAcrossLoadEntryPoints(t *testing.T) {
|
||||
t.Fatalf("layered item placement = %#v", layered.Config)
|
||||
}
|
||||
for _, source := range layered.Sources() {
|
||||
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationUnresolved {
|
||||
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationIsFallback {
|
||||
t.Errorf("layered item source = %#v", source)
|
||||
}
|
||||
}
|
||||
@@ -277,7 +277,7 @@ func TestFacadeSequencePlacementControlsAcrossLoadEntryPoints(t *testing.T) {
|
||||
func TestFacadeItemImportGlobComposesWithAuthoredSequence(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := filepath.Join(directory, "main.yml")
|
||||
if err := os.WriteFile(mainPath, []byte(`configkit:
|
||||
if err := os.WriteFile(mainPath, []byte(`config:
|
||||
imports:
|
||||
- glob: records/*.yml
|
||||
destination: [items]
|
||||
@@ -334,7 +334,7 @@ items:
|
||||
t.Fatalf("callback snapshots = %#v, contexts %#v", callbackSources, callbackContexts)
|
||||
}
|
||||
for index, source := range callbackSources {
|
||||
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/items" || source.DestinationUnresolved {
|
||||
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/items" || source.DestinationIsFallback {
|
||||
t.Errorf("callback source = %#v", source)
|
||||
}
|
||||
if callbackContexts[index].FS == nil || callbackContexts[index].BaseDir != "records" {
|
||||
@@ -349,7 +349,7 @@ items:
|
||||
t.Errorf("shifted item origin at %s = %#v, %v", path.String(), info, ok)
|
||||
}
|
||||
}
|
||||
if got := result.Sources(); len(got) != 3 || got[1].Destination.String() != "/items" || got[1].DestinationUnresolved {
|
||||
if got := result.Sources(); len(got) != 3 || got[1].Destination.String() != "/items" || got[1].DestinationIsFallback {
|
||||
t.Fatalf("final item sources = %#v", got)
|
||||
}
|
||||
}
|
||||
@@ -358,7 +358,7 @@ func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := filepath.Join(directory, "main.yml")
|
||||
helperPath := filepath.Join(directory, "helper.yml")
|
||||
if err := os.WriteFile(mainPath, []byte("configkit: {imports: [{path: helper.yml, destination: [metadata]}]}\nid: main\n"), 0o600); err != nil {
|
||||
if err := os.WriteFile(mainPath, []byte("config: {imports: [{path: helper.yml, destination: [metadata]}]}\nid: main\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(helperPath, []byte("value: [unterminated\n"), 0o600); err != nil {
|
||||
@@ -372,9 +372,9 @@ func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
|
||||
t.Fatalf("early failure = %v", err)
|
||||
}
|
||||
sources := loadError.Sources()
|
||||
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationUnresolved ||
|
||||
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationIsFallback ||
|
||||
sources[0].PlacementMode != configkit.PlacementModeSequenceItem ||
|
||||
sources[1].Destination.String() != "/items" || !sources[1].DestinationUnresolved ||
|
||||
sources[1].Destination.String() != "/items" || !sources[1].DestinationIsFallback ||
|
||||
sources[1].PlacementMode != configkit.PlacementModeMapping {
|
||||
t.Fatalf("early source snapshots = %#v", sources)
|
||||
}
|
||||
@@ -399,13 +399,13 @@ func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
|
||||
t.Fatalf("post-placement failure = %v", err)
|
||||
}
|
||||
sources = loadError.Sources()
|
||||
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationUnresolved ||
|
||||
sources[1].Destination.String() != "/items/0/metadata" || sources[1].DestinationUnresolved {
|
||||
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationIsFallback ||
|
||||
sources[1].Destination.String() != "/items/0/metadata" || sources[1].DestinationIsFallback {
|
||||
t.Fatalf("post-placement source snapshots = %#v", sources)
|
||||
}
|
||||
firstSnapshot := sources[1]
|
||||
sources[1].Destination = configkit.Keys("mutated")
|
||||
if again := loadError.Sources(); len(again) != 2 || again[1].Destination.String() != firstSnapshot.Destination.String() || again[1].DestinationUnresolved {
|
||||
if again := loadError.Sources(); len(again) != 2 || again[1].Destination.String() != firstSnapshot.Destination.String() || again[1].DestinationIsFallback {
|
||||
t.Fatalf("failure source snapshots were aliased: %#v", again)
|
||||
}
|
||||
|
||||
@@ -420,7 +420,7 @@ func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("successful item load: %v", err)
|
||||
}
|
||||
if callback.Destination.String() != "/items/0/metadata" || callback.DestinationUnresolved || callback.PlacementMode != configkit.PlacementModeMapping {
|
||||
if callback.Destination.String() != "/items/0/metadata" || callback.DestinationIsFallback || callback.PlacementMode != configkit.PlacementModeMapping {
|
||||
t.Fatalf("callback source snapshot = %#v", callback)
|
||||
}
|
||||
}
|
||||
@@ -430,10 +430,10 @@ func TestFacadeNestedItemFallbackUsesNearestResolvedContainer(t *testing.T) {
|
||||
mainPath := filepath.Join(directory, "main.yml")
|
||||
childPath := filepath.Join(directory, "child.yml")
|
||||
helperPath := filepath.Join(directory, "helper.yml")
|
||||
if err := os.WriteFile(mainPath, []byte("configkit: {imports: [{path: child.yml, destination: [children], placement: sequence-item}]}\nid: parent\n"), 0o600); err != nil {
|
||||
if err := os.WriteFile(mainPath, []byte("config: {imports: [{path: child.yml, destination: [children], placement: sequence-item}]}\nid: parent\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(childPath, []byte("configkit: {imports: [{path: helper.yml, destination: [metadata]}]}\nid: child\n"), 0o600); err != nil {
|
||||
if err := os.WriteFile(childPath, []byte("config: {imports: [{path: helper.yml, destination: [metadata]}]}\nid: child\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(helperPath, []byte("value: [unterminated\n"), 0o600); err != nil {
|
||||
@@ -450,13 +450,13 @@ func TestFacadeNestedItemFallbackUsesNearestResolvedContainer(t *testing.T) {
|
||||
if len(sources) != 3 {
|
||||
t.Fatalf("nested source snapshots = %#v", sources)
|
||||
}
|
||||
if got := sources[0]; got.Destination.String() != "/groups" || got.DestinationUnresolved || got.PlacementMode != configkit.PlacementModeSequenceItem {
|
||||
if got := sources[0]; got.Destination.String() != "/groups" || got.DestinationIsFallback || got.PlacementMode != configkit.PlacementModeSequenceItem {
|
||||
t.Errorf("outer item snapshot = %#v", got)
|
||||
}
|
||||
if got := sources[1]; got.Destination.String() != "/groups" || !got.DestinationUnresolved || got.PlacementMode != configkit.PlacementModeSequenceItem {
|
||||
if got := sources[1]; got.Destination.String() != "/groups" || !got.DestinationIsFallback || got.PlacementMode != configkit.PlacementModeSequenceItem {
|
||||
t.Errorf("nested item fallback = %#v", got)
|
||||
}
|
||||
if got := sources[2]; got.Destination.String() != "/groups" || !got.DestinationUnresolved || got.PlacementMode != configkit.PlacementModeMapping {
|
||||
if got := sources[2]; got.Destination.String() != "/groups" || !got.DestinationIsFallback || got.PlacementMode != configkit.PlacementModeMapping {
|
||||
t.Errorf("nested helper fallback = %#v", got)
|
||||
}
|
||||
}
|
||||
@@ -496,7 +496,7 @@ func TestFacadeSequenceIdentityRegistrationIsDetached(t *testing.T) {
|
||||
|
||||
func TestFacadeSourceInfoPlacementCopiesAndZeroValues(t *testing.T) {
|
||||
zero := configkit.SourceInfo{}
|
||||
if zero.PlacementMode != configkit.PlacementModeMapping || zero.DestinationUnresolved {
|
||||
if zero.PlacementMode != configkit.PlacementModeMapping || zero.DestinationIsFallback {
|
||||
t.Fatalf("zero source info = %#v", zero)
|
||||
}
|
||||
loaded, err := configkit.Load[map[string]any](configkit.Mapping("mapping", map[string]any{"value": true}))
|
||||
@@ -504,12 +504,12 @@ func TestFacadeSourceInfoPlacementCopiesAndZeroValues(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sources := loaded.Sources()
|
||||
if len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeMapping || sources[0].DestinationUnresolved {
|
||||
if len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeMapping || sources[0].DestinationIsFallback {
|
||||
t.Fatalf("mapping source evidence = %#v", sources)
|
||||
}
|
||||
first := sources[0]
|
||||
first.Destination = configkit.Keys("caller")
|
||||
if again := loaded.Sources(); len(again) != 1 || again[0].Destination.String() != "" || again[0].DestinationUnresolved {
|
||||
if again := loaded.Sources(); len(again) != 1 || again[0].Destination.String() != "" || again[0].DestinationIsFallback {
|
||||
t.Fatalf("source evidence changed after caller mutation: %#v", again)
|
||||
}
|
||||
}
|
||||
@@ -556,7 +556,7 @@ func TestFacadeSequenceReplacementKeepsTypedAndHistoricalEvidenceAligned(t *test
|
||||
directory := t.TempDir()
|
||||
mainPath := filepath.Join(directory, "main.yml")
|
||||
underlayPath := filepath.Join(directory, "underlay.yml")
|
||||
if err := os.WriteFile(mainPath, []byte(`configkit:
|
||||
if err := os.WriteFile(mainPath, []byte(`config:
|
||||
sequences:
|
||||
- destination: [entries]
|
||||
key: id
|
||||
@@ -571,7 +571,7 @@ entries:
|
||||
`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(underlayPath, []byte(`configkit:
|
||||
if err := os.WriteFile(underlayPath, []byte(`config:
|
||||
imports:
|
||||
- glob: records/*.yml
|
||||
destination: [entries]
|
||||
@@ -598,7 +598,7 @@ entries:
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.File(underlayPath), Imports: true}}}},
|
||||
Definition: configkit.Definition[configuration]{
|
||||
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("entries"), "id")},
|
||||
Secret: func(path configkit.Path) bool {
|
||||
Sensitive: func(path configkit.Path) bool {
|
||||
return path.String() == "/entries/0/token" || path.String() == "/entries/1/token"
|
||||
},
|
||||
Resolve: func(value *configuration, resolution *configkit.Resolution) error {
|
||||
@@ -611,8 +611,8 @@ entries:
|
||||
if resolution.IsExplicit(configkit.Keys("entries").Index(0).Key("omitted")) {
|
||||
t.Error("resolver treated an omitted field as explicit")
|
||||
}
|
||||
info, found := resolution.InputInfo(configkit.Keys("entries").Index(0).Key("count"))
|
||||
if !found || info.Origin.Layer != configkit.LayerBase {
|
||||
info, found := resolution.EntryInfo(configkit.Keys("entries").Index(0).Key("count"))
|
||||
if !found || info.Origin.Layer != configkit.LayerMain {
|
||||
t.Errorf("resolver current-record input info = %#v, %v", info, found)
|
||||
}
|
||||
for _, id := range []string{"source-3", "source-4"} {
|
||||
@@ -646,7 +646,7 @@ entries:
|
||||
t.Fatalf("callback sources = %#v; contexts = %#v", callbackItems, callbackContexts)
|
||||
}
|
||||
for _, source := range callbackItems {
|
||||
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationUnresolved {
|
||||
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationIsFallback {
|
||||
t.Errorf("callback item source = %#v", source)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
type movedVersionItem struct {
|
||||
@@ -64,7 +64,7 @@ func TestMainVersionDecodingFollowsSequenceMovement(t *testing.T) {
|
||||
t.Fatal("unrelated underlay payload was treated as the main declaration")
|
||||
}
|
||||
for _, source := range loaded.Sources() {
|
||||
if source.Layer == configkit.LayerBase && source.VersionCheck.Path.String() != path.String() {
|
||||
if source.Layer == configkit.LayerMain && source.VersionEvidence.Path.String() != path.String() {
|
||||
t.Fatal("effective movement changed source-local version evidence")
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ func TestUnderlayVersionProtectionUsesMainRecordPosition(t *testing.T) {
|
||||
want := configkit.CodeUnderlayVersion
|
||||
if imported {
|
||||
files := configkit.NewFilesystem("lower", fstest.MapFS{
|
||||
"main.yml": {Data: []byte("configkit: {imports: [{path: fragment.yml}]}\n")},
|
||||
"main.yml": {Data: []byte("config: {imports: [{path: fragment.yml}]}\n")},
|
||||
"fragment.yml": {Data: []byte(payload)},
|
||||
})
|
||||
lower.Source, lower.Imports = files.File("main.yml"), true
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFacadeSourceVersionEvidenceFollowsAssembledSequenceRanges(t *testing.T) {
|
||||
@@ -25,7 +25,7 @@ func TestFacadeSourceVersionEvidenceFollowsAssembledSequenceRanges(t *testing.T)
|
||||
definition := configkit.Definition[map[string]any]{
|
||||
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("records"), "id")},
|
||||
Version: &configkit.VersionRule{Path: configkit.Keys("records").Index(0).Key("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }},
|
||||
Secret: func(path configkit.Path) bool { return sensitivePath != "" && path.String() == sensitivePath },
|
||||
Sensitive: func(path configkit.Path) bool { return sensitivePath != "" && path.String() == sensitivePath },
|
||||
}
|
||||
first := "records: [{id: first, version: first-version}]\n"
|
||||
child := "records: [{id: child, version: PRIVATE-VERSION-CANARY}]\n"
|
||||
@@ -40,7 +40,7 @@ func TestFacadeSourceVersionEvidenceFollowsAssembledSequenceRanges(t *testing.T)
|
||||
})
|
||||
} else {
|
||||
filesystem := configkit.NewFilesystem("versions", fstest.MapFS{
|
||||
"main.yml": {Data: []byte("configkit: {imports: [{path: child.yml}]}\n" + first)},
|
||||
"main.yml": {Data: []byte("config: {imports: [{path: child.yml}]}\n" + first)},
|
||||
"child.yml": {Data: []byte(child)},
|
||||
})
|
||||
result, err = configkit.Load(filesystem.File("main.yml"), configkit.Options[map[string]any]{
|
||||
@@ -61,7 +61,7 @@ func TestFacadeSourceVersionEvidenceFollowsAssembledSequenceRanges(t *testing.T)
|
||||
}
|
||||
}
|
||||
sources := result.Sources()
|
||||
check := sources[len(sources)-1].VersionCheck
|
||||
check := sources[len(sources)-1].VersionEvidence
|
||||
if check == nil || check.Path.String() != "/records/0/version" || check.Location == nil || check.Location.SourceID != sources[len(sources)-1].ID {
|
||||
t.Fatalf("source-local evidence changed: %#v", check)
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func TestFacadeSourceVersionOwnershipAcceptsContextualIndexes(t *testing.T) {
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Bytes("lower", []byte(child))}}}},
|
||||
})
|
||||
} else {
|
||||
filesystem := configkit.NewFilesystem("versions", fstest.MapFS{"main.yml": {Data: []byte("configkit: {imports: [{path: child.yml}]}\n" + first)}, "child.yml": {Data: []byte(child)}})
|
||||
filesystem := configkit.NewFilesystem("versions", fstest.MapFS{"main.yml": {Data: []byte("config: {imports: [{path: child.yml}]}\n" + first)}, "child.yml": {Data: []byte(child)}})
|
||||
result, err = configkit.Load(filesystem.File("main.yml"), configkit.Options[map[string]any]{Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired, Imports: true})
|
||||
}
|
||||
if err != nil || result == nil {
|
||||
@@ -121,7 +121,7 @@ func TestFacadeItemMainKeepsDefaultVersionOwnership(t *testing.T) {
|
||||
helper = "other: {version: permitted}\n"
|
||||
}
|
||||
filesystem := configkit.NewFilesystem("item-main", fstest.MapFS{
|
||||
"main.yml": {Data: []byte(body + "configkit: {imports: [{path: helper.yml}]}\n")}, "helper.yml": {Data: []byte(helper)},
|
||||
"main.yml": {Data: []byte(body + "config: {imports: [{path: helper.yml}]}\n")}, "helper.yml": {Data: []byte(helper)},
|
||||
})
|
||||
checks := 0
|
||||
definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
||||
@@ -209,7 +209,7 @@ func TestFacadeSourceVersionEvidenceRedactsCollidingShapes(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
foundMapping = true
|
||||
if check := source.VersionCheck; check == nil || !check.Version.Present || !check.Redacted || check.Version.Text != "" {
|
||||
if check := source.VersionEvidence; check == nil || !check.Version.Present || !check.Redacted || check.Version.Text != "" {
|
||||
t.Errorf("reverse=%v: schema-tagged header was not redacted", reverse)
|
||||
}
|
||||
}
|
||||
@@ -223,7 +223,7 @@ func TestFacadeSourceVersionModesPreserveImportOptIn(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, mode := range []configkit.SourceVersionMode{configkit.SourceVersionModeIfPresent, configkit.SourceVersionModeRequired} {
|
||||
for _, underlay := range []bool{false, true} {
|
||||
bad := configkit.Bytes("disabled-imports", []byte("version: accepted\nconfigkit: {imports: [{path: missing.yml}]}\nvalue: visible\n"))
|
||||
bad := configkit.Bytes("disabled-imports", []byte("version: accepted\nconfig: {imports: [{path: missing.yml}]}\nvalue: visible\n"))
|
||||
definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
||||
Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil },
|
||||
}}
|
||||
@@ -246,7 +246,7 @@ func TestFacadeSourceVersionModesPreserveImportOptIn(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
diagnostics := failure.Diagnostics()
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeImportDisabled || diagnostics[0].Path.String() != "/configkit" ||
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeImportDisabled || diagnostics[0].Path.String() != "/config" ||
|
||||
diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != wantSource {
|
||||
t.Errorf("mode=%v underlay=%v: diagnostics=%#v", mode, underlay, diagnostics)
|
||||
}
|
||||
@@ -265,7 +265,7 @@ func TestFacadeSourceVersionAbsenceKeepsCompleteEvidencePath(t *testing.T) {
|
||||
{"unavailable index", "meta: [null, null, null, {version: main}]\n", "meta: []\n", configkit.Keys("meta").Index(3).Key("version")},
|
||||
} {
|
||||
for _, mode := range []configkit.SourceVersionMode{configkit.SourceVersionModeIfPresent, configkit.SourceVersionModeRequired} {
|
||||
var callbackEvidence *configkit.SourceVersionCheck
|
||||
var callbackEvidence *configkit.SourceVersionEvidence
|
||||
loaded, err := configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte(test.main))}, configkit.LayeredOptions[map[string]any]{
|
||||
SourceVersionMode: mode,
|
||||
Definition: configkit.Definition[map[string]any]{
|
||||
@@ -275,7 +275,7 @@ func TestFacadeSourceVersionAbsenceKeepsCompleteEvidencePath(t *testing.T) {
|
||||
if !found {
|
||||
t.Fatal("missing callback source")
|
||||
}
|
||||
callbackEvidence = source.VersionCheck
|
||||
callbackEvidence = source.VersionEvidence
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -283,7 +283,7 @@ func TestFacadeSourceVersionAbsenceKeepsCompleteEvidencePath(t *testing.T) {
|
||||
Source: configkit.Bytes("underlay", []byte(test.underlay)), Destination: configkit.Keys("under"),
|
||||
}}}},
|
||||
})
|
||||
var evidence *configkit.SourceVersionCheck
|
||||
var evidence *configkit.SourceVersionEvidence
|
||||
if mode == configkit.SourceVersionModeRequired {
|
||||
var failure *configkit.LoadError
|
||||
if loaded != nil || !errors.As(err, &failure) {
|
||||
@@ -293,12 +293,12 @@ func TestFacadeSourceVersionAbsenceKeepsCompleteEvidencePath(t *testing.T) {
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeVersionError || diagnostics[0].Path.String() != test.path.String() {
|
||||
t.Fatalf("%s: required diagnostics=%#v", test.name, diagnostics)
|
||||
}
|
||||
evidence = failure.Sources()[1].VersionCheck
|
||||
evidence = failure.Sources()[1].VersionEvidence
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", test.name, err)
|
||||
}
|
||||
evidence = loaded.Sources()[1].VersionCheck
|
||||
evidence = loaded.Sources()[1].VersionEvidence
|
||||
if callbackEvidence == nil || callbackEvidence.Path.String() != test.path.String() {
|
||||
t.Errorf("%s: callback path is incomplete", test.name)
|
||||
}
|
||||
@@ -365,7 +365,7 @@ func TestFacadeSourceVersionMissingHeaderDoesNotOverlapControl(t *testing.T) {
|
||||
if child["value"] != "good" {
|
||||
t.Fatal("valid sibling inheritance was not applied")
|
||||
}
|
||||
check := loaded.Sources()[1].VersionCheck
|
||||
check := loaded.Sources()[1].VersionEvidence
|
||||
if check == nil || check.Version.Present || check.Path.String() != path.String() {
|
||||
t.Fatal("optional omission evidence was not retained")
|
||||
}
|
||||
@@ -544,16 +544,16 @@ func TestFacadeSourceVersionEvidenceIsSanitizedAndDetached(t *testing.T) {
|
||||
var callbackSource configkit.SourceInfo
|
||||
var callbackContext configkit.ResourceContext
|
||||
definition := configkit.Definition[map[string]any]{
|
||||
Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }},
|
||||
Secret: func(candidate configkit.Path) bool { return candidate.String() == "/service/version" },
|
||||
Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }},
|
||||
Sensitive: func(candidate configkit.Path) bool { return candidate.String() == "/service/version" },
|
||||
Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
|
||||
var found bool
|
||||
callbackSource, found = resolution.Source("source-1")
|
||||
if !found {
|
||||
t.Fatal("resolution callback could not query the acquired source")
|
||||
}
|
||||
if callbackSource.VersionCheck == nil || !callbackSource.VersionCheck.Redacted || callbackSource.VersionCheck.Version.Text != "" {
|
||||
t.Fatalf("callback source did not conservatively redact evidence: %#v", callbackSource.VersionCheck)
|
||||
if callbackSource.VersionEvidence == nil || !callbackSource.VersionEvidence.Redacted || callbackSource.VersionEvidence.Version.Text != "" {
|
||||
t.Fatalf("callback source did not conservatively redact evidence: %#v", callbackSource.VersionEvidence)
|
||||
}
|
||||
callbackContext, found = resolution.SourceContext("source-1")
|
||||
if !found || callbackContext.FS == nil || callbackContext.BaseDir != "." {
|
||||
@@ -568,17 +568,17 @@ func TestFacadeSourceVersionEvidenceIsSanitizedAndDetached(t *testing.T) {
|
||||
if err != nil || loaded == nil {
|
||||
t.Fatalf("LoadLayered = %#v, %v", loaded, err)
|
||||
}
|
||||
if callbackSource.VersionCheck == nil || !callbackSource.VersionCheck.Redacted || callbackSource.VersionCheck.Version.Text != "" {
|
||||
t.Fatalf("callback evidence lost its conservative snapshot: %#v", callbackSource.VersionCheck)
|
||||
if callbackSource.VersionEvidence == nil || !callbackSource.VersionEvidence.Redacted || callbackSource.VersionEvidence.Version.Text != "" {
|
||||
t.Fatalf("callback evidence lost its conservative snapshot: %#v", callbackSource.VersionEvidence)
|
||||
}
|
||||
callbackSource.VersionCheck.Path = configkit.Keys("mutated-callback")
|
||||
callbackSource.VersionCheck.Version.Text = canary
|
||||
callbackSource.VersionCheck.Location.Line = 98
|
||||
callbackSource.VersionEvidence.Path = configkit.Keys("mutated-callback")
|
||||
callbackSource.VersionEvidence.Version.Text = canary
|
||||
callbackSource.VersionEvidence.Location.Line = 98
|
||||
finalSources := loaded.Sources()
|
||||
if len(finalSources) != 1 || finalSources[0].Path != path || finalSources[0].VersionCheck == nil {
|
||||
if len(finalSources) != 1 || finalSources[0].Path != path || finalSources[0].VersionEvidence == nil {
|
||||
t.Fatalf("final sources = %#v", finalSources)
|
||||
}
|
||||
check := finalSources[0].VersionCheck
|
||||
check := finalSources[0].VersionEvidence
|
||||
if check.Path.String() != "/version" || check.Version != (configkit.Version{Present: true}) || !check.Redacted ||
|
||||
check.Location == nil || check.Location.SourceID != finalSources[0].ID || check.Location.Line != 1 {
|
||||
t.Fatalf("final classified evidence = %#v", check)
|
||||
@@ -594,9 +594,9 @@ func TestFacadeSourceVersionEvidenceIsSanitizedAndDetached(t *testing.T) {
|
||||
check.Redacted = false
|
||||
check.Version.Text = "mutated"
|
||||
repeated := loaded.Sources()
|
||||
if len(repeated) != 1 || repeated[0].VersionCheck.Path.String() != "/version" ||
|
||||
repeated[0].VersionCheck.Location.Line != 1 || !repeated[0].VersionCheck.Redacted ||
|
||||
repeated[0].VersionCheck.Version.Text != "" {
|
||||
if len(repeated) != 1 || repeated[0].VersionEvidence.Path.String() != "/version" ||
|
||||
repeated[0].VersionEvidence.Location.Line != 1 || !repeated[0].VersionEvidence.Redacted ||
|
||||
repeated[0].VersionEvidence.Version.Text != "" {
|
||||
t.Fatalf("source evidence was aliased or re-read after source removal: %#v", repeated)
|
||||
}
|
||||
|
||||
@@ -612,14 +612,14 @@ func TestFacadeSourceVersionEvidenceIsSanitizedAndDetached(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil || public == nil || len(public.Sources()) != 1 || public.Sources()[0].VersionCheck == nil {
|
||||
if err != nil || public == nil || len(public.Sources()) != 1 || public.Sources()[0].VersionEvidence == nil {
|
||||
t.Fatalf("unclassified source evidence = %#v, %v", public, err)
|
||||
}
|
||||
if got := public.Sources()[0].VersionCheck; got.Redacted || got.Version.Text != publicText || !got.Version.Present {
|
||||
if got := public.Sources()[0].VersionEvidence; got.Redacted || got.Version.Text != publicText || !got.Version.Present {
|
||||
t.Fatalf("unclassified evidence = %#v", got)
|
||||
}
|
||||
if publicCallback.VersionCheck == nil || !publicCallback.VersionCheck.Redacted || publicCallback.VersionCheck.Version.Text != "" {
|
||||
t.Fatalf("unclassified callback evidence was not initially conservative: %#v", publicCallback.VersionCheck)
|
||||
if publicCallback.VersionEvidence == nil || !publicCallback.VersionEvidence.Redacted || publicCallback.VersionEvidence.Version.Text != "" {
|
||||
t.Fatalf("unclassified callback evidence was not initially conservative: %#v", publicCallback.VersionEvidence)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,7 +636,7 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
|
||||
Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { checked++; return nil },
|
||||
}}
|
||||
path := filepath.Join(t.TempDir(), "main.yml")
|
||||
contents := "version: " + canary + "\nconfigkit:\n imports:\n - path: missing.yml\n required: true\n"
|
||||
contents := "version: " + canary + "\nconfig:\n imports:\n - path: missing.yml\n required: true\n"
|
||||
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -650,7 +650,7 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
|
||||
if checked != 1 {
|
||||
t.Fatalf("main declaration checker calls = %d; error %v", checked, err)
|
||||
}
|
||||
check := failure.Sources()[0].VersionCheck
|
||||
check := failure.Sources()[0].VersionEvidence
|
||||
if check == nil || !check.Redacted || !check.Version.Present || check.Version.Text != "" {
|
||||
t.Fatalf("early acquisition evidence was not conservatively redacted: %#v; sources %#v", check, failure.Sources())
|
||||
}
|
||||
@@ -666,14 +666,14 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
|
||||
t.Fatalf("malformed version error = %v", err)
|
||||
}
|
||||
sources := loadError.Sources()
|
||||
if len(sources) != 1 || sources[0].VersionCheck == nil || !sources[0].VersionCheck.Redacted ||
|
||||
!sources[0].VersionCheck.Version.Present || sources[0].VersionCheck.Version.Text != "" || sources[0].VersionCheck.Path.String() != "/version" {
|
||||
if len(sources) != 1 || sources[0].VersionEvidence == nil || !sources[0].VersionEvidence.Redacted ||
|
||||
!sources[0].VersionEvidence.Version.Present || sources[0].VersionEvidence.Version.Text != "" || sources[0].VersionEvidence.Path.String() != "/version" {
|
||||
t.Fatalf("failed-load source evidence = %#v", sources)
|
||||
}
|
||||
sources[0].VersionCheck.Path = configkit.Keys("mutated-error")
|
||||
sources[0].VersionCheck.Version.Text = "mutated"
|
||||
sources[0].VersionCheck.Location.Line = 99
|
||||
if again := loadError.Sources()[0].VersionCheck; again.Path.String() != "/version" || again.Version.Text != "" || again.Location.Line == 99 {
|
||||
sources[0].VersionEvidence.Path = configkit.Keys("mutated-error")
|
||||
sources[0].VersionEvidence.Version.Text = "mutated"
|
||||
sources[0].VersionEvidence.Location.Line = 99
|
||||
if again := loadError.Sources()[0].VersionEvidence; again.Path.String() != "/version" || again.Version.Text != "" || again.Location.Line == 99 {
|
||||
t.Fatalf("LoadError source evidence was aliased: %#v", again)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
@@ -707,7 +707,7 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
|
||||
if strings.Contains(err.Error(), test.canary) {
|
||||
t.Fatalf("late failure disclosed source-version canary: %v", err)
|
||||
}
|
||||
check := failure.Sources()[0].VersionCheck
|
||||
check := failure.Sources()[0].VersionEvidence
|
||||
if check == nil || !check.Redacted || !check.Version.Present || check.Version.Text != "" {
|
||||
t.Fatalf("late-failure evidence was not conservatively redacted: %#v", check)
|
||||
}
|
||||
@@ -715,7 +715,7 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
|
||||
}
|
||||
|
||||
defaultResult, err := configkit.Load(configkit.Bytes("default", []byte("version: 7\n")), configkit.Options[map[string]any]{})
|
||||
if err != nil || defaultResult == nil || len(defaultResult.Sources()) != 1 || defaultResult.Sources()[0].VersionCheck != nil {
|
||||
if err != nil || defaultResult == nil || len(defaultResult.Sources()) != 1 || defaultResult.Sources()[0].VersionEvidence != nil {
|
||||
t.Fatalf("default source version check = %#v, %v", defaultResult, err)
|
||||
}
|
||||
|
||||
@@ -728,7 +728,7 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
|
||||
}
|
||||
sawCLI := false
|
||||
for _, source := range withCLI.Sources() {
|
||||
if source.Kind == configkit.SourceKindCLI && source.VersionCheck != nil {
|
||||
if source.Kind == configkit.SourceKindCLI && source.VersionEvidence != nil {
|
||||
t.Fatalf("synthetic CLI source acquired declaration evidence: %#v", source)
|
||||
}
|
||||
sawCLI = sawCLI || source.Kind == configkit.SourceKindCLI
|
||||
@@ -764,14 +764,14 @@ func TestFacadeSourceVersionEvidencePreservesAbsenceAndNull(t *testing.T) {
|
||||
t.Fatalf("checker calls = %#v; absent contribution must skip while null is checked", checked)
|
||||
}
|
||||
sources := loaded.Sources()
|
||||
if len(sources) != 3 || sources[1].VersionCheck == nil || sources[2].VersionCheck == nil {
|
||||
if len(sources) != 3 || sources[1].VersionEvidence == nil || sources[2].VersionEvidence == nil {
|
||||
t.Fatalf("source version evidence = %#v", sources)
|
||||
}
|
||||
absent := sources[1].VersionCheck
|
||||
absent := sources[1].VersionEvidence
|
||||
if absent.Version.Present || absent.Path.String() != "/version" || absent.Location == nil || absent.Redacted {
|
||||
t.Fatalf("optional omission evidence = %#v", absent)
|
||||
}
|
||||
null := sources[2].VersionCheck
|
||||
null := sources[2].VersionEvidence
|
||||
if !null.Version.Present || !null.Version.Null || null.Version.Text != "" || null.Path.String() != "/version" {
|
||||
t.Fatalf("null declaration evidence = %#v", null)
|
||||
}
|
||||
@@ -807,7 +807,7 @@ func TestFacadeSourceVersionLifecycleKeepsConsumedHeadersOutOfPayloadHistory(t *
|
||||
},
|
||||
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
|
||||
current.Service.Resolved = "resolved once"
|
||||
resolution.Record(configkit.Keys("service", "resolved"), configkit.ChangeNormalization, "derive resolved value")
|
||||
resolution.RecordNormalization(configkit.Keys("service", "resolved"), "derive resolved value")
|
||||
return nil
|
||||
},
|
||||
Validate: func(current *configuration, validation *configkit.Validation) {
|
||||
@@ -865,8 +865,8 @@ func TestFacadeSourceVersionLifecycleKeepsConsumedHeadersOutOfPayloadHistory(t *
|
||||
t.Fatalf("consumed declarations affected effective rendering:\n%s\n%s", firstEffective.String(), secondEffective.String())
|
||||
}
|
||||
sourcesA, sourcesB := first.Sources(), second.Sources()
|
||||
if len(sourcesA) != 2 || len(sourcesB) != 2 || sourcesA[1].VersionCheck == nil || sourcesB[1].VersionCheck == nil ||
|
||||
sourcesA[1].VersionCheck.Version.Text != "underlay-release-a" || sourcesB[1].VersionCheck.Version.Text != "underlay-release-b" {
|
||||
if len(sourcesA) != 2 || len(sourcesB) != 2 || sourcesA[1].VersionEvidence == nil || sourcesB[1].VersionEvidence == nil ||
|
||||
sourcesA[1].VersionEvidence.Version.Text != "underlay-release-a" || sourcesB[1].VersionEvidence.Version.Text != "underlay-release-b" {
|
||||
t.Fatalf("sanitized evidence did not retain distinct accepted declarations: %#v / %#v", sourcesA, sourcesB)
|
||||
}
|
||||
versionExplanation, found := first.Explain(configkit.Keys("service", "version"))
|
||||
|
||||
50
facade_tier_history_test.go
Normal file
50
facade_tier_history_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package configkit_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestTierHistoryRecordsEachContributionOnce(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, middle := range []string{"mid", "low"} {
|
||||
t.Run(middle, func(t *testing.T) {
|
||||
result, err := configkit.LoadLayered[map[string]any](
|
||||
configkit.Input{Source: configkit.Bytes("main", []byte("x: high\n"))},
|
||||
configkit.LayeredOptions[map[string]any]{Underlays: []configkit.Underlay{
|
||||
{Inputs: []configkit.Input{{Source: configkit.Bytes("low", []byte("x: low\n"))}}},
|
||||
{Inputs: []configkit.Input{{Source: configkit.Bytes("middle", []byte("x: "+middle+"\n"))}}},
|
||||
}},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
history := result.Metadata().History(configkit.Keys("x"))
|
||||
if len(history) != 3 {
|
||||
t.Fatalf("history has %d records; want the three independent contributions", len(history))
|
||||
}
|
||||
explanation, found := result.Explain(configkit.Keys("x"))
|
||||
if !found || len(explanation.History) != len(history) {
|
||||
t.Fatal("explanation lost ordered tier contributions")
|
||||
}
|
||||
for index, sourceID := range []string{"source-2", "source-3", "source-1"} {
|
||||
change := history[index]
|
||||
if len(change.Origin.Locations) != 1 || change.Origin.Locations[0].SourceID != sourceID {
|
||||
t.Fatalf("contribution %d has origin %#v", index, change.Origin)
|
||||
}
|
||||
if len(explanation.History[index].Origin.Locations) != 1 || explanation.History[index].Origin.Locations[0] != change.Origin.Locations[0] {
|
||||
t.Fatalf("explanation contribution %d has a different source origin", index)
|
||||
}
|
||||
if index == 0 {
|
||||
if len(change.Previous) != 0 {
|
||||
t.Fatal("first source contribution has a previous origin")
|
||||
}
|
||||
} else if len(change.Previous) != 1 || len(change.Previous[0].Locations) != 1 ||
|
||||
change.Previous[0].Locations[0] != history[index-1].Origin.Locations[0] {
|
||||
t.Fatalf("contribution %d has incorrect previous origin %#v", index, change.Previous)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestUnderlaySequenceDescendantOrigins(t *testing.T) {
|
||||
@@ -16,7 +16,7 @@ func TestUnderlaySequenceDescendantOrigins(t *testing.T) {
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
bundle := configkit.NewFilesystem("bundle", fstest.MapFS{
|
||||
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: items.yml}]}\n")},
|
||||
"main.yml": &fstest.MapFile{Data: []byte("config: {imports: [{path: items.yml}]}\n")},
|
||||
"items.yml": &fstest.MapFile{Data: []byte("items: [{name: first, nested: [false]}]\n")},
|
||||
})
|
||||
tiers := []configkit.Underlay{}
|
||||
@@ -31,7 +31,7 @@ func TestUnderlaySequenceDescendantOrigins(t *testing.T) {
|
||||
Definition: configkit.Definition[map[string]any]{Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
|
||||
for _, pointer := range paths {
|
||||
path, _ := configkit.ParsePath(pointer)
|
||||
info, ok := resolution.InputInfo(path)
|
||||
info, ok := resolution.EntryInfo(path)
|
||||
if !ok {
|
||||
t.Errorf("missing resolver input %s", pointer)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
type versionCountingReader struct {
|
||||
|
||||
8
go.mod
8
go.mod
@@ -1,13 +1,13 @@
|
||||
module gitea.maximumdirect.net/eric/ConfigKit
|
||||
module go.maximumdirect.net/configkit
|
||||
|
||||
go 1.26.0
|
||||
go 1.27.0
|
||||
|
||||
require go.yaml.in/yaml/v4 v4.0.0-rc.6
|
||||
|
||||
require (
|
||||
gitea.maximumdirect.net/eric/ReleaseKit v0.4.1 // indirect
|
||||
github.com/yuin/goldmark/v2 v2.1.6 // indirect
|
||||
go.maximumdirect.net/releasekit v0.6.0 // indirect
|
||||
golang.org/x/mod v0.40.0 // indirect
|
||||
)
|
||||
|
||||
tool gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit
|
||||
tool go.maximumdirect.net/releasekit/cmd/releasekit
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1,7 +1,7 @@
|
||||
gitea.maximumdirect.net/eric/ReleaseKit v0.4.1 h1:Qn+mdebYOzCVCEXgCbuGSO/WHmJtItwaQ8e3XwY3qtI=
|
||||
gitea.maximumdirect.net/eric/ReleaseKit v0.4.1/go.mod h1:LVodqNQ3OIKv9NBq+fFeyMByd1lHY25YOmmpah/NaNI=
|
||||
github.com/yuin/goldmark/v2 v2.1.6 h1:igy0q9JcqovoFXFx/XjtU8sHVhcUHavPj2sA3EoKJUE=
|
||||
github.com/yuin/goldmark/v2 v2.1.6/go.mod h1:G6M4/qOFtfn01/o14BU1UR2Lo5N3S9Qo7xCuz/sHjGQ=
|
||||
go.maximumdirect.net/releasekit v0.6.0 h1:wbzMeQGeLjXigBSN9j9iJLf/CC3htjzt9iEwubezgy0=
|
||||
go.maximumdirect.net/releasekit v0.6.0/go.mod h1:QE4BLrAhVJTzi+/YTcVl21K/qZp9ojRLg5kuoy3Zb44=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
|
||||
|
||||
@@ -44,10 +44,10 @@ func TestDependencyImportBoundary(t *testing.T) {
|
||||
production := !strings.HasSuffix(path, "_test.go")
|
||||
rootProduction := production && filepath.Dir(path) == "."
|
||||
internalProduction := production && strings.HasPrefix(filepath.ToSlash(path), "internal/")
|
||||
if rootProduction && strings.HasPrefix(importPath, "gitea.maximumdirect.net/eric/ConfigKit/internal/") && importPath != "gitea.maximumdirect.net/eric/ConfigKit/internal/engine" {
|
||||
if rootProduction && strings.HasPrefix(importPath, "go.maximumdirect.net/configkit/internal/") && importPath != "go.maximumdirect.net/configkit/internal/engine" {
|
||||
t.Errorf("%s bypasses the engine boundary through %s", path, importPath)
|
||||
}
|
||||
if internalProduction && importPath == "gitea.maximumdirect.net/eric/ConfigKit" {
|
||||
if internalProduction && importPath == "go.maximumdirect.net/configkit" {
|
||||
t.Errorf("%s imports the public root package", path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,21 @@ package configkit
|
||||
import (
|
||||
"io"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
"go.maximumdirect.net/configkit/internal/engine"
|
||||
)
|
||||
|
||||
// ValueSnapshot is a detached, deterministic YAML representation of one value.
|
||||
// Present distinguishes absence or deletion from an explicit null value.
|
||||
// ValueSnapshot describes a saved value as consistently formatted YAML text.
|
||||
// It is a copy; changing it cannot affect later inspection. Present is false for
|
||||
// absence or deletion, while explicit null is present. Redacted reports whether
|
||||
// the value or a descendant was hidden.
|
||||
type ValueSnapshot struct {
|
||||
Present bool
|
||||
Redacted bool
|
||||
YAML string
|
||||
}
|
||||
|
||||
// ValueChange describes one explicit definition or audited domain operation.
|
||||
// ValueChange shows values before and after an input contribution or recorded
|
||||
// application operation, together with its origin, reasons, and dependencies.
|
||||
type ValueChange struct {
|
||||
Before ValueSnapshot
|
||||
After ValueSnapshot
|
||||
@@ -29,7 +32,8 @@ type SecretReferenceInfo struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// Explanation is a detached safe account of one effective or historical path.
|
||||
// Explanation describes a path's final value and earlier contributions, with
|
||||
// sensitive values hidden. It is a copy that cannot alter later inspection.
|
||||
type Explanation struct {
|
||||
Path Path
|
||||
Effective ValueSnapshot
|
||||
@@ -38,9 +42,10 @@ type Explanation struct {
|
||||
SecretReference *SecretReferenceInfo
|
||||
}
|
||||
|
||||
// Explain returns safe effective and chronological values for a known final or
|
||||
// historical path. Structural inheritance appears between source applications
|
||||
// and typed reconciliation. Unknown and malformed paths return false.
|
||||
// Explain returns the saved final value and earlier values for a known current
|
||||
// or historical path, hiding sensitive information. History follows processing
|
||||
// order: inheritance appears after source combination and before the transition
|
||||
// to typed values. Unknown and malformed paths return false.
|
||||
func (r *Result[T]) Explain(path Path) (Explanation, bool) {
|
||||
if r == nil {
|
||||
return Explanation{}, false
|
||||
@@ -49,8 +54,9 @@ func (r *Result[T]) Explain(path Path) (Explanation, bool) {
|
||||
return fromEngineExplanation(value), ok
|
||||
}
|
||||
|
||||
// WriteEffective writes deterministic redacted YAML from the frozen resolved
|
||||
// snapshot. The output is intended for inspection and is not promised reloadable.
|
||||
// WriteEffective writes consistently formatted YAML for the saved final
|
||||
// configuration, with sensitive values hidden. Later changes to Result.Config
|
||||
// do not change the output. It is for inspection and is not promised reloadable.
|
||||
// Writer errors, including short writes, are returned and may leave partial output.
|
||||
func (r *Result[T]) WriteEffective(output io.Writer) error {
|
||||
if r == nil {
|
||||
@@ -61,7 +67,7 @@ func (r *Result[T]) WriteEffective(output io.Writer) error {
|
||||
}
|
||||
|
||||
// WriteExplanation writes a deterministic human-readable rendering of Explain.
|
||||
// Unknown paths produce no output. Writer failures may leave partial output.
|
||||
// Unknown paths return an error without output. Writer failures may leave partial output.
|
||||
func (r *Result[T]) WriteExplanation(output io.Writer, path Path) error {
|
||||
if r == nil {
|
||||
var inspection *engine.Inspection
|
||||
|
||||
@@ -3,7 +3,7 @@ package engine
|
||||
import "strconv"
|
||||
|
||||
type acquisitionPolicy struct {
|
||||
importDefaults importDefaults
|
||||
importDefaults ImportDefaults
|
||||
sequencePolicy sequencePolicy
|
||||
importLimits importLimits
|
||||
}
|
||||
@@ -23,7 +23,6 @@ type inputAcquisitionRequest struct {
|
||||
role sourceVersionRole
|
||||
directiveContext rootDirectiveContext
|
||||
protectionRule *VersionRule
|
||||
deferAssembly bool
|
||||
ordinaryOverlay string
|
||||
}
|
||||
|
||||
@@ -38,9 +37,6 @@ func acquireInput(request inputAcquisitionRequest, policy acquisitionPolicy, sta
|
||||
destination := request.input.destination
|
||||
accounting := state.accounting
|
||||
versionState := state.versionState
|
||||
visited := tier.visited
|
||||
hostToken := tier.hostToken
|
||||
role := request.role
|
||||
sourceID := "source-" + strconv.Itoa(accounting.sources+1)
|
||||
selectedSource := source
|
||||
if source.kind == sourceSearch {
|
||||
@@ -99,67 +95,10 @@ func acquireInput(request inputAcquisitionRequest, policy acquisitionPolicy, sta
|
||||
return input, directiveDiagnostics
|
||||
}
|
||||
}
|
||||
var versionProcessing *sourceVersionProcessingResult
|
||||
if directiveContext == rootDirectiveContextSource {
|
||||
var sourceID string
|
||||
if parsed.source != nil {
|
||||
sourceID = parsed.source.ID
|
||||
}
|
||||
var processed sourceVersionProcessingResult
|
||||
var failures []Diagnostic
|
||||
if versionState != nil {
|
||||
processed, failures = versionState.processWithDirectiveContext(parsed.document, role, destination, sourceID, request.input.placementMode, directiveContext)
|
||||
} else {
|
||||
processed = processSourceVersionWithDirectiveContext(parsed.document, request.protectionRule, role, destination, sourceVersionModeDefault, directiveContext)
|
||||
failures = processed.diagnostics
|
||||
}
|
||||
if len(failures) != 0 {
|
||||
return input, failures
|
||||
}
|
||||
versionProcessing = &processed
|
||||
if role == sourceVersionMain && versionState != nil {
|
||||
versionState.mainProcessing = &processed
|
||||
}
|
||||
} else if directiveContext == rootDirectiveContextLayered {
|
||||
extracted, failures := extractRootDirectives(parsed.document, rootDirectiveContextLayered)
|
||||
if len(failures) != 0 {
|
||||
return input, failures
|
||||
}
|
||||
processed := sourceVersionProcessingResult{
|
||||
payload: extracted.body, imports: extracted.imports, sequences: extracted.sequences,
|
||||
directiveContext: rootDirectiveContextLayered,
|
||||
}
|
||||
if versionState != nil && versionState.mode != sourceVersionModeDefault {
|
||||
var sourceID string
|
||||
if parsed.source != nil {
|
||||
sourceID = parsed.source.ID
|
||||
}
|
||||
versionProcessed, versionFailures := versionState.process(extracted.body, role, destination, sourceID, request.input.placementMode)
|
||||
if len(versionFailures) != 0 {
|
||||
return input, versionFailures
|
||||
}
|
||||
versionProcessed.imports = extracted.imports
|
||||
versionProcessed.sequences = append(cloneSequenceDeclarations(extracted.sequences), versionProcessed.sequences...)
|
||||
versionProcessed.directiveContext = rootDirectiveContextLayered
|
||||
processed = versionProcessed
|
||||
if role == sourceVersionMain {
|
||||
versionState.mainProcessing = &processed
|
||||
}
|
||||
}
|
||||
versionProcessing = &processed
|
||||
} else if versionState != nil && versionState.mode != sourceVersionModeDefault {
|
||||
var sourceID string
|
||||
if parsed.source != nil {
|
||||
sourceID = parsed.source.ID
|
||||
}
|
||||
processed, failures := versionState.process(parsed.document, role, destination, sourceID, request.input.placementMode)
|
||||
if len(failures) != 0 {
|
||||
return input, failures
|
||||
}
|
||||
versionProcessing = &processed
|
||||
if role == sourceVersionMain {
|
||||
versionState.mainProcessing = &processed
|
||||
}
|
||||
request.directiveContext = directiveContext
|
||||
versionProcessing, diagnostics := processAcquiredInputVersion(parsed, request, versionState)
|
||||
if len(diagnostics) != 0 {
|
||||
return input, diagnostics
|
||||
}
|
||||
request.input.source = selectedSource
|
||||
if request.input.imports && selectedSource.kind != sourceNone {
|
||||
@@ -173,67 +112,144 @@ func acquireInput(request inputAcquisitionRequest, policy acquisitionPolicy, sta
|
||||
input.document = input.composition.root
|
||||
}
|
||||
} else {
|
||||
input.sources = sourceInfos(parsed.source)
|
||||
if versionProcessing != nil {
|
||||
input.document = versionProcessing.payload
|
||||
}
|
||||
if request.input.filesystem != nil && (selectedSource.kind == sourceNone || !activeOverlay(request.ordinaryOverlay)) {
|
||||
if diagnostic := validateFilesystemContext(selectedSource, request.input.filesystem, parsed); diagnostic != nil {
|
||||
return input, []Diagnostic{*diagnostic}
|
||||
}
|
||||
input, diagnostics = acquireStandaloneInput(input, request, versionProcessing, policy, tier)
|
||||
if len(diagnostics) != 0 {
|
||||
return input, diagnostics
|
||||
}
|
||||
}
|
||||
if !request.input.imports && input.document != nil {
|
||||
_, placed, err := placeDocumentWithMode(input.document, destination, request.input.placementMode, input.document.Location, parseLimits.document.Depth, parseLimits.document.Nodes)
|
||||
if err != nil {
|
||||
return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the source depth or node limit after destination placement")}
|
||||
}
|
||||
localPayload := input.document
|
||||
input.document = placed
|
||||
identity := importKeyForMainSource(parsed.source)
|
||||
var sequenceDeclarations []sequenceDeclarationSpec
|
||||
if versionProcessing != nil {
|
||||
sequenceDeclarations = cloneSequenceDeclarations(versionProcessing.sequences)
|
||||
}
|
||||
input.acquisition = []acquisitionScope{{
|
||||
owner: identity,
|
||||
destination: destination.clone(),
|
||||
contributions: []acquiredContribution{{
|
||||
source: cloneSourceInfo(*parsed.source), payload: localPayload,
|
||||
identity: identity, role: importRoleMain, scopeOwner: identity,
|
||||
document: placed, destination: destination.clone(), processing: versionProcessing,
|
||||
sequenceDeclarations: sequenceDeclarations,
|
||||
}},
|
||||
}}
|
||||
rawNodes := countInputNodes(parsed.document)
|
||||
placementNodes := destinationWrapperNodeCountForMode(destination, request.input.placementMode)
|
||||
if accounting.sources+1 > limits.sources || accounting.bytes+parsed.bytes > limits.bytes || accounting.nodes+rawNodes+placementNodes > limits.nodes {
|
||||
return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the aggregate input limit")}
|
||||
}
|
||||
if diagnostic := registerAcquiredSource(selectedSource, request.input.filesystem, parsed.source, visited, hostToken); diagnostic != nil {
|
||||
return input, []Diagnostic{*diagnostic}
|
||||
}
|
||||
accounting.sources++
|
||||
accounting.bytes += parsed.bytes
|
||||
accounting.nodes += rawNodes + placementNodes
|
||||
return completeAcquisitionContexts(input, request, tier)
|
||||
}
|
||||
|
||||
func acquireStandaloneInput(input assembledInput, request inputAcquisitionRequest, processing *sourceVersionProcessingResult, policy acquisitionPolicy, tier *tierAcquisitionState) (assembledInput, []Diagnostic) {
|
||||
parsed := input.parsed
|
||||
input.sources = sourceInfos(parsed.source)
|
||||
if processing != nil {
|
||||
input.document = processing.payload
|
||||
}
|
||||
if !activeOverlay(request.ordinaryOverlay) || request.input.imports {
|
||||
if input.resolvedFilesystem == nil && len(input.sources) != 0 && (selectedSource.kind == sourceFile || request.input.filesystem != nil || nativeContext(selectedSource, request.input.filesystem)) {
|
||||
resolved, diagnostic := resolveFilesystemContext(selectedSource, request.input.filesystem, parsed, tier.hostToken)
|
||||
if diagnostic != nil {
|
||||
return input, []Diagnostic{*diagnostic}
|
||||
}
|
||||
input.resolvedFilesystem = &resolved
|
||||
}
|
||||
contexts, diagnostic := contextsForResolvedSources(input.resolvedFilesystem, input.sources)
|
||||
if request.input.filesystem != nil && (input.source.kind == sourceNone || !activeOverlay(request.ordinaryOverlay)) {
|
||||
resolved, diagnostic := validateFilesystemContext(input.source, request.input.filesystem, parsed, tier.hostToken)
|
||||
if diagnostic != nil {
|
||||
return input, []Diagnostic{*diagnostic}
|
||||
}
|
||||
input.contexts = contexts
|
||||
input.resolvedFilesystem = &resolved
|
||||
}
|
||||
if request.input.imports || input.document == nil {
|
||||
return input, nil
|
||||
}
|
||||
|
||||
limits := policy.importLimits
|
||||
placement := request.input.destination
|
||||
documentLimits := defaultParseLimits().document
|
||||
_, placed, err := placeDocumentWithMode(input.document, placement, request.input.placementMode, input.document.Location, documentLimits.Depth, documentLimits.Nodes)
|
||||
if err != nil {
|
||||
return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the source depth or node limit after destination placement")}
|
||||
}
|
||||
localPayload := input.document
|
||||
input.document = placed
|
||||
identity := importKeyForMainSource(parsed.source)
|
||||
var sequenceDeclarations []sequenceDeclarationSpec
|
||||
if processing != nil {
|
||||
sequenceDeclarations = cloneSequenceDeclarations(processing.sequences)
|
||||
}
|
||||
input.acquisition = []acquisitionScope{{
|
||||
owner: identity,
|
||||
destination: placement.clone(),
|
||||
contributions: []acquiredContribution{{
|
||||
source: cloneSourceInfo(*parsed.source), payload: localPayload,
|
||||
identity: identity, role: importRoleMain, scopeOwner: identity,
|
||||
document: placed, destination: placement.clone(), processing: processing,
|
||||
sequenceDeclarations: sequenceDeclarations,
|
||||
}},
|
||||
}}
|
||||
rawNodes := countInputNodes(parsed.document)
|
||||
placementNodes := destinationWrapperNodeCountForMode(placement, request.input.placementMode)
|
||||
accounting := input.accounting
|
||||
if accounting.sources+1 > limits.sources || accounting.bytes+parsed.bytes > limits.bytes || accounting.nodes+rawNodes+placementNodes > limits.nodes {
|
||||
return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the aggregate input limit")}
|
||||
}
|
||||
if diagnostic := registerAcquiredSource(input.source, request.input.filesystem, parsed.source, tier.visited, tier.hostToken); diagnostic != nil {
|
||||
return input, []Diagnostic{*diagnostic}
|
||||
}
|
||||
accounting.sources++
|
||||
accounting.bytes += parsed.bytes
|
||||
accounting.nodes += rawNodes + placementNodes
|
||||
return input, nil
|
||||
}
|
||||
|
||||
func completeAcquisitionContexts(input assembledInput, request inputAcquisitionRequest, tier *tierAcquisitionState) (assembledInput, []Diagnostic) {
|
||||
if activeOverlay(request.ordinaryOverlay) && !request.input.imports {
|
||||
return input, nil
|
||||
}
|
||||
if input.resolvedFilesystem == nil && len(input.sources) != 0 && (input.source.kind == sourceFile || request.input.filesystem != nil || nativeContext(input.source, request.input.filesystem)) {
|
||||
resolved, diagnostic := resolveFilesystemContext(input.source, request.input.filesystem, input.parsed, tier.hostToken)
|
||||
if diagnostic != nil {
|
||||
return input, []Diagnostic{*diagnostic}
|
||||
}
|
||||
input.resolvedFilesystem = &resolved
|
||||
}
|
||||
contexts, diagnostic := contextsForResolvedSources(input.resolvedFilesystem, input.sources)
|
||||
if diagnostic != nil {
|
||||
return input, []Diagnostic{*diagnostic}
|
||||
}
|
||||
input.contexts = contexts
|
||||
return input, nil
|
||||
}
|
||||
|
||||
func processAcquiredInputVersion(parsed parsedSource, request inputAcquisitionRequest, state *sourceVersionState) (*sourceVersionProcessingResult, []Diagnostic) {
|
||||
sourceID := ""
|
||||
if parsed.source != nil {
|
||||
sourceID = parsed.source.ID
|
||||
}
|
||||
|
||||
var result sourceVersionProcessingResult
|
||||
hasResult := false
|
||||
updateMainProcessing := false
|
||||
switch request.directiveContext {
|
||||
case rootDirectiveContextSource, rootDirectiveContextLayered:
|
||||
if state != nil {
|
||||
processed, diagnostics := state.processWithDirectiveContext(
|
||||
parsed.document, request.role, request.input.destination, sourceID,
|
||||
request.input.placementMode, request.directiveContext,
|
||||
)
|
||||
if len(diagnostics) != 0 {
|
||||
return nil, diagnostics
|
||||
}
|
||||
result = processed
|
||||
updateMainProcessing = request.role == sourceVersionMain
|
||||
} else {
|
||||
result = processSourceVersionWithDirectiveContext(
|
||||
parsed.document, request.protectionRule, request.role,
|
||||
request.input.destination, sourceVersionModeDefault, request.directiveContext,
|
||||
)
|
||||
if len(result.diagnostics) != 0 {
|
||||
return nil, result.diagnostics
|
||||
}
|
||||
}
|
||||
hasResult = true
|
||||
default:
|
||||
if state != nil && state.mode != sourceVersionModeDefault {
|
||||
processed, diagnostics := state.process(
|
||||
parsed.document, request.role, request.input.destination,
|
||||
sourceID, request.input.placementMode,
|
||||
)
|
||||
if len(diagnostics) != 0 {
|
||||
return nil, diagnostics
|
||||
}
|
||||
result = processed
|
||||
hasResult = true
|
||||
updateMainProcessing = request.role == sourceVersionMain
|
||||
}
|
||||
}
|
||||
if !hasResult {
|
||||
return nil, nil
|
||||
}
|
||||
if updateMainProcessing {
|
||||
completed := result
|
||||
state.mainProcessing = &completed
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func registerAcquiredSource(source Source, context *FilesystemContext, info *SourceInfo, visited map[resourceIdentity]importRoute, hostToken *namespaceToken) *Diagnostic {
|
||||
if visited == nil || info == nil || info.Path == "" {
|
||||
return nil
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user