Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -18,8 +18,3 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|||||||
GNU Lesser General Public License for more details.
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
Third-party code and assets are subject to their respective license terms.
|
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
|
# 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
|
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
|
## Load a configuration file
|
||||||
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).
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
import "go.maximumdirect.net/configkit"
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
Workers int `yaml:"workers"`
|
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
|
## Design philosophy
|
||||||
|
|
||||||
ConfigKit favors one coherent, predictable configuration model over flexibility
|
ConfigKit favors consistent, predictable rules across applications. A load returns
|
||||||
for reproducing each consumer's conventions. Its goal is to let applications
|
the final configuration and metadata that distinguish supplied values such as
|
||||||
load once, inspect where final values came from, and distinguish explicit values
|
`false` and `0` from omissions. Applications keep their domain rules and runtime
|
||||||
from omissions with minimal configuration bookkeeping. Applications retain their
|
initialization.
|
||||||
domain rules and execution responsibilities.
|
|
||||||
|
|
||||||
Loading is synchronous and complete on return. Applications normally load once
|
Within one layer, independent contributions combine strictly: conflicting
|
||||||
near startup and then use the final configuration and metadata; ConfigKit stays
|
values are errors. Across layers, higher-priority settings can replace
|
||||||
inactive until explicitly called again. Applications may initiate additional
|
lower-priority settings, while mappings combine by key. Identified lists have
|
||||||
loads as needed. See the [loading lifecycle](docs/config.md#completion-and-resource-lifetime)
|
their declared combination rules. See the [worked combination
|
||||||
for completion and ownership details.
|
examples](docs/config.md#strict-document-composition) for the details and
|
||||||
|
exceptions.
|
||||||
|
|
||||||
Within a configuration tier, independent contributions compose strictly: shared
|
Loading finishes before the call returns. ConfigKit then stays inactive until you
|
||||||
mappings combine, while overlapping non-mapping definitions are errors. Between
|
call it again; inspection uses the saved result. See
|
||||||
precedence tiers, mappings combine recursively, retaining omitted lower-priority
|
[completion and resource lifetime](docs/config.md#completion-and-resource-lifetime).
|
||||||
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.
|
|
||||||
|
|
||||||
The [architecture goals](docs/policy/architecture.md#success-criteria) explain
|
The [architecture goals](docs/policy/architecture.md#success-criteria) and
|
||||||
these priorities and how genuine new needs can justify extending the shared
|
[ADRs](docs/adr) explain design decisions, including shared import and inheritance
|
||||||
model. The [future feature list](docs/roadmap/future.md) preserves candidates for
|
ordering. [Future work](docs/roadmap/future.md) lists possible extensions.
|
||||||
consideration in future releases.
|
Contributors can start with the [development guide](docs/development.md);
|
||||||
|
release maintainers use the [release procedure](docs/release.md).
|
||||||
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.
|
|
||||||
|
|
||||||
ConfigKit code is licensed under the [LGPL v3.0 or later](COPYRIGHT).
|
ConfigKit code is licensed under the [LGPL v3.0 or later](COPYRIGHT).
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package configkit
|
package configkit
|
||||||
|
|
||||||
import "gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
import "go.maximumdirect.net/configkit/internal/engine"
|
||||||
|
|
||||||
func fromEngineLocation(value engine.Location) Location {
|
func fromEngineLocation(value engine.Location) Location {
|
||||||
return Location{SourceID: value.SourceID, Line: value.Line, Column: value.Column}
|
return Location{SourceID: value.SourceID, Line: value.Line, Column: value.Column}
|
||||||
@@ -46,21 +46,21 @@ func fromEngineSourceInfo(value engine.SourceInfo) SourceInfo {
|
|||||||
return SourceInfo{
|
return SourceInfo{
|
||||||
ID: value.ID, Label: value.Label, Path: value.Path, Destination: fromEnginePath(value.Destination),
|
ID: value.ID, Label: value.Label, Path: value.Path, Destination: fromEnginePath(value.Destination),
|
||||||
PlacementMode: PlacementMode(engine.SourceInfoPlacementModeForFacade(value)),
|
PlacementMode: PlacementMode(engine.SourceInfoPlacementModeForFacade(value)),
|
||||||
DestinationUnresolved: engine.SourceInfoDestinationUnresolvedForFacade(value),
|
DestinationIsFallback: engine.SourceInfoDestinationUnresolvedForFacade(value),
|
||||||
Layer: Layer(value.Layer), Kind: SourceKind(value.Kind), UnderlayIndex: value.UnderlayIndex,
|
Layer: Layer(value.Layer), Kind: SourceKind(value.Kind), UnderlayIndex: value.UnderlayIndex,
|
||||||
ImportedFrom: fromEngineLocationPointer(value.ImportedFrom), VersionCheck: fromEngineSourceVersionCheck(value.VersionCheck),
|
ImportedFrom: fromEngineLocationPointer(value.ImportedFrom), VersionEvidence: fromEngineSourceVersionEvidence(value.VersionCheck),
|
||||||
ImportSelections: fromEngineImportSelections(engine.SourceInfoImportSelectionsForFacade(value)),
|
EmptyImportSelections: fromEngineEmptyImportSelections(engine.SourceInfoImportSelectionsForFacade(value)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func fromEngineImportSelections(values []engine.ImportSelectionForFacade) []ImportSelection {
|
func fromEngineEmptyImportSelections(values []engine.ImportSelectionForFacade) []EmptyImportSelection {
|
||||||
if values == nil {
|
if values == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
result := make([]ImportSelection, len(values))
|
result := make([]EmptyImportSelection, len(values))
|
||||||
for index, value := range values {
|
for index, value := range values {
|
||||||
result[index] = ImportSelection{
|
result[index] = EmptyImportSelection{
|
||||||
Path: fromEnginePath(value.Path), Location: fromEngineLocation(value.Location),
|
Path: fromEnginePath(value.Path), Location: fromEngineLocation(value.Location),
|
||||||
Selector: ImportSelector(value.Selector), Outcome: ImportSelectionOutcome(value.Outcome),
|
Selector: ImportSelector(value.Selector),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -82,18 +82,18 @@ func toEngineOverrides(values []Override) []engine.Override {
|
|||||||
func toEngineImportDefaults(value ImportDefaults) engine.ImportDefaults {
|
func toEngineImportDefaults(value ImportDefaults) engine.ImportDefaults {
|
||||||
return engine.ImportDefaults{
|
return engine.ImportDefaults{
|
||||||
Required: value.Required,
|
Required: value.Required,
|
||||||
Follow: value.Follow,
|
Follow: value.FollowSymlinks,
|
||||||
Hidden: value.Hidden,
|
Hidden: value.IncludeHidden,
|
||||||
Recursive: value.Recursive,
|
Recursive: value.Recursive,
|
||||||
Pattern: value.Pattern,
|
Pattern: value.Pattern,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fromEngineSourceVersionCheck(value *engine.SourceVersionCheck) *SourceVersionCheck {
|
func fromEngineSourceVersionEvidence(value *engine.SourceVersionCheck) *SourceVersionEvidence {
|
||||||
if value == nil {
|
if value == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &SourceVersionCheck{
|
return &SourceVersionEvidence{
|
||||||
Path: fromEnginePath(value.Path),
|
Path: fromEnginePath(value.Path),
|
||||||
Version: Version{Present: value.Version.Present, Null: value.Version.Null, Text: value.Version.Text},
|
Version: Version{Present: value.Version.Present, Null: value.Version.Null, Text: value.Version.Text},
|
||||||
Location: fromEngineLocationPointer(value.Location), Redacted: value.Redacted,
|
Location: fromEngineLocationPointer(value.Location), Redacted: value.Redacted,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package configkit
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
"go.maximumdirect.net/configkit/internal/engine"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoadError reports safe structured diagnostics for a failed configuration load.
|
// LoadError reports safe structured diagnostics for a failed configuration load.
|
||||||
@@ -50,6 +50,21 @@ func (e *LoadError) Sources() []SourceInfo {
|
|||||||
return result
|
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
|
// WriteDiagnostics writes diagnostics in canonical order. It escapes terminal
|
||||||
// control characters and propagates writer errors, including short writes.
|
// control characters and propagates writer errors, including short writes.
|
||||||
func WriteDiagnostics(output io.Writer, diagnostics []Diagnostic) error {
|
func WriteDiagnostics(output io.Writer, diagnostics []Diagnostic) error {
|
||||||
|
|||||||
22
diff.go
22
diff.go
@@ -1,15 +1,14 @@
|
|||||||
package configkit
|
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
|
// Difference describes one changed value or subtree between two successful loads.
|
||||||
// loads. Left is the earlier value and Right is the later value. Each snapshot
|
// Left is the earlier value and Right is the later value. Snapshots are copies;
|
||||||
// is detached; Present distinguishes absence from an explicit null, and
|
// Present distinguishes absence from explicit null. Redacted reports hiding
|
||||||
// Redacted reports whether combined sensitivity affected that side. Mappings
|
// based on sensitivity from either load. Mappings compare by key; a changed
|
||||||
// are compared recursively, while sequences are reported as atomic values.
|
// sequence is reported as a whole value. Records are ordered by canonical JSON
|
||||||
// Records are ordered by canonical JSON pointer. HiddenChanged indicates that
|
// pointer. HiddenChanged reports an actual hidden change, without identifying
|
||||||
// a value concealed by combined sensitivity changed, without revealing which
|
// the hidden member. Redaction alone does not set it.
|
||||||
// concealed member changed.
|
|
||||||
type Difference struct {
|
type Difference struct {
|
||||||
Path Path
|
Path Path
|
||||||
Left ValueSnapshot
|
Left ValueSnapshot
|
||||||
@@ -17,8 +16,9 @@ type Difference struct {
|
|||||||
HiddenChanged bool
|
HiddenChanged bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diff compares the frozen effective values of two successful loads of the
|
// Diff compares the saved final values of two successful loads of the same
|
||||||
// same configuration type. It does not reread sources, environment variables,
|
// 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
|
// or rerun defaults or resolvers. Equal values produce no records even when
|
||||||
// their sources, defaults, or resolution histories differ. Credential contents
|
// their sources, defaults, or resolution histories differ. Credential contents
|
||||||
// are not compared: typed secret references compare by reference name only.
|
// are not compared: typed secret references compare by reference name only.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package configkit
|
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.
|
// Search is an immutable ordered description of possible host-file inputs.
|
||||||
// It is consumed only when passed to SearchConfig and used by a load.
|
// 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...)}
|
return Search{value: engine.SearchDirectory(directory, options...)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paths describes literal host-file candidates in the supplied order.
|
// SearchPaths describes literal host-file candidates in the supplied order.
|
||||||
func Paths(paths ...string) Search { return Search{value: engine.Paths(paths...)} }
|
func SearchPaths(paths ...string) Search { return Search{value: engine.Paths(paths...)} }
|
||||||
|
|
||||||
// Searches concatenates descriptions in order and snapshots their entries.
|
// Searches concatenates descriptions in order and snapshots their entries.
|
||||||
func Searches(searches ...Search) Search {
|
func Searches(searches ...Search) Search {
|
||||||
|
|||||||
22
doc.go
22
doc.go
@@ -1,9 +1,17 @@
|
|||||||
// Package configkit provides deterministic, application-owned configuration
|
// Package configkit loads YAML into an application's Go configuration type and
|
||||||
// loading for Go programs.
|
// lets the application inspect where the final values came from.
|
||||||
//
|
//
|
||||||
// Load supports file, byte, reader, and explicit no-input sources with strict YAML,
|
// Start with Load and a File, Bytes, Reader, Mapping, or Filesystem.File source.
|
||||||
// typed defaults, imports, overlays, CLI overrides, schema-version checks, detached
|
// Use NoSource for deliberately absent input or SearchConfig for an explicitly
|
||||||
// metadata, recorded application resolution, typed secret references, safe effective
|
// chosen search. Applications supply types, defaults, version rules, and callbacks
|
||||||
// rendering, read-only semantic validation, and structured load errors. The
|
// for deriving and validating values. LoadLayered adds lower-priority input groups.
|
||||||
// configuration contract is documented in docs/config.md.
|
//
|
||||||
|
// 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
|
package configkit
|
||||||
|
|||||||
841
docs/config.md
841
docs/config.md
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,35 @@
|
|||||||
# Consumer Adoption Guide
|
# Consumer Adoption Guide
|
||||||
|
|
||||||
ConfigKit removes parser setup, strict decoding, presence bookkeeping, import and
|
ConfigKit loads configuration into your application's Go types. You provide
|
||||||
selected-overlay traversal, precedence, provenance, secret lookup, and safe inspection from
|
those types, defaults, and application rules. ConfigKit reads and combines the
|
||||||
applications. Consumers still own their configuration structs, defaults, semantic
|
selected inputs, checks their types, and returns values with source information.
|
||||||
validation, schema-version policy, CLI parsing, runtime matching and conversion,
|
Your application still owns CLI parsing, resource reads, remote acquisition, and
|
||||||
resource checks, remote acquisition, and service initialization.
|
runtime initialization.
|
||||||
|
|
||||||
|
Import `"go.maximumdirect.net/configkit"`; the package name is `configkit`.
|
||||||
|
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
|
## Choosing a loading entry point
|
||||||
|
|
||||||
- Use [`Load`](../../lifecycle.go) with one source for an ordinary configuration.
|
- Use [`Load`](../../lifecycle.go) with one source for an ordinary configuration.
|
||||||
[`SearchConfig`](../../discovery.go) is also a source, so its explicit
|
[`SearchConfig`](../../discovery.go) is also a source; pass it to `Load` or use
|
||||||
discovery result can be passed to `Load` or used as the main input to
|
it as the main input to [`LoadLayered`](../../layered.go).
|
||||||
[`LoadLayered`](../../layered.go).
|
- Use `LoadLayered` when several inputs should supply lower-priority defaults
|
||||||
- Use `LoadLayered` when inputs must be composed in underlay groups before the
|
beneath the main source. Each underlay group combines its inputs strictly.
|
||||||
main source. Each [`Input`](../../layered.go) owns its source, filesystem,
|
Each [`Input`](../../layered.go) owns its source, filesystem, destination, and
|
||||||
destination, and import choice. The overlay filesystem context is selected
|
import choice. The overlay filesystem context is selected independently in
|
||||||
independently in [`LayeredOptions`](../../layered.go).
|
[`LayeredOptions`](../../layered.go).
|
||||||
- Load-wide rules belong to [`Options`](../../lifecycle.go) or
|
- Load-wide rules belong to [`Options`](../../lifecycle.go) or
|
||||||
[`LayeredOptions`](../../layered.go): the application definition (including
|
[`LayeredOptions`](../../layered.go): the application definition (including
|
||||||
version policy), import defaults, and explicit overrides. These do not turn
|
version policy), import defaults, and explicit overrides. These do not turn
|
||||||
@@ -30,68 +44,68 @@ file and layered cases. For import-focused adoption, see the
|
|||||||
discovery composed with sequences, see the
|
discovery composed with sequences, see the
|
||||||
[sequence catalog](../../examples/consumers/sequence_catalog.go).
|
[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
|
## Happy path
|
||||||
|
|
||||||
An application defines ordinary types and passes one explicit source. Optional
|
Define the Go configuration type, choose one source, and call `Load`. Handle a
|
||||||
features appear only when the application needs them:
|
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
|
```go
|
||||||
result, err := configkit.Load[Config](configkit.File(path), configkit.Options[Config]{
|
definition := configkit.Definition[Config]{
|
||||||
Definition: rules,
|
Defaults: func() Config { return Config{Enabled: true, Workers: 2} },
|
||||||
Imports: useDeclaredImports,
|
Validate: func(value *Config, validation *configkit.Validation) {
|
||||||
Overlay: selectedConfigOverlay,
|
if value.Workers < 0 {
|
||||||
Overrides: explicitlySetFlags,
|
validation.Error(configkit.Keys("workers"), "must be nonnegative")
|
||||||
})
|
}
|
||||||
if err != nil {
|
},
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
return start(result.Config)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Simple consumers use neither imports nor selected overlays. A complex consumer can opt into
|
Pass this definition in `Options[Config]{Definition: definition}` when calling
|
||||||
both without redefining their syntax. ConfigKit overlays are configuration overlays;
|
`Load`. Supplied `enabled: false` or `workers: 0` replaces those defaults.
|
||||||
they do not select or replace an application's LLM, model, pipeline, report, or
|
Use `Resolve` when a value must be derived or normalized before validation, and
|
||||||
execution profile concepts.
|
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
|
## Explicit main-file discovery
|
||||||
|
|
||||||
Applications choose the search conventions and their precedence, then pass the
|
Applications choose the search conventions and their precedence, then pass the
|
||||||
resulting `SearchConfig` source to the same `Load` or `LoadLayered` entry point.
|
resulting `SearchConfig` source to the same `Load` or `LoadLayered` entry
|
||||||
For example, the maintained sequence catalog puts a caller-selected file before
|
point. For example, the maintained sequence catalog puts a caller-selected
|
||||||
a custom directory fallback while keeping its underlay and overlay choices
|
file before a custom directory fallback while keeping its underlay and overlay
|
||||||
independent. See [`sequence_catalog.go`](../../examples/consumers/sequence_catalog.go)
|
choices independent. See
|
||||||
and its [behavior tests](../../examples/consumers/sequence_catalog_test.go), plus
|
[`sequence_catalog.go`](../../examples/consumers/sequence_catalog.go) and its
|
||||||
the [working-directory executable example](../../examples/consumers/discovery_test.go).
|
[behavior tests](../../examples/consumers/sequence_catalog_test.go), plus the
|
||||||
Together, the root-owned [discovery API](../../discovery.go) and [configuration
|
[working-directory executable
|
||||||
discovery contract](../config.md#configuration-discovery) define ordered
|
example](../../examples/consumers/discovery_test.go). Together, the [discovery
|
||||||
concatenation, directory-first Cartesian expansion, optional exhaustion, and
|
API](../../discovery.go) and [configuration discovery
|
||||||
selected-file behavior. [ADR 0019](../adr/0019-explicit-configuration-discovery.md)
|
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.
|
records why discovery is explicitly selected.
|
||||||
|
|
||||||
## Import defaults and selection inspection
|
## Import defaults and selection inspection
|
||||||
@@ -99,15 +113,20 @@ records why discovery is explicitly selected.
|
|||||||
`Options.ImportDefaults` and `LayeredOptions.ImportDefaults` apply one
|
`Options.ImportDefaults` and `LayeredOptions.ImportDefaults` apply one
|
||||||
load-wide policy to enabled main, underlay, and transitive imports. Each field
|
load-wide policy to enabled main, underlay, and transitive imports. Each field
|
||||||
is resolved independently: a declaration's explicit value wins, then the
|
is resolved independently: a declaration's explicit value wins, then the
|
||||||
application default, then the library default. `Required`, `Follow`, `Hidden`,
|
application default, then the library default. `Required`, `FollowSymlinks`,
|
||||||
and `Recursive` use nil pointers for unspecified values. `Pattern` applies to
|
`IncludeHidden`, and `Recursive` use nil pointers for unspecified values.
|
||||||
|
`Pattern` applies to
|
||||||
directory searches: nil leaves filenames unrestricted, while a non-nil empty
|
directory searches: nil leaves filenames unrestricted, while a non-nil empty
|
||||||
slice selects no files. A declaration's own `pattern` replaces the application
|
slice selects no files. A declaration's own `pattern` replaces the application
|
||||||
default. The defaults do not enable imports or change selected-overlay behavior.
|
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
|
`Result.Sources`, `LoadError.Sources`, and `Resolution.Source` expose each
|
||||||
declaring source's `ImportSelections`. These records identify the source-local
|
declaring source's `EmptyImportSelections`. `Result.Source(id)` and
|
||||||
declaration path, its authored location and selector, and the `none` outcome;
|
`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
|
they do not include candidate names or a guessed reason for absence. Returned
|
||||||
records are detached snapshots. The [configuration contract](../config.md#explicit-imports)
|
records are detached snapshots. The [configuration contract](../config.md#explicit-imports)
|
||||||
owns selector applicability and exact absence semantics.
|
owns selector applicability and exact absence semantics.
|
||||||
@@ -117,7 +136,7 @@ owns selector applicability and exact absence semantics.
|
|||||||
[`LoadVersionedService`](../../examples/consumers/source_version.go) demonstrates
|
[`LoadVersionedService`](../../examples/consumers/source_version.go) demonstrates
|
||||||
if-present and required modes with one `Definition.Version` rule across a mounted
|
if-present and required modes with one `Definition.Version` rule across a mounted
|
||||||
main input and an underlay. The example reads only sanitized
|
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
|
[`layered_test.go`](../../examples/consumers/layered_test.go). The
|
||||||
[configuration contract](../config.md#shared-source-version-checks) defines
|
[configuration contract](../config.md#shared-source-version-checks) defines
|
||||||
participation, source-local lookup and diagnostics, non-main header consumption,
|
participation, source-local lookup and diagnostics, non-main header consumption,
|
||||||
@@ -135,13 +154,14 @@ priority values remain present. Its resolver records a derived endpoint URL afte
|
|||||||
overlay application, and its nested typed secret reference is classified before
|
overlay application, and its nested typed secret reference is classified before
|
||||||
the resulting records are presented.
|
the resulting records are presented.
|
||||||
|
|
||||||
The maintained [behavior tests and executable examples](../../examples/consumers/diff_test.go)
|
The maintained [behavior tests and executable
|
||||||
use temporary files and disposable secret values. Presenters should use
|
examples](../../examples/consumers/diff_test.go) use temporary files and
|
||||||
`ValueSnapshot.Present` for absence, record membership for change, and
|
disposable secret values. Presenters should use `ValueSnapshot.Present` for
|
||||||
`HiddenChanged` for a withheld change indicator. Do not parse the redaction marker
|
absence, record membership for change, and `HiddenChanged` for a withheld
|
||||||
or add separately rendered full contexts that lack combined sensitivity. The
|
change indicator. Do not parse the redaction marker or add separately rendered
|
||||||
[configuration contract](../config.md#effective-configuration-diffs) owns shared
|
full contexts that lack combined sensitivity. The [configuration
|
||||||
equality, granularity, and disclosure semantics.
|
contract](../config.md#effective-configuration-diffs) owns shared equality,
|
||||||
|
granularity, and disclosure semantics.
|
||||||
|
|
||||||
## Named sibling inheritance
|
## Named sibling inheritance
|
||||||
|
|
||||||
@@ -264,14 +284,13 @@ conversion API.
|
|||||||
## Source-aware application path policy
|
## Source-aware application path policy
|
||||||
|
|
||||||
[`LoadNotariusPaths`](../../examples/consumers/consumers.go) is the maintained
|
[`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
|
`Resolution.Source` before normalizing: relative CLI input uses an explicitly
|
||||||
supplied invocation directory, retained defaults use the canonical main source,
|
supplied invocation directory, retained defaults use the canonical main source,
|
||||||
and a file-authored scalar uses its one declaring source. Absolute paths remain
|
and a file-authored scalar uses its one declaring source. Absolute paths remain
|
||||||
unchanged. Its executable test covers the matching effective YAML and explanation
|
unchanged. Its executable test checks the resulting YAML and explanation
|
||||||
history, source-file ownership, and a symlink opened through an alias; canonical
|
history. When the configuration filename is a symlink, this example uses the
|
||||||
target-file directories, rather than the alias directory or process working
|
actual target file's directory as the base for file-authored paths.
|
||||||
directory, are the selected bases.
|
|
||||||
|
|
||||||
The selected example policy leaves a relative value unchanged and emits a warning
|
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
|
when a CLI base is absent or file provenance is missing or composite. That is an
|
||||||
@@ -281,7 +300,15 @@ existence checks nor confinement checks.
|
|||||||
|
|
||||||
## Intentional migration changes
|
## 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.
|
- Explicit empty strings, lists, zeroes, false values, and nulls remain intentional.
|
||||||
ConfigKit accepts an empty list structurally; WeatherReporter-shaped validation
|
ConfigKit accepts an empty list structurally; WeatherReporter-shaped validation
|
||||||
@@ -301,10 +328,70 @@ The examples preserve domain meaning while adopting one configuration convention
|
|||||||
parsers translate only explicitly set configuration flags into `Override` records.
|
parsers translate only explicitly set configuration flags into `Override` records.
|
||||||
Separate invocation state remains outside configuration.
|
Separate invocation state remains outside configuration.
|
||||||
- Credential fields migrate to environment-name `Secret` references. Applications
|
- 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
|
blocked empty variables remain blocked, and ordinary `Load` never changes the
|
||||||
process environment.
|
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
|
### Selected-overlay migration
|
||||||
|
|
||||||
To adopt the current selected-overlay API, rename `Options.Profile`,
|
To adopt the current selected-overlay API, rename `Options.Profile`,
|
||||||
@@ -315,10 +402,40 @@ The [configuration contract](../config.md#selected-overlays) is the canonical
|
|||||||
behavioral reference. Application-owned profile concepts, such as `model_profile`,
|
behavioral reference. Application-owned profile concepts, such as `model_profile`,
|
||||||
are unchanged, and any CLI flag rename remains the application's decision.
|
are unchanged, and any CLI flag rename remains the application's decision.
|
||||||
|
|
||||||
These examples are migration evidence, not completed changes to Narratio, Notarius,
|
### Background on the examples
|
||||||
WeatherReporter, or Seriatim. Updating those repositories, their deployed YAML, CLI
|
|
||||||
wiring, runtime catalogs, and operational documentation remains separate downstream
|
These examples demonstrate migration shapes. They do not claim that Narratio,
|
||||||
work.
|
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
|
## Maintained API examples
|
||||||
|
|
||||||
@@ -330,7 +447,7 @@ focused behavioral owner:
|
|||||||
| `Load`, `NoSource`, validation, versions, resolution, `SecretEnv`, overrides, and explanation | Executable examples in [`example_test.go`](../../example_test.go) |
|
| `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 |
|
| 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 |
|
| 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 |
|
| 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` |
|
| 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` |
|
| Named sibling inheritance, presence, and derivation | `Example_inheritedPresets` and `TestPresetCatalogOneLoadAndDerivation` |
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public behavior.
|
|||||||
| Shared source-version checks | [Configuration contract](config.md#shared-source-version-checks), [ADR 0013](adr/0013-shared-source-version-checks.md), and the version/source-version tests below |
|
| Shared source-version checks | [Configuration contract](config.md#shared-source-version-checks), [ADR 0013](adr/0013-shared-source-version-checks.md), and the version/source-version tests below |
|
||||||
| Documentation changes | The owning document |
|
| Documentation changes | The owning document |
|
||||||
| Licensing, dependencies, attribution, or release-document changes | [Project grant](../COPYRIGHT) and [attribution](oss/attribution.md); release changes also follow the release procedure |
|
| Licensing, dependencies, attribution, or release-document changes | [Project grant](../COPYRIGHT) and [attribution](oss/attribution.md); release changes also follow the release procedure |
|
||||||
| Architecture decisions or future work | [Future features](roadmap/future.md), [consolidated audit review](roadmap/audit.md), [remediation roadmap](roadmap/roadmap.md), and relevant ADRs listed below |
|
| Architecture decisions or future work | [Future features](roadmap/future.md) and relevant ADRs listed below |
|
||||||
| Code and test changes | The owning contract and tests from the map below |
|
| Code and test changes | The owning contract and tests from the map below |
|
||||||
| Examples or copyable assets | The owning implemented contract and [consumer adoption guide](consumers/README.md) |
|
| Examples or copyable assets | The owning implemented contract and [consumer adoption guide](consumers/README.md) |
|
||||||
| Release preparation | [Release procedure](release.md) |
|
| Release preparation | [Release procedure](release.md) |
|
||||||
@@ -199,11 +199,32 @@ git show 34cbc0d:docs/roadmap/remediation.md
|
|||||||
```
|
```
|
||||||
|
|
||||||
Potential enhancements are tracked in [future features](roadmap/future.md).
|
Potential enhancements are tracked in [future features](roadmap/future.md).
|
||||||
The [consolidated audit review](roadmap/audit.md) owns the open findings from
|
The completed audit-remediation, internal-boundary simplification, shared-history,
|
||||||
the nine 2026-10-25 audits, their evidence, and settled scope decisions. The
|
and public API cleanup roadmaps, reviews, and resolved findings are preserved at
|
||||||
[remediation roadmap](roadmap/roadmap.md) owns the combined target state and
|
`68400ff` under `docs/roadmap/`. For example:
|
||||||
high-level dependency order. The original reports are preserved at `ecd5d5d`
|
|
||||||
under `docs/roadmap/`.
|
```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.
|
Install the Go version required by `go.mod` or a compatible newer toolchain.
|
||||||
Acquire declared dependencies explicitly before entering an offline
|
Acquire declared dependencies explicitly before entering an offline
|
||||||
@@ -252,21 +273,21 @@ 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'` |
|
| 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'` |
|
| 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'` |
|
| 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` |
|
| 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'` |
|
| Native filesystem and mapping sources | [Native acquisition](../internal/engine/native_filesystem_test.go), [filesystem facade](../facade_filesystem_test.go), [mapping facade](../facade_mapping_test.go) | `go test ./internal/engine -run 'NativeFilesystem'` and `go test . -run 'TestFacade'` |
|
||||||
| Composition, selected overlays, CLI precedence | [Composition](../internal/engine/compose_test.go), [overlay](../internal/engine/overlay_test.go), [selected overlays](../internal/engine/selected_overlay_test.go), [overrides](../internal/engine/override_test.go) | `go test ./internal/engine -run 'Compose\|Overlay\|Override'` |
|
| Composition, selected overlays, CLI precedence | [Composition](../internal/engine/compose_test.go), [overlay](../internal/engine/overlay_test.go), [selected overlays](../internal/engine/selected_overlay_test.go), [overrides](../internal/engine/override_test.go) | `go test ./internal/engine -run 'Compose\|Overlay\|Override'` |
|
||||||
| 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` |
|
| 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'` |
|
| 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'` |
|
| 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` |
|
| 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) | `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'` |
|
| 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'` |
|
| 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'` |
|
| 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'` |
|
| 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'` |
|
| 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` |
|
| 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` |
|
| Identified-sequence scaling evidence | [Mixed-upsert benchmark](../internal/engine/sequence_combine_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkSequenceMixedUpsert$' -benchmem` |
|
||||||
|
|||||||
@@ -151,6 +151,27 @@ descriptor caching and type policy, and
|
|||||||
inline field assembly. [`decode.go`](../../internal/engine/decode.go) consumes
|
inline field assembly. [`decode.go`](../../internal/engine/decode.go) consumes
|
||||||
those descriptors for typed decoding.
|
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
|
Behavioral ownership follows the same handoffs: imports and grouped lifetimes are
|
||||||
covered by `imports_test.go`, `load_request_test.go`, and focused source,
|
covered by `imports_test.go`, `load_request_test.go`, and focused source,
|
||||||
discovery, and destination tests; inheritance by the inheritance test files;
|
discovery, and destination tests; inheritance by the inheritance test files;
|
||||||
@@ -158,3 +179,9 @@ reflection behavior by `schema_test.go`, `projection_test.go`, and
|
|||||||
`decode_test.go`. The [development test map](../development.md#task-to-test-map)
|
`decode_test.go`. The [development test map](../development.md#task-to-test-map)
|
||||||
links focused commands. [`clone.go`](../../internal/engine/clone.go) owns typed
|
links focused commands. [`clone.go`](../../internal/engine/clone.go) owns typed
|
||||||
copying, using the shared descriptors and per-operation traversal helpers.
|
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.
|
||||||
|
|||||||
114
docs/release.md
114
docs/release.md
@@ -1,16 +1,23 @@
|
|||||||
# ConfigKit Release Procedure
|
# ConfigKit Release Procedure
|
||||||
|
|
||||||
This is the maintainer runbook for ConfigKit's [release policy](policy/release.md).
|
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
|
A release publishes the Go source under a new Git tag, with a matching release
|
||||||
matching checked-in release note are the deliverables; there is no binary,
|
note committed in the repository. The tag is annotated: it includes the release
|
||||||
container image, generated package, or separate registry publication.
|
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.
|
||||||
|
|
||||||
|
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
|
## Release identity and support
|
||||||
|
|
||||||
- Module: `gitea.maximumdirect.net/eric/ConfigKit`
|
- Module: `go.maximumdirect.net/configkit`
|
||||||
- Primary branch: `main`
|
- Primary branch: `main`
|
||||||
- Authoritative remote: `origin`
|
- 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
|
- 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
|
- 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
|
higher patch when required by a dependency or specific correctness fix, as
|
||||||
@@ -20,10 +27,15 @@ container image, generated package, or separate registry publication.
|
|||||||
- Best-effort targets: macOS amd64 and arm64, under the
|
- Best-effort targets: macOS amd64 and arm64, under the
|
||||||
[platform support policy](policy/release.md#platform-support)
|
[platform support policy](policy/release.md#platform-support)
|
||||||
|
|
||||||
Release validation runs one offline race-enabled Go suite, one full vet pass,
|
Release validation runs the offline Go test suite once with the race detector,
|
||||||
and CGO-disabled package compilation for both Linux targets. macOS and CI are
|
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.
|
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
|
## 1. Select Go and prepare the tool
|
||||||
|
|
||||||
Use a clean checkout with Git, Bash 3.2 or newer, and a native compiler for race
|
Use a clean checkout with Git, Bash 3.2 or newer, and a native compiler for race
|
||||||
@@ -33,10 +45,10 @@ For a patch release, retain the series recorded in `go.mod` and leave its consum
|
|||||||
minimum unchanged. Follow the
|
minimum unchanged. Follow the
|
||||||
[toolchain selection policy](policy/release.md#configkit-toolchain-selection).
|
[toolchain selection policy](policy/release.md#configkit-toolchain-selection).
|
||||||
|
|
||||||
Put the selected series' latest locally available compatible patch first on
|
Choose the latest compatible patch installed locally in that Go series. Put
|
||||||
`PATH`, ensuring it meets `go.mod`'s minimum. The same `go` executable must build
|
its `go` executable first on `PATH` and ensure it meets the minimum in
|
||||||
ReleaseKit and run its child validation commands. Set local toolchain selection
|
`go.mod`. The same `go` executable must build ReleaseKit and run its child
|
||||||
and verify it:
|
validation commands. Set local toolchain selection and verify it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
export GOTOOLCHAIN=local
|
export GOTOOLCHAIN=local
|
||||||
@@ -61,8 +73,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
|
unchanged, leave the minimum unchanged unless a minor or major release
|
||||||
deliberately changes it under the
|
deliberately changes it under the
|
||||||
[consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain).
|
[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
|
For example:
|
||||||
separate requirement to validate with Go 1.26.0.
|
|
||||||
|
| 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:
|
Prepare dependencies whether or not the minimum changes:
|
||||||
|
|
||||||
@@ -70,7 +91,8 @@ Prepare dependencies whether or not the minimum changes:
|
|||||||
go mod download
|
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
|
Ensure `$TMPDIR` names a system temporary location, not the repository. These
|
||||||
commands never install another ReleaseKit version:
|
commands never install another ReleaseKit version:
|
||||||
|
|
||||||
@@ -82,8 +104,8 @@ go list -m gitea.maximumdirect.net/eric/ReleaseKit
|
|||||||
"$releasekit_bin" version
|
"$releasekit_bin" version
|
||||||
```
|
```
|
||||||
|
|
||||||
The module-selected executable is ReleaseKit v0.4.2. Use the built executable
|
This repository selects ReleaseKit v0.4.2. Use the compiled program above for
|
||||||
for publication to preserve its exit status and publication-state diagnostics.
|
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
|
`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
|
use the `go` found on `PATH` with `GOTOOLCHAIN=local`; keep the selected
|
||||||
executable first on `PATH` through the publication command.
|
executable first on `PATH` through the publication command.
|
||||||
@@ -91,7 +113,7 @@ executable first on `PATH` through the publication command.
|
|||||||
Verify access to the authoritative remote without displaying credentials:
|
Verify access to the authoritative remote without displaying credentials:
|
||||||
|
|
||||||
```sh
|
```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
|
Publication requires explicit authorization. Local implementation commits do
|
||||||
@@ -99,10 +121,10 @@ not authorize a push or release.
|
|||||||
|
|
||||||
## 2. Prepare and commit the candidate
|
## 2. Prepare and commit the candidate
|
||||||
|
|
||||||
Choose an unused stable version locally and on the authoritative remote. Compare
|
Choose a stable version that has not been used locally or on the publication
|
||||||
the candidate with the previous tag and review exported Go signatures and
|
remote. Compare the candidate with the previous tag. Review public Go APIs and
|
||||||
ownership, YAML and resolution semantics, diagnostic contracts, minimum Go
|
value ownership, YAML and resolution rules, diagnostic contracts, the minimum
|
||||||
version, and platform support. Review maintained consumer examples and the
|
Go version, and platform support. Review maintained consumer examples and the
|
||||||
migration guidance. Explain any consumer-minimum increase and required consumer
|
migration guidance. Explain any consumer-minimum increase and required consumer
|
||||||
action under the
|
action under the
|
||||||
[consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain).
|
[consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain).
|
||||||
@@ -111,7 +133,7 @@ Create `docs/releases/<version>.md`. Its first line must be exactly
|
|||||||
`# ConfigKit <version>`, and it must end with the license and attribution footer:
|
`# ConfigKit <version>`, and it must end with the license and attribution footer:
|
||||||
|
|
||||||
```markdown
|
```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
|
Replace `VERSION` with the full tag and confirm the three target files exist in
|
||||||
@@ -127,7 +149,7 @@ Push only that committed candidate to the literal authoritative URL. This does
|
|||||||
not follow local tags:
|
not follow local tags:
|
||||||
|
|
||||||
```sh
|
```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
|
ReleaseKit requires authoritative `main` to equal this candidate before it
|
||||||
@@ -139,32 +161,40 @@ candidate to `main`, and invoke validation again.
|
|||||||
|
|
||||||
## 4. Publish the committed candidate
|
## 4. Publish the committed candidate
|
||||||
|
|
||||||
With the authorized candidate already on authoritative `main`, invoke the
|
Once publication is authorized and the candidate is on the publication remote's
|
||||||
module-selected native executable exactly once:
|
`main`, run the compiled ReleaseKit program once to validate and publish it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
"$releasekit_bin" publish vMAJOR.MINOR.PATCH
|
"$releasekit_bin" publish vMAJOR.MINOR.PATCH
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace the placeholder with the selected version. ReleaseKit checks the
|
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
|
`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,
|
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
|
one full vet pass, and CGO-disabled package compilation for Linux amd64 and
|
||||||
arm64. It publishes only the annotated tag for that candidate. Confirmed
|
arm64. It publishes only the annotated tag for that candidate. Confirmed
|
||||||
publication of the intended tag completes the release. The tag points to the
|
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.
|
captured commit and uses the complete checked-in note as its annotation message.
|
||||||
The optional `check`
|
Use the optional `check` command for diagnosis when useful. It validates without
|
||||||
command is available for diagnosis but is not a separate required pass:
|
publishing and is not required before `publish`, which performs its own checks:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
"$releasekit_bin" check vMAJOR.MINOR.PATCH
|
"$releasekit_bin" check vMAJOR.MINOR.PATCH
|
||||||
```
|
```
|
||||||
|
|
||||||
Status 0 confirms publication, status 1 reports a known publication failure or
|
For `publish`, interpret the program's exit status as follows:
|
||||||
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
|
| Status | Meaning | Next action |
|
||||||
diagnostics.
|
| --- | --- | --- |
|
||||||
|
| `0` | Publication succeeded. | The release is complete. Optional verification can follow. |
|
||||||
|
| `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
|
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
|
a hosted release page. A successful intended-tag publication does not require
|
||||||
@@ -173,12 +203,17 @@ remote read-back or consumer retrieval; those checks remain optional.
|
|||||||
## 5. Recover from a failed or uncertain tag push
|
## 5. Recover from a failed or uncertain tag push
|
||||||
|
|
||||||
The executable reports the publication state, phase, version, candidate commit,
|
The executable reports the publication state, phase, version, candidate commit,
|
||||||
tag-object ID, effective endpoint, and error on standard error. It performs one
|
tag-object ID, effective endpoint, and error on standard error. After a failed
|
||||||
bounded authoritative reconciliation after a failed tag push. Exit status 3
|
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.
|
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
|
An annotated tag is a Git object containing the release note and a pointer to
|
||||||
tag-object ID from the report. Copy the captured endpoint into the variable
|
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,
|
below using normal shell quoting. Check that Git still resolves it to itself,
|
||||||
then query the authoritative refs once:
|
then query the authoritative refs once:
|
||||||
|
|
||||||
@@ -195,7 +230,8 @@ cannot establish absence at the captured endpoint.
|
|||||||
|
|
||||||
Compare the `main` object ID with the reported candidate commit and the tag
|
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 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
|
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
|
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
|
the endpoint is reachable, the tag is confirmed absent, and `main` still equals
|
||||||
@@ -228,8 +264,8 @@ consumer_dir=$(mktemp -d)
|
|||||||
(
|
(
|
||||||
cd "$consumer_dir"
|
cd "$consumer_dir"
|
||||||
go mod init example.invalid/configkit-release-check
|
go mod init example.invalid/configkit-release-check
|
||||||
GOPRIVATE=gitea.maximumdirect.net GOPROXY=direct go get "gitea.maximumdirect.net/eric/ConfigKit@$release_version"
|
GOWORK=off GOPROXY=direct go get "go.maximumdirect.net/configkit@$release_version"
|
||||||
go list -m -f '{{.Path}} {{.Version}}' gitea.maximumdirect.net/eric/ConfigKit
|
GOWORK=off go list -m -f '{{.Path}} {{.Version}}' go.maximumdirect.net/configkit
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,949 +0,0 @@
|
|||||||
# Consolidated Audit Review
|
|
||||||
|
|
||||||
Status: scope settled; 29 entries selected for implementation planning.
|
|
||||||
Two refactors are deferred to `future.md`. Implementation has not begun.
|
|
||||||
Consolidated: 2026-10-25. Baseline: `ecd5d5d`.
|
|
||||||
|
|
||||||
This document consolidates nine sequential audits for maintainer review. It owns
|
|
||||||
their open findings, supporting evidence, proposed remedies, and relationships.
|
|
||||||
The settled decisions select the [remediation roadmap's](roadmap.md) scope.
|
|
||||||
That document owns the combined target state and high-level dependency order;
|
|
||||||
this review owns detailed finding evidence and acceptance cases. Neither is a
|
|
||||||
staged implementation plan or a new set of release gates. The
|
|
||||||
[configuration contract](../config.md), public Go declarations, and
|
|
||||||
[architecture policy](../policy/architecture.md) continue to
|
|
||||||
own behavior and guarantees.
|
|
||||||
|
|
||||||
There are nine distinct reported defects: four from correctness, three from
|
|
||||||
resources, one from error handling, and one from security. Concurrency adds
|
|
||||||
evidence for an existing callback-lifetime defect. Other entries are cleanup,
|
|
||||||
efficiency, maintainability, or test improvements. Regression recommendations
|
|
||||||
are attached to the defects they protect rather than counted as additional
|
|
||||||
product fixes.
|
|
||||||
|
|
||||||
The 35 original numbered findings map to 31 review entries after deduplication
|
|
||||||
and integration of related test work. All findings remain unresolved; the
|
|
||||||
[settled scope decisions](#settled-scope-decisions) identify the 29 selected
|
|
||||||
entries. C03 and F07 have moved to `future.md`, with reference entries retained
|
|
||||||
here for traceability. All three scope questions are settled.
|
|
||||||
Their medium/low priorities retain the original assessments; those labels do not
|
|
||||||
imply severity certification or implementation order. IDs remain stable as work
|
|
||||||
is selected or completed.
|
|
||||||
|
|
||||||
## Evidence And Provenance
|
|
||||||
|
|
||||||
All nine audits were dated 2026-10-25. Their sequential commits changed only the
|
|
||||||
audit documents, so their production baseline is the same. This consolidation
|
|
||||||
does not independently reproduce the findings or claim new runtime evidence.
|
|
||||||
|
|
||||||
| Original report | Reviewed revision | Evidence contributed |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `correctness.md` | `47944d2` | Source review and standalone public-API reproductions of four defects. |
|
|
||||||
| `efficiency.md` | `5098b56` | Source-derived scaling cases and proposed algorithm changes; no benchmarks. |
|
|
||||||
| `complexity.md` | `320d0c5` | Responsibility/caller review, graph complexity estimates, and maintainability candidates. |
|
|
||||||
| `concurrency.md` | `a802734` | Ownership review, race checks, and corroboration of callback lifetime after panic. |
|
|
||||||
| `resources.md` | `0cca633` | Bounded allocation, retained-node, and host-handle diagnostics. |
|
|
||||||
| `error-handling.md` | `5925066` | Directory read/close fault injection through the public API. |
|
|
||||||
| `security.md` | `b30c9f1` | Trust-boundary review, bounded symlink operation counts, and a historical dependency spot-check. |
|
|
||||||
| `testing.md` | `eb5a82f` | Aggregate statement coverage, assertion review, and regression/test-cleanup recommendations. |
|
|
||||||
| `deadcode.md` | `b1c278b` | Type-aware reference inventory, graph/caller checks, and source review of private cleanup candidates. |
|
|
||||||
|
|
||||||
The full original reports, including exact probes, commands, source line
|
|
||||||
references, and validation limitations, are preserved at `ecd5d5d`. For example:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git show ecd5d5d:docs/roadmap/correctness.md
|
|
||||||
git show ecd5d5d:docs/roadmap/security.md
|
|
||||||
```
|
|
||||||
|
|
||||||
The [original-finding map](#original-finding-map) accounts for every numbered
|
|
||||||
finding, including duplicates and test recommendations integrated elsewhere.
|
|
||||||
Source links below identify implementation owners; historical line numbers are
|
|
||||||
left in the originals because later edits will move them.
|
|
||||||
|
|
||||||
### Recorded Validation And Its Limits
|
|
||||||
|
|
||||||
The audits report successful development checks, ordinary tests, full race tests,
|
|
||||||
`go vet`, and focused suites as specified in their individual records, using
|
|
||||||
Go 1.26.10 on Linux amd64. Concurrency additionally repeated six relevant engine
|
|
||||||
tests with race instrumentation, count 20, and CPUs 1, 4, and 8. Testing ran the
|
|
||||||
suite with shuffle enabled three times. These results did not detect the newly
|
|
||||||
reported defects and are not evidence that those defects have been fixed.
|
|
||||||
|
|
||||||
Testing recorded 771 ordinary test functions, three fuzz targets, and 29 executable
|
|
||||||
examples. Aggregate cross-package coverage was 90.3%: root 81.3%, engine 91.0%,
|
|
||||||
document 100%, YAML 89.9%, and maintained consumers 79.2%. Those figures locate
|
|
||||||
gaps; they are not targets or a measure of assertion strength. Only checked-in
|
|
||||||
fuzz seeds ran, not an extended campaign or mutation analysis.
|
|
||||||
|
|
||||||
Standalone diagnostics stayed outside the repository. Some potentially fatal
|
|
||||||
cases used disposable processes, small limits, or operation ceilings; larger
|
|
||||||
scaling examples were derived rather than executed. The dead-code inventory
|
|
||||||
considered Linux, Darwin, and Windows build configurations, but no audit reports
|
|
||||||
native macOS or Windows execution. Graph scores and reachability results were
|
|
||||||
triage aids, not proof that a function is unnecessary or a numerical refactoring
|
|
||||||
threshold. Cached analysis tooling was used outside the repository; no new
|
|
||||||
project tools or dependencies were introduced.
|
|
||||||
|
|
||||||
The security report's module-path advisory spot-check and successful module
|
|
||||||
integrity verification are historical evidence only. It recorded no listed
|
|
||||||
advisories for the selected YAML v4, ReleaseKit, or Goldmark v2 module paths, and
|
|
||||||
recorded the selected `x/mod` version as fixed for the two reports it found. It
|
|
||||||
did not perform complete toolchain/transitive reachability analysis or supply-
|
|
||||||
chain attestation; dedicated vulnerability, security, and secret scanners were
|
|
||||||
unavailable. This consolidation neither refreshes nor broadens that conclusion.
|
|
||||||
|
|
||||||
## Review Priorities And Dependencies
|
|
||||||
|
|
||||||
Recommended review order is reported defects with their regression coverage,
|
|
||||||
bounded cleanup, targeted efficiency changes, and then broader structural
|
|
||||||
refactoring. Small prerequisite cleanups can accompany a defect fix. The order
|
|
||||||
of the groups below is for navigation, not an execution sequence.
|
|
||||||
|
|
||||||
| Relationship | Consequence for scope selection |
|
|
||||||
| --- | --- |
|
|
||||||
| A02 and A05 | Make descriptor construction safe before reusing successful dynamic-type eligibility results. |
|
|
||||||
| B01 and B02 | Panic cleanup must close callback state and the owned host root independently. Neither fix substitutes for the other. |
|
|
||||||
| C01 and its filesystem tests | Strengthen error/handle assertions while adding failure-phase regressions, using one shared test owner. |
|
|
||||||
| C02 and deferred C03 | Complete link expansion remediation independently; reconsider glob readability extraction afterward through `future.md`. |
|
|
||||||
| D01, D02, and D03 | Remove the confirmed unused copies, then account for the complete retained graph. Removing one unused tree does not fix the remaining omissions. |
|
|
||||||
| E01, E02, and E03 | Address policy-state costs and responsibility boundaries together, preserving separate scaling and behavioral acceptance cases. |
|
|
||||||
| F01 and F02 | Removing the test-only acquisition mode does not by itself simplify active lifecycle handoffs. |
|
|
||||||
| F04, F09, and F12 | Replace useful facade ownership/query coverage before deleting disconnected or representation-coupled scaffolding. |
|
|
||||||
| F05 and deferred F07 | Remove unused placement bridges in this scope; reconsider wider transport simplification afterward through `future.md`. |
|
|
||||||
|
|
||||||
## A. Schema And Typed Values
|
|
||||||
|
|
||||||
### A01. Terminate Pointer-Only Recursive Descriptor Traversal
|
|
||||||
|
|
||||||
Type: reported defect. Priority: medium. Source: correctness 1; testing 1.
|
|
||||||
|
|
||||||
**Problem and evidence.** In [schema.go](../../internal/engine/schema.go), a legal
|
|
||||||
type such as `type Loop *Loop` produces a pointer descriptor whose element is
|
|
||||||
itself. [decode.go](../../internal/engine/decode.go) unwraps that descriptor
|
|
||||||
without consuming a non-null input node; `resolveSchemaPath` likewise assumes
|
|
||||||
pointer traversal eventually reaches another kind. A public load with a `Loop`
|
|
||||||
field and `value: {}` overflowed the stack in a process with a small stack limit.
|
|
||||||
`value: null` succeeded, but resolving a normalization path for that field hung
|
|
||||||
until the diagnostic process was stopped. The [schema
|
|
||||||
contract](../config.md#application-schema-binding) permits recursive types with
|
|
||||||
acyclic values; the inline-field restriction is not a blanket pointer restriction.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Detect repeated descriptors while unwrapping
|
|
||||||
pointer-only chains in decoding and schema-path resolution, and review analogous
|
|
||||||
helpers. Incompatible non-null input must fail safely; path traversal must
|
|
||||||
terminate and reject unaddressable descendants. Preserve supported nil/null
|
|
||||||
values and ordinary recursive structs. Do not add a blanket schema rejection or
|
|
||||||
arbitrary depth cap. Add bounded regression coverage for both decoding and helper
|
|
||||||
path resolution, keeping fatal/hanging probes out of the default test process.
|
|
||||||
|
|
||||||
### A02. Make Failed Dynamic Descriptor Construction Transactional
|
|
||||||
|
|
||||||
Type: reported defect. Priority: medium. Source: correctness 2; testing 1.
|
|
||||||
|
|
||||||
**Problem and evidence.** `dynamicDescriptor` in
|
|
||||||
[value_operation.go](../../internal/engine/value_operation.go) passes the load's
|
|
||||||
descriptor cache to `schemaBuilder`. Failed construction leaves cached container
|
|
||||||
placeholders. For a dynamic struct containing an unsupported channel field, two
|
|
||||||
`Normalize` calls in one resolver, first with a typed nil pointer and then a
|
|
||||||
non-nil pointer to that type, reproduced a nil-pointer panic on the second call.
|
|
||||||
The first call reported a diagnostic but left a reusable partial descriptor.
|
|
||||||
The application callback itself did not panic.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Stage new descriptors and commit only on
|
|
||||||
successful construction, or roll back all additions from a failed build. Include
|
|
||||||
recursive references among newly built descriptors; clearing only the immediate
|
|
||||||
failed container or adding a projection nil guard is insufficient. Preserve
|
|
||||||
previously valid descriptors, load-local ownership, and safe path-specific
|
|
||||||
diagnostics under [typed projection](../config.md#typed-projection-and-ownership)
|
|
||||||
and [resolution](../config.md#recorded-application-resolution). Test repeated
|
|
||||||
operations after a failed build in the same load. Coordinate A05 only after this
|
|
||||||
failure-state boundary is sound.
|
|
||||||
|
|
||||||
### A03. Distinguish Acyclic Slice Views In Cycle Detection
|
|
||||||
|
|
||||||
Type: reported defect. Priority: medium. Source: correctness 3; testing 1.
|
|
||||||
|
|
||||||
**Problem and evidence.** `valueVisit` / `visitFor` in
|
|
||||||
[value_operation.go](../../internal/engine/value_operation.go) identify slices by
|
|
||||||
type, kind, and starting pointer, omitting length. The finite value built by
|
|
||||||
`shared := []any{"leaf", nil}; shared[1] = shared[:1]` was rejected through mapping
|
|
||||||
sources, typed defaults, and CLI overrides, while an equivalent independently
|
|
||||||
allocated value succeeded. The nested view contains only the leaf, not its
|
|
||||||
enclosing view. The same helper serves projection/cloning and
|
|
||||||
[value.go](../../internal/engine/value.go).
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Include visible slice length in active-visit
|
|
||||||
identity, or use an equivalent distinction. Preserve rejection of actual self
|
|
||||||
and mutual cycles, nil/empty behavior, and detached output; preserving alias
|
|
||||||
identity is not required. Regressions should cover the shared source, override,
|
|
||||||
projection, and cloning paths under the existing supported-acyclic-value contract,
|
|
||||||
alongside true-cycle rejection.
|
|
||||||
|
|
||||||
### A04. Enforce Existing Node Budgets Before Container Reservations
|
|
||||||
|
|
||||||
Type: reported defect. Priority: medium. Source: resources 1; testing 1.
|
|
||||||
|
|
||||||
**Problem and evidence.** `projectSequence` and `projectMap` in
|
|
||||||
[projection.go](../../internal/engine/projection.go) reserve space for the full
|
|
||||||
container, and maps collect all keys, before child charging in
|
|
||||||
[value_operation.go](../../internal/engine/value_operation.go). Defaults containing
|
|
||||||
a maximum-length slice of zero-sized structs caused a `makeslice: cap out of range`
|
|
||||||
panic instead of `schema.projection_limit`; that diagnostic did not allocate a
|
|
||||||
huge input backing array. Less extreme cases can allocate excessive temporary
|
|
||||||
memory before rejection. Application resolution can reach these paths too.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Check the known minimum node cost against the
|
|
||||||
remaining existing allowance with overflow-safe arithmetic, or bound initial
|
|
||||||
capacity and grow as charging succeeds. Review map reservation/key collection as
|
|
||||||
well as sequences. Preserve node/depth semantics, inline handling, and useful
|
|
||||||
diagnostic paths. Use bounded regressions, including small controlled allowances,
|
|
||||||
without enormous allocations. This enforces an existing budget, not a new length
|
|
||||||
cap or universal heap-size guarantee.
|
|
||||||
|
|
||||||
### A05. Reuse Successful Dynamic-Type Eligibility Checks Locally
|
|
||||||
|
|
||||||
Type: optimization candidate. Priority: low. Source: efficiency 6.
|
|
||||||
|
|
||||||
**Problem and evidence.** Each dynamic value in projection/cloning invokes
|
|
||||||
`descriptorContainsSecret` with a new visited map, even when its descriptor is
|
|
||||||
cached. N values of one concrete type with descriptor-graph size S therefore cost
|
|
||||||
O(N × S) eligibility work. N interface values containing nil pointers to one wide
|
|
||||||
supported struct can produce only N null values while repeating the full type
|
|
||||||
walk. This is a source-derived bound, not measured runtime.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** After A02, consider remembering successfully
|
|
||||||
approved concrete descriptors within `valueOperation`, reducing repeated-type
|
|
||||||
eligibility work to O(S + N). Cache only completed successful eligibility checks,
|
|
||||||
not traversal-local visits, partial descriptors, or path-bearing failures.
|
|
||||||
Preserve rejection of dynamic secret-reference types, including nil pointers and
|
|
||||||
recursive descriptor cases. This does not select cross-load schema caching or a
|
|
||||||
clone traversal redesign.
|
|
||||||
|
|
||||||
## B. Callback And Filesystem Lifetime
|
|
||||||
|
|
||||||
### B01. Deactivate Callback Contexts During Panic Unwinding
|
|
||||||
|
|
||||||
Type: reported defect. Priority: low. Sources: correctness 4, concurrency 1;
|
|
||||||
testing 1; corroborated by error-handling and security reviews.
|
|
||||||
|
|
||||||
**Problem and evidence.** `runResolution` in
|
|
||||||
[resolution.go](../../internal/engine/resolution.go) and `runValidation` /
|
|
||||||
`reportVersion` in [validation.go](../../internal/engine/validation.go) deactivate
|
|
||||||
contexts only after normal callback return. Public probes retained the context,
|
|
||||||
panicked, recovered outside `Load`, and then successfully queried resolution or
|
|
||||||
wrote validation/version diagnostics. The concurrency audit reproduced this
|
|
||||||
with race instrumentation without reporting a data race. The defect violates
|
|
||||||
callback lifetime, not a newly supported concurrent-use scenario.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Defer deactivation within each callback
|
|
||||||
invocation, closing immediately at normal return and on unwinding, before later
|
|
||||||
projection or metadata work. Preserve the original panic and prohibit retained
|
|
||||||
context use through the established closed-context behavior. Test all three
|
|
||||||
callback kinds with external recovery. No recovery service, lock, atomic flag,
|
|
||||||
or broader callback thread-safety guarantee is needed. B02 separately owns root
|
|
||||||
handle cleanup.
|
|
||||||
|
|
||||||
### B02. Close The Owned Import Root When A Version Callback Panics
|
|
||||||
|
|
||||||
Type: reported defect. Priority: medium. Source: resources 3; testing 1.
|
|
||||||
|
|
||||||
**Problem and evidence.** Host import acquisition in
|
|
||||||
[import_acquisition.go](../../internal/engine/import_acquisition.go) opens an
|
|
||||||
`os.Root` through [filesystem_access.go](../../internal/engine/filesystem_access.go)
|
|
||||||
but relies on explicit ordinary-path closes. An imported version-check panic
|
|
||||||
bypasses them. A disposable Linux probe disabled GC, recovered outside `Load`,
|
|
||||||
and observed one remaining root descriptor through `/proc/self/fd`; the normal
|
|
||||||
control returned to zero. The imported file's own deferred close still ran.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Install deferred fallback cleanup at the
|
|
||||||
owning boundary, preserving explicit normal close timing and diagnostics, as
|
|
||||||
selected-overlay acquisition already does. Avoid double-close confusion, leave
|
|
||||||
borrowed filesystems untouched, and propagate the original panic. A regression
|
|
||||||
must observe the owned host root, not merely a native file-close counter, and
|
|
||||||
document platform prerequisites. Context deactivation in B01 is a separate
|
|
||||||
requirement. This restores load-scope cleanup without adding panic recovery.
|
|
||||||
|
|
||||||
## C. Import Selection And Link Resolution
|
|
||||||
|
|
||||||
### C01. Preserve Directory Failure Phase And Strengthen Its Tests
|
|
||||||
|
|
||||||
Type: reported defect with test improvements. Priority: medium.
|
|
||||||
Sources: error-handling 1, testing 3.
|
|
||||||
|
|
||||||
**Problem and evidence.** `filesystemAccess.readDirectory` in
|
|
||||||
[filesystem_access.go](../../internal/engine/filesystem_access.go) returns open,
|
|
||||||
read, and close errors through one unqualified error channel.
|
|
||||||
[import_selection.go](../../internal/engine/import_selection.go) and
|
|
||||||
[glob.go](../../internal/engine/glob.go) treat any `fs.ErrNotExist` as absence.
|
|
||||||
A borrowed filesystem probe selected a failing directory plus a good directory,
|
|
||||||
using both `dir` and `glob` with `required: true`. Injecting `fs.ErrNotExist`
|
|
||||||
alongside entries from `ReadDir`, or from `Close`, let loads succeed with the bad
|
|
||||||
directory's contribution omitted. Permission-classified controls failed. All
|
|
||||||
eight selector/phase/error cases ran. This violates the existing distinction
|
|
||||||
between optional absence and failed acquisition.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Preserve missing-on-open versus post-open
|
|
||||||
failure, for example with a small private failure record analogous to regular
|
|
||||||
file acquisition. Keep genuine missing-target rules, entry budgets, first-error
|
|
||||||
precedence, borrowed filesystem ownership, and safe existing traversal diagnostics.
|
|
||||||
Review both batched `fs.ReadDirFile` and standard fallback paths. A successful
|
|
||||||
second directory must not hide enumeration/close failure; raw backing error text
|
|
||||||
must not leak. No retries or filesystem transaction are proposed.
|
|
||||||
|
|
||||||
**Test improvements.** The shared fixture in
|
|
||||||
[native_filesystem_test.go](../../internal/engine/native_filesystem_test.go) and
|
|
||||||
[resource_context_test.go](../../internal/engine/resource_context_test.go) currently
|
|
||||||
checks aggregate closes and often only non-nil errors. One leaked handle and
|
|
||||||
one double-close can cancel out. Its canary check examines the terse error
|
|
||||||
summary, not structured diagnostics. Track per-handle closure, assert nil result,
|
|
||||||
diagnostic code and source, and inspect diagnostic messages/rendering for canaries.
|
|
||||||
Add entries/data-plus-error and close-phase cases through narrow failure seams.
|
|
||||||
Preserve intentionally ignored read-only regular-file close errors; this does
|
|
||||||
not impose a universal close-error policy.
|
|
||||||
|
|
||||||
### C02. Avoid Reexpanding Completed Finite Symlink Dependencies
|
|
||||||
|
|
||||||
Type: reported availability defect. Priority: medium. Source: security 1;
|
|
||||||
testing 1.
|
|
||||||
|
|
||||||
**Problem and evidence.** Host/native resolvers in
|
|
||||||
[filesystem_links.go](../../internal/engine/filesystem_links.go) track active links
|
|
||||||
but reuse no completed resolution. A finite contained graph `s00 -> .`,
|
|
||||||
`s01 -> s00/s00`, `s02 -> s01/s01`, and so on repeats completed dependencies.
|
|
||||||
Its work follows T(N) = 2T(N-1) + O(1), despite linear graph size. Directory
|
|
||||||
selection resolves discovered links before deciding on recursive descent, so
|
|
||||||
`recursive: false` does not avoid it. Existing source/entry/node/import-depth
|
|
||||||
budgets do not account for the repeated expansion.
|
|
||||||
|
|
||||||
A real-link fixture exposed through a native `fs.ReadLinkFS` wrapper recorded
|
|
||||||
32, 128, 512, 2,048, and 8,192 `Lstat` calls for 3, 5, 7, 9, and 11 entries;
|
|
||||||
corresponding `ReadLink` counts were 11, 57, 247, 1,013, and 4,083. Thirteen
|
|
||||||
entries reached a diagnostic ceiling of 12,000 `Lstat` calls. With following
|
|
||||||
disabled, that fixture used 17 `Lstat` calls and no `ReadLink`. The host resolver's
|
|
||||||
matching logic was reviewed, not separately operation-count tested.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Reuse fully resolved targets within the
|
|
||||||
resolution operation, keyed for the rooted namespace and full link path, or use
|
|
||||||
equivalent dependency evaluation. Retain active-path cycle detection, containment,
|
|
||||||
finite reuse, relative/absolute host semantics, and the stable-input assumption.
|
|
||||||
A global visited-means-cycle rule would incorrectly reject valid inputs. Cover
|
|
||||||
both resolvers and directory/glob entry points with small fixtures and bounded
|
|
||||||
operation observation, not timing thresholds or exhaustion tests. No arbitrary
|
|
||||||
link cap, public cache, or default-follow change is selected.
|
|
||||||
|
|
||||||
The availability risk requires influence over imported filesystem data; it is
|
|
||||||
not an anonymous remote vulnerability by itself. No concurrent mutation or
|
|
||||||
outside-root target is needed, and accidental graphs can cause the same work.
|
|
||||||
|
|
||||||
### C03. Separate Glob Entry Preparation From Traversal
|
|
||||||
|
|
||||||
Deferred under OQ-3. The evidence, proposed scope, and remaining questions now
|
|
||||||
belong to [future work](future.md#1-separate-glob-entry-preparation-from-traversal),
|
|
||||||
for consideration after this roadmap's selected work is complete.
|
|
||||||
|
|
||||||
## D. Retained History And Unused Evidence
|
|
||||||
|
|
||||||
### D01. Account For The Complete Published History Graph
|
|
||||||
|
|
||||||
Type: reported defect. Priority: medium. Source: resources 2; testing 1.
|
|
||||||
|
|
||||||
**Problem and evidence.** The checkpoint in [load.go](../../internal/engine/load.go)
|
|
||||||
and [history.go](../../internal/engine/history.go) counts a different root set
|
|
||||||
from [load_result.go](../../internal/engine/load_result.go) publication. Separately
|
|
||||||
cloned post-CLI input, `structuralInput`, `preCLI`, and later inheritance copies
|
|
||||||
are not all represented. A read-only diagnostic of a small successful load with
|
|
||||||
a no-op resolver counted 14 distinct nodes before including the two published
|
|
||||||
replay roots and 22 with them. Source inspection found that the checkpoint uses
|
|
||||||
a different, same-sized decoded-final instance and also counts 14 in this case.
|
|
||||||
Generalized to N scalar items, the reported counts were 3N + 8 versus 5N + 12;
|
|
||||||
800,000 items would pass the checkpoint yet exceed the existing 4,000,000-node
|
|
||||||
history allowance. That large case was derived, not allocated or executed.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Use pointer-aware accounting for every document
|
|
||||||
root actually retained at publication, including replay and copied inheritance
|
|
||||||
history, counting shared nodes once. Where practical, reserve/charge copies before
|
|
||||||
allocation. Coordinate removal of unused trees under D03 and recalculate the
|
|
||||||
retained set afterward; that removal alone does not establish complete accounting.
|
|
||||||
Reuse already-owned immutable snapshots only where equivalence and ownership
|
|
||||||
are demonstrated. This need not redesign public metadata copying.
|
|
||||||
|
|
||||||
Use a small controlled allowance to test publication integration and the existing
|
|
||||||
`explain.limit_exceeded` outcome, as well as count-once sharing. Helper-only tests
|
|
||||||
cannot detect roots omitted by callers. Preserve [historical
|
|
||||||
inspection](../config.md#historical-value-explanation), detached ownership, and
|
|
||||||
the existing quota; no smaller limit or universal heap-size bound is proposed.
|
|
||||||
|
|
||||||
### D02. Remove The Unconsumed Override-Result Pre-CLI Copy
|
|
||||||
|
|
||||||
Type: cleanup. Priority: medium. Source: deadcode 1.
|
|
||||||
|
|
||||||
**Problem and evidence.** `decodedOverrideResult.preCLI` in
|
|
||||||
[override_decode.go](../../internal/engine/override_decode.go) clones the entire
|
|
||||||
decoded final tree even without CLI assignments. Its readers are private tests;
|
|
||||||
production independently clones `before.final` into the active context in
|
|
||||||
[override_context.go](../../internal/engine/override_context.go).
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Remove that result member, its clone, and
|
|
||||||
assertions solely about it. Preserve the independently owned pre-CLI context
|
|
||||||
that actually feeds resolution, publication, and explanation. Retain behavioral
|
|
||||||
CLI/history/ownership coverage. The change removes a redundant traversal, not
|
|
||||||
the active replay snapshot or its detachment boundary.
|
|
||||||
|
|
||||||
### D03. Remove The Unqueried Published Structural-Input Copy
|
|
||||||
|
|
||||||
Type: cleanup. Priority: medium. Source: deadcode 2.
|
|
||||||
|
|
||||||
**Problem and evidence.** `Inspection.structuralInput` in
|
|
||||||
[load.go](../../internal/engine/load.go) is cloned and indexed at publication,
|
|
||||||
but has no production reader. [explain.go](../../internal/engine/explain.go)
|
|
||||||
uses source/inheritance replay, `preCLI`, and `input`. The immutable lookup is
|
|
||||||
addressed by supplied node pointers; indexing this otherwise unused root does
|
|
||||||
not make it useful and keeps its mappings alive.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Remove the inspection member, final clone,
|
|
||||||
and its `retainedTrees` inclusion. Keep earlier `resolved.structuralInput` uses
|
|
||||||
for path finalization and sensitivity. Check explanation, historical paths,
|
|
||||||
redaction, and ownership through active outputs. Coordinate D01 for all remaining
|
|
||||||
retained roots; do not infer that structural evidence is globally unused.
|
|
||||||
|
|
||||||
### D04. Stop Constructing Unconsumed Combination Evidence
|
|
||||||
|
|
||||||
Type: cleanup. Priority: medium. Source: deadcode 5.
|
|
||||||
|
|
||||||
**Problem and evidence.** `sequenceCombinationResult.discardedLower` in
|
|
||||||
[sequence_combine.go](../../internal/engine/sequence_combine.go) allocates a list
|
|
||||||
of all replaced lower indexes with no production reader. Three tests inspect it.
|
|
||||||
`inheritanceReplacement.path`, `previous`, and `effective` in
|
|
||||||
[inheritance.go](../../internal/engine/inheritance.go) are also unread, but their
|
|
||||||
writers perform path/origin copies and displaced-subtree walks. Active inheritance
|
|
||||||
history uses operations and projected changes elsewhere. Replacement `before`
|
|
||||||
and `after` trees still feed node accounting.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Remove only unread members and work solely
|
|
||||||
feeding them. Keep replacement trees, item projections, replacement mappings,
|
|
||||||
`appendedHigher`, active provenance/history, and node accounting. Keep
|
|
||||||
`sequenceIndexes` for append and `displacedOrigins` for overlay callers. Replace
|
|
||||||
private unused-evidence assertions with existing behavior-level replacement,
|
|
||||||
removed-descendant, and history protection where necessary. Coordinate D01 and
|
|
||||||
E03 without deleting active evidence merely because another representation exists.
|
|
||||||
|
|
||||||
## E. Collection Processing And Efficiency
|
|
||||||
|
|
||||||
All complexity estimates here describe identified internal work with bounded-
|
|
||||||
depth examples and expected hash lookup costs. Actual path processing, copied
|
|
||||||
payload, and expanded output remain costs; no end-to-end complexity or timing
|
|
||||||
guarantee is proposed.
|
|
||||||
|
|
||||||
### E01. Avoid Scanning Every Policy For Each Replacement
|
|
||||||
|
|
||||||
Type: optimization candidate. Priority: medium. Source: efficiency 1.
|
|
||||||
|
|
||||||
**Problem and evidence.** `retireCollectionPoliciesAtOrBelow` in
|
|
||||||
[collection_policy_state.go](../../internal/engine/collection_policy_state.go)
|
|
||||||
scans all P policies for each of Q ordinary replacements in
|
|
||||||
[overlay.go](../../internal/engine/overlay.go), even if the changed scalars are
|
|
||||||
unrelated to every collection. P = Q = N gives O(N²) visits for linear-sized
|
|
||||||
input and output within one tier.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Consider an operation-local semantic-path
|
|
||||||
index with descendant queries, maintained as policies change, or batch retirement
|
|
||||||
where timing remains equivalent. The disjoint case can approach O(P + Q), with
|
|
||||||
additional work charged to affected entries/path lengths. Preserve explicit-mode
|
|
||||||
persistence, record ownership, and the moment old identity retires; delayed
|
|
||||||
retirement must not clear newly established higher-tier policy. Evaluate existing
|
|
||||||
inheritance indexing without requiring identical interfaces. Retain a distinct
|
|
||||||
disjoint-replacement case even if implementation is coordinated with E02/E03.
|
|
||||||
|
|
||||||
### E02. Avoid Recopying Unrelated Policy Across Small Tiers
|
|
||||||
|
|
||||||
Type: optimization candidate. Priority: medium. Source: efficiency 2.
|
|
||||||
|
|
||||||
**Problem and evidence.** `lowerCollectionPoliciesForClaims` in
|
|
||||||
[collection_retirement.go](../../internal/engine/collection_retirement.go) clones
|
|
||||||
and scans lower policy, and `resolveStrictCollectionPolicies` in
|
|
||||||
[compose.go](../../internal/engine/compose.go) clones it again. Overlay preparation
|
|
||||||
repeats full-state work and checks retained destinations. P initial policies
|
|
||||||
followed by T scalar-only tiers cost O(T × P) work/allocation although only O(P + T)
|
|
||||||
material exists. Copies include record order and nested record policy; these
|
|
||||||
are transient private copies, not independently published snapshots.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Separate read-only lower-policy lookup from
|
|
||||||
owned current-tier changes, localize work to incoming claims/declarations, and
|
|
||||||
apply changes to the assembly owner. Keep copies at actual mutable ownership
|
|
||||||
boundaries. Removing the immediate duplicate copy helps but does not alone fix
|
|
||||||
O(T × P) growth. Preserve pending declarations, replacement eligibility, and
|
|
||||||
independent nested record policies. This case remains distinct from E01 and the
|
|
||||||
deferred public metadata-copy experiment.
|
|
||||||
|
|
||||||
### E03. Clarify Policy Preparation And Merge Evidence Application
|
|
||||||
|
|
||||||
Type: selected refactor. Priority: medium. Source: complexity 3.
|
|
||||||
|
|
||||||
**Problem and evidence.** [overlay.go](../../internal/engine/overlay.go) combines
|
|
||||||
claim/declaration preparation, operation resolution, callbacks, diagnostics,
|
|
||||||
origins/history, retirement, record policy rebuilding, and declaration-only
|
|
||||||
finalization. Overlay and [inheritance](../../internal/engine/inheritance_collection.go)
|
|
||||||
separately translate sequence results into history descriptors and reconstruct
|
|
||||||
record order. `combineSequence` already owns shared value combination.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Consider explicit preparation, application,
|
|
||||||
and finalization operations with a small result/owner for evidence and diagnostics.
|
|
||||||
Share straightforward history-descriptor construction; assess projected-record
|
|
||||||
selection sharing while keeping precedence and inheritance policy ownership
|
|
||||||
explicit. Do not unify distinct origin or mode-lifetime rules behind a generic
|
|
||||||
strategy framework. Coordinate E01/E02 so extraction does not preserve avoidable
|
|
||||||
full-table work. Preserve pending declarations, replacement ordering, retained
|
|
||||||
versus incoming record owners, and stable history. D04 can remove unused evidence
|
|
||||||
without requiring this broader refactor.
|
|
||||||
|
|
||||||
### E04. Index Tier-History Projection Lookups
|
|
||||||
|
|
||||||
Type: optimization candidate. Priority: medium. Source: efficiency 3.
|
|
||||||
|
|
||||||
**Problem and evidence.** `projectTierHistoryApplications` in
|
|
||||||
[sequence_history_projection.go](../../internal/engine/sequence_history_projection.go)
|
|
||||||
calls unindexed `documentAtPath` for each mapped sequence. N sibling collections
|
|
||||||
selected by one wildcard identity registration incur O(N²) mapping comparisons
|
|
||||||
while projecting linear-sized source/history material.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Lazily prepare one read-only `documentLookup`
|
|
||||||
per original application value with mapped contributions, or collect needed nodes
|
|
||||||
in one traversal. With V nodes, S contributions, depth D, and I projected items,
|
|
||||||
that portion can be O(V + S × D + I). Preserve authored coordinates, sparse item
|
|
||||||
projections, replacement evidence, and independent output storage. This is load-
|
|
||||||
time coordinate projection, not branch-local explanation replay.
|
|
||||||
|
|
||||||
### E05. Restore Compound Identity Without Repeated Mapping Scans
|
|
||||||
|
|
||||||
Type: optimization candidate. Priority: medium. Source: efficiency 4.
|
|
||||||
|
|
||||||
**Problem and evidence.** `preserveInheritanceIdentityFields` in
|
|
||||||
[inheritance_scope.go](../../internal/engine/inheritance_scope.go) searches child
|
|
||||||
and merged mappings for each identity field. K fields and C/M child/merged
|
|
||||||
entries cost O(K × (C + M)) comparisons plus required clones. A wide compound
|
|
||||||
identity can make this quadratic without expanding record output.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Index merged positions and walk child entries
|
|
||||||
against an identity-field set, or use equivalent local mapping indexes, for
|
|
||||||
O(K + C + M) lookup work plus copies. Preserve child-owned identity values and
|
|
||||||
locations, mapping order, and detached nodes. Do not change identity equality or
|
|
||||||
parent selection. Removing the restoration step instead requires a separate
|
|
||||||
equivalence review; apparent duplication alone is insufficient.
|
|
||||||
|
|
||||||
## F. Remaining Maintenance And Test Improvements
|
|
||||||
|
|
||||||
### F01. Remove The Test-Only Eager Acquisition Mode
|
|
||||||
|
|
||||||
Type: cleanup. Priority: medium. Sources: complexity 1, deadcode 8.
|
|
||||||
|
|
||||||
**Problem and evidence.** Both production constructors of the request declared in
|
|
||||||
[acquisition.go](../../internal/engine/acquisition.go) set
|
|
||||||
`inputAcquisitionRequest.deferAssembly` to true. Only private
|
|
||||||
test helpers select the eager composition branch in
|
|
||||||
[import_acquisition.go](../../internal/engine/import_acquisition.go). Acquisition
|
|
||||||
maintains two return protocols and tests can bypass the production phase handoff.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Return retained scopes consistently and remove
|
|
||||||
the unused production mode. Tests needing composition should invoke the active
|
|
||||||
composition phase through a small test-only helper; acquisition-only tests should
|
|
||||||
remain acquisition-only. Preserve closure, pending-item reservation, accounting,
|
|
||||||
diagnostics, and meaningful composition coverage. This does not retire public
|
|
||||||
loading workflows or imply that every nil-state/directive context is unused.
|
|
||||||
|
|
||||||
### F02. Clarify Active Acquisition And Coordinator Handoffs
|
|
||||||
|
|
||||||
Type: selected refactor. Priority: medium. Source: complexity 2.
|
|
||||||
|
|
||||||
**Problem and evidence.** `acquireInput` in
|
|
||||||
[acquisition.go](../../internal/engine/acquisition.go) combines discovery, parsing,
|
|
||||||
directive/version branches, placement/accounting, dispatch, and passive contexts.
|
|
||||||
Version branches repeat source-ID/failure/result handling, and layered processing
|
|
||||||
extracts and restores directives. [load.go](../../internal/engine/load.go) contains
|
|
||||||
a substantial ordinary-load context fallback; [load_request.go](../../internal/engine/load_request.go)
|
|
||||||
repeats tier declaration/application/projection handoffs. Parsed, local, composed,
|
|
||||||
inherited, and version-path representations are difficult to follow together.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Consider cohesive parsed-input directive/version
|
|
||||||
processing and standalone placement/registration operations, plus explicit context,
|
|
||||||
structural-expansion, and repeated tier handoffs. Reuse existing state/results;
|
|
||||||
helpers that merely share the same mutable surrounding locals do not resolve
|
|
||||||
ownership. Preserve visible phase order, ordinary/layered overlay differences,
|
|
||||||
default/enabled version timing, warnings, exactly-once callbacks, and load-wide
|
|
||||||
versus tier-local state. F01 removes a different, inactive protocol; neither task
|
|
||||||
requires a new context hierarchy or public request model.
|
|
||||||
|
|
||||||
### F03. Retire The Parallel Test-Only Import Directive Parser
|
|
||||||
|
|
||||||
Type: cleanup. Priority: low. Source: deadcode 7.
|
|
||||||
|
|
||||||
**Problem and evidence.** `parseImportSpecificationDirective` in
|
|
||||||
[import_spec.go](../../internal/engine/import_spec.go) is called only by the
|
|
||||||
`parseImportSpecsYAML` test fixture. It duplicates mapping/field dispatch already
|
|
||||||
owned by `parseRootDirectiveBlock` in [directives.go](../../internal/engine/directives.go).
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Use the active parser with the appropriate
|
|
||||||
context, or parse the declaration list directly when that is the tested boundary,
|
|
||||||
then remove the obsolete implementation. Preserve malformed-declaration and
|
|
||||||
selector coverage. Keep the active imports-only context used in layered version
|
|
||||||
processing; thin test helpers delegating to production are not inherently dead.
|
|
||||||
|
|
||||||
### F04. Remove The Disconnected Root Record-Clone Family
|
|
||||||
|
|
||||||
Type: cleanup. Priority: low. Source: deadcode 3; related testing 6.
|
|
||||||
|
|
||||||
**Problem and evidence.** The private family in root [records.go](../../records.go)
|
|
||||||
has no production facade entry: `cloneLocation`, `cloneLocations`, `cloneSourceInfo`,
|
|
||||||
`cloneImportSelections`, `cloneSourceVersionCheck`, `cloneSourceInfos`, `cloneOrigin`,
|
|
||||||
`cloneFieldInfo`, `cloneChange`, `cloneDiagnostic`, and `cloneDiagnostics`.
|
|
||||||
Helpers refer to each other; only tests call `cloneSourceInfo`. Active root-owned
|
|
||||||
records come through [conversion.go](../../conversion.go).
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Delete the disconnected family and tests solely
|
|
||||||
protecting it. Keep conversion assertions and useful returned-record ownership
|
|
||||||
coverage through repeated public queries and mutation of mutable nested results.
|
|
||||||
Replacing an entire `Destination` field on a copied struct does not demonstrate
|
|
||||||
nested detachment. Keep active engine clones and `Path.clone`; do not expose
|
|
||||||
opaque path internals or invent a stronger copy guarantee. Coordinate F12.
|
|
||||||
|
|
||||||
### F05. Remove Uncalled Engine Helpers
|
|
||||||
|
|
||||||
Type: cleanup. Priority: low. Source: deadcode 4; placement subset also complexity 5.
|
|
||||||
|
|
||||||
The type-aware audit found no callers or function-value uses in the inspected
|
|
||||||
production/test build configurations for the following helpers:
|
|
||||||
|
|
||||||
| Candidate | Active counterpart to preserve |
|
|
||||||
| --- | --- |
|
|
||||||
| `mergeDisjointMappings` in [compose.go](../../internal/engine/compose.go) | `mergeDisjointMappingsAtPath` and shared recursive merging; the old helper references only itself. |
|
|
||||||
| `mergeHistoryDocument` in [history.go](../../internal/engine/history.go) | `mergeHistoryDocumentAtPath` with sequence evidence. |
|
|
||||||
| `inheritanceDirectiveLocation` in [inheritance.go](../../internal/engine/inheritance.go) | Preparation's directive entry/location handling. |
|
|
||||||
| `addSecretEvidencePath` in [secret.go](../../internal/engine/secret.go) | Projected version sensitivity evidence and the active path accumulator. |
|
|
||||||
| `PlacementModeFromFacade`, `PlacementModeForFacade` in [destination.go](../../internal/engine/destination.go) | Input/options conversion and `SourceInfoPlacementModeForFacade`. |
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Remove these definitions and imports/comments
|
|
||||||
solely supporting them after confirming references at implementation time. Preserve
|
|
||||||
active counterparts and public root APIs. The placement pair can be removed
|
|
||||||
without selecting the broader transport work in F07.
|
|
||||||
|
|
||||||
### F06. Trim Unread Private Result And Transport Members
|
|
||||||
|
|
||||||
Type: cleanup. Priority: low. Source: deadcode 6.
|
|
||||||
|
|
||||||
The audit identified the following populated members without production readers.
|
|
||||||
Some private tests inspect them. Remove only these slots and computations solely
|
|
||||||
feeding them, preserving validation and active outputs:
|
|
||||||
|
|
||||||
| Owner and candidate members | Boundary to preserve |
|
|
||||||
| --- | --- |
|
|
||||||
| `collectionPolicyResolution.declarations`, `declarationsPresent`, `identityDeclarationPresent`, `applicationIdentityFields` in [collection_policy.go](../../internal/engine/collection_policy.go) | Local declaration normalization, `modeDeclarationPresent`, and `applicationIdentityPresent`. |
|
|
||||||
| `inputPresence.null` in [decode.go](../../internal/engine/decode.go) | Presence/location and effective-node null reporting. |
|
|
||||||
| `rootDirectiveExtraction.importsPresent`, `sequencesPresent` in [directives.go](../../internal/engine/directives.go) | Parsed declarations, diagnostics, and directive validation. |
|
|
||||||
| `importSelection.outcome` in [import_selection.go](../../internal/engine/import_selection.go) and glob selection | Candidate/admission behavior and the distinct, active public `importSelectionEvidence.outcome`. |
|
|
||||||
| `importDeclarationSpec.excludeSet` in [import_spec.go](../../internal/engine/import_spec.go) | Active `exclude` policy. |
|
|
||||||
| `schemaPath.field`, `dynamic` in [schema.go](../../internal/engine/schema.go) | Descriptor and success result used by path consumers; coordinate A01. |
|
|
||||||
| `selectedOverlayRequest.filesystem` in [selected_overlay.go](../../internal/engine/selected_overlay.go) | Explicit filesystem-access argument and borrowed ownership. |
|
|
||||||
| `sequenceDeclarationSpec.destinationLocation` in [sequence_declaration.go](../../internal/engine/sequence_declaration.go) | Other active declaration/field diagnostic locations. |
|
|
||||||
| `sourceVersionEvidence.absence`, `checked`; `sourceVersionAncestor.kind` in [source_version.go](../../internal/engine/source_version.go) | Actual checks, version/presence/location evidence, ancestor paths, and sensitivity. |
|
|
||||||
| `postDecodeOverrideContext.sources` in [override_context.go](../../internal/engine/override_context.go) | Local source list used to build callback resolution input. |
|
|
||||||
|
|
||||||
**Acceptance.** Preserve public presence, null, selection, version, diagnostic,
|
|
||||||
and ownership behavior. Rework assertions on inactive annotations only where
|
|
||||||
they protect a meaningful supported outcome. A lack of selector reads alone is
|
|
||||||
not a general removal rule: map keys, embedded records, and interfaces require
|
|
||||||
separate analysis.
|
|
||||||
|
|
||||||
### F07. Simplify Redundant Facade-To-Engine Transport
|
|
||||||
|
|
||||||
Deferred under OQ-3. The evidence, proposed scope, and remaining questions now
|
|
||||||
belong to [future work](future.md#2-simplify-redundant-facade-to-engine-transport),
|
|
||||||
for consideration after this roadmap's selected work is complete. F05's bounded
|
|
||||||
removal of unused placement bridges remains selected.
|
|
||||||
|
|
||||||
### F08. Index Secret-Failure Location Queries Lazily
|
|
||||||
|
|
||||||
Type: optimization candidate. Priority: low. Source: efficiency 5.
|
|
||||||
|
|
||||||
**Problem and evidence.** `secretDiagnostic` in
|
|
||||||
[secret_reference.go](../../internal/engine/secret_reference.go) calls unindexed
|
|
||||||
nearest-location traversal for each failed reference. N missing references in a
|
|
||||||
wide mapping incur O(N²) comparisons, even when they use one cached environment
|
|
||||||
name. Normal deterministic traversal/sorting does not require these scans.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Consider a lazy read-only `documentLookup`
|
|
||||||
shared during secret resolution, or carry relevant location state through the
|
|
||||||
walk. With V nodes, F failures, and depth D, attribution can be O(V + F × D),
|
|
||||||
apart from sorting. Keep the successful path free of an unnecessary eager index.
|
|
||||||
Preserve all diagnostics, ancestor fallback, source coordinates, and safe messages;
|
|
||||||
failing fast would change behavior rather than optimize it.
|
|
||||||
|
|
||||||
### F09. Cover Public Historical-Path Enumeration
|
|
||||||
|
|
||||||
Type: test improvement. Priority: medium. Source: testing 2; related testing 6.
|
|
||||||
|
|
||||||
**Problem and evidence.** Root `Metadata.HistoricalPaths` in
|
|
||||||
[metadata.go](../../metadata.go) had no executed statements in the aggregate
|
|
||||||
profile. Engine tests cover it thoroughly, but not root path conversion. An empty
|
|
||||||
or incorrectly converted public result could leave engine tests green.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Add one external-package test enumerating
|
|
||||||
effective and removed paths, using returned paths with `Lookup` and `History`,
|
|
||||||
including a numeric-key/sequence shape change. Cover sorted unique pointer
|
|
||||||
spelling, zero metadata, and replacement of a returned slice element without
|
|
||||||
affecting later queries. Keep detailed path mutation/shape tests in the engine.
|
|
||||||
This can replace F12's private segment-representation assertion without copying
|
|
||||||
the complete internal matrix or exposing private segments.
|
|
||||||
|
|
||||||
### F10. Strengthen Fuzz Detachment Assertions
|
|
||||||
|
|
||||||
Type: test improvement. Priority: low. Source: testing 4.
|
|
||||||
|
|
||||||
**Problem and evidence.** `FuzzCompositionAndOverlay` in
|
|
||||||
[composition_fuzz_test.go](../../internal/engine/composition_fuzz_test.go) appends
|
|
||||||
to the result root then compares inputs. A shallow root copy sharing existing
|
|
||||||
descendants can pass because append changes neither the input's slice header nor
|
|
||||||
elements within its old length. Existing deterministic merge tests already
|
|
||||||
exercise meaningful nested mutation.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Mutate an existing result descendant or
|
|
||||||
in-length element within the bounded fuzz target, then compare both inputs.
|
|
||||||
Handle empty containers separately and retain input immutability/commutativity
|
|
||||||
properties. Improve the existing assertion rather than adding another ownership
|
|
||||||
matrix or claiming all ownership coverage is absent.
|
|
||||||
|
|
||||||
### F11. Replace Exact Stat-Call Choreography With Observable Ordering
|
|
||||||
|
|
||||||
Type: test cleanup. Priority: low. Source: testing 5.
|
|
||||||
|
|
||||||
**Problem and evidence.** `TestSelectNamedImportsChecksNormalizedPathsLexically`
|
|
||||||
in [import_selection_test.go](../../internal/engine/import_selection_test.go)
|
|
||||||
requires `a.yaml,a.yaml,z.yaml,z.yaml` stat calls but checks candidate count only.
|
|
||||||
Lexical handling matters; exactly two stats per file is not an established contract.
|
|
||||||
|
|
||||||
**Recommendation and acceptance.** Assert ordered candidate paths and, where
|
|
||||||
needed, the first diagnostic site using failing targets. Consolidate with existing
|
|
||||||
selection tests and delete the standalone case if its protection is subsumed.
|
|
||||||
Keep meaningful observations such as not opening excluded contents. Do not turn
|
|
||||||
incidental helper call counts into an efficiency or behavior guarantee.
|
|
||||||
|
|
||||||
### F12. Consolidate Facade Scaffolding Around Public Behavior
|
|
||||||
|
|
||||||
Type: test cleanup. Priority: low. Source: testing 6; related deadcode 3.
|
|
||||||
|
|
||||||
**Evidence and recommendations.**
|
|
||||||
|
|
||||||
- Remove the duplicate six-type ownership loop in
|
|
||||||
[facade_compatibility_test.go](../../facade_compatibility_test.go), retaining the
|
|
||||||
comprehensive owner in [facade_candidate_test.go](../../facade_candidate_test.go),
|
|
||||||
compile-time interfaces, public construction, and distinct lifecycle checks.
|
|
||||||
A literal assignment equality assertion on a new `Result.Config` adds no
|
|
||||||
protection beyond compilation and ordinary Go assignment.
|
|
||||||
- Replace the former `TestWrappedPathsRetainEngineSegmentIntent` internal
|
|
||||||
representation check with public contextual mapping/index behavior under F09.
|
|
||||||
Keep engine segment-intent coverage; public source-form behavior is consolidated
|
|
||||||
in [facade_equivalence_test.go](../../facade_equivalence_test.go).
|
|
||||||
- Move `TestWrappedSourcesPreserveEngineOwnership` from direct `engine.Load`
|
|
||||||
through `source.toEngine()` to public `Load` and root error assertions.
|
|
||||||
Consolidate reader cases with [facade_equivalence_test.go](../../facade_equivalence_test.go)
|
|
||||||
while preserving explicit copied-reader and byte-source mutation coverage.
|
|
||||||
- F04 owns removal of the disconnected clone family and the ineffective whole-
|
|
||||||
destination assignment assertion in [records_test.go](../../records_test.go).
|
|
||||||
Keep useful conversion and returned-record ownership protection there or in
|
|
||||||
existing public source-snapshot tests.
|
|
||||||
|
|
||||||
**Acceptance.** Remove scaffolding only after preserving its distinct behavioral
|
|
||||||
risks. Root integration tests still protect conversion, error typing, callbacks,
|
|
||||||
source identity, disclosure, and ownership; engine coverage does not substitute
|
|
||||||
for them. Static public/dependency-boundary tests remain useful.
|
|
||||||
|
|
||||||
## Boundaries To Preserve
|
|
||||||
|
|
||||||
The audits found the architecture coherent and did not establish a need for new
|
|
||||||
public features, a replacement loading API, package proliferation, or a general
|
|
||||||
visitor/strategy framework. Root-owned conversions, synchronous load-local state,
|
|
||||||
shared dependency scheduling, indexed combination, separated inheritance phases,
|
|
||||||
and passive resource contexts remain useful owners. The clone traversal was
|
|
||||||
already reviewed and retained at `34cbc0d:docs/roadmap/consolidation.md`; a high
|
|
||||||
complexity estimate does not reopen it.
|
|
||||||
|
|
||||||
No additional supported-use data race, deadlock, background-work leak, credential
|
|
||||||
disclosure, command injection, authority bypass, or host containment escape was
|
|
||||||
established. These are limited audit observations, not absence guarantees.
|
|
||||||
Caller-owned mutable objects, blocking operations, safe callback messages, and
|
|
||||||
borrowed filesystem behavior retain their documented responsibilities. No new
|
|
||||||
locking, cancellation, retry, authentication, permission, or environment-variable
|
|
||||||
restriction follows from these findings.
|
|
||||||
|
|
||||||
Preserve raw-error suppression, structured safe diagnostics, original application
|
|
||||||
panic propagation, intentional bootstrap partial writes, allowed partial writer
|
|
||||||
output, and documented read-only regular-file close handling. Regression tests
|
|
||||||
must enforce existing contracts rather than broaden them. Optional absence must
|
|
||||||
remain distinct from acquisition failure, and genuine finite reuse from cycles.
|
|
||||||
|
|
||||||
Public APIs are not dead merely because repository examples omit them. Interface
|
|
||||||
dispatch, reflection, build constraints, identity tokens, map-key equality, and
|
|
||||||
uninstantiated generics require more than caller counts. In particular,
|
|
||||||
`decodeInlineMapEntry` and import-cycle diagnostics remain active.
|
|
||||||
`Inspection.resolution` retains recorded snapshots under the existing resolution
|
|
||||||
contract even though replay uses a coalesced view; it is not a cleanup candidate.
|
|
||||||
|
|
||||||
Selected stable Kahn ordering, deterministic sorting, exact numeric identity,
|
|
||||||
and the expressly selected registration-overlap check are not reconsidered here.
|
|
||||||
Required detached copies and genuinely expanding output remain legitimate costs.
|
|
||||||
The broader replay, metadata-handoff, cross-load schema-cache, and contributor-
|
|
||||||
trace experiments stay in [future.md](future.md#5-performance-experiments).
|
|
||||||
Local fixes and removal of proven unused work do not select those experiments.
|
|
||||||
|
|
||||||
## Validation Approach For Selected Work
|
|
||||||
|
|
||||||
Use the existing [development guide](../development.md#baseline-validation) and
|
|
||||||
[testing policy](../policy/testing.md). Add meaningful regressions with the
|
|
||||||
selected fixes, at the narrowest stable boundary that detects the reported
|
|
||||||
failure. Extend existing owners rather than duplicating internal matrices at
|
|
||||||
every layer. Assert nil results and safe structured failures where relevant,
|
|
||||||
and verify retained state/handles independently when they have distinct owners.
|
|
||||||
|
|
||||||
Keep termination, allocation, and link-expansion probes bounded. Use small
|
|
||||||
controlled budgets, disposable processes where necessary, and filesystem
|
|
||||||
observation where it detects the specific defect. No wall-clock thresholds,
|
|
||||||
coverage percentages, complexity-score limits, exact incidental syscall traces,
|
|
||||||
new resource caps, or extra release gates are selected. Algorithmic evidence is
|
|
||||||
not a measured speedup. Performance experiments need separate scope and suitable
|
|
||||||
measurement conditions; no benchmarks were run for this consolidation.
|
|
||||||
|
|
||||||
Once selected work changes behavior or structure, update its canonical contract
|
|
||||||
or internal owner as appropriate. This review document remains the finding/status
|
|
||||||
owner until an implementation plan is prepared; completed records can retire
|
|
||||||
to Git history under the normal documentation lifecycle.
|
|
||||||
|
|
||||||
## Original-Finding Map
|
|
||||||
|
|
||||||
Original files are retrieved from `ecd5d5d:docs/roadmap/<filename>`. Every numbered
|
|
||||||
finding is mapped below; repeated citations are corroboration or shared coverage,
|
|
||||||
not extra fixes.
|
|
||||||
|
|
||||||
| Original finding | Consolidated owner |
|
|
||||||
| --- | --- |
|
|
||||||
| correctness 1 | [A01](#a01-terminate-pointer-only-recursive-descriptor-traversal) |
|
|
||||||
| correctness 2 | [A02](#a02-make-failed-dynamic-descriptor-construction-transactional) |
|
|
||||||
| correctness 3 | [A03](#a03-distinguish-acyclic-slice-views-in-cycle-detection) |
|
|
||||||
| correctness 4 | [B01](#b01-deactivate-callback-contexts-during-panic-unwinding) |
|
|
||||||
| efficiency 1 | [E01](#e01-avoid-scanning-every-policy-for-each-replacement) |
|
|
||||||
| efficiency 2 | [E02](#e02-avoid-recopying-unrelated-policy-across-small-tiers) |
|
|
||||||
| efficiency 3 | [E04](#e04-index-tier-history-projection-lookups) |
|
|
||||||
| efficiency 4 | [E05](#e05-restore-compound-identity-without-repeated-mapping-scans) |
|
|
||||||
| efficiency 5 | [F08](#f08-index-secret-failure-location-queries-lazily) |
|
|
||||||
| efficiency 6 | [A05](#a05-reuse-successful-dynamic-type-eligibility-checks-locally) |
|
|
||||||
| complexity 1 | [F01](#f01-remove-the-test-only-eager-acquisition-mode) |
|
|
||||||
| complexity 2 | [F02](#f02-clarify-active-acquisition-and-coordinator-handoffs) |
|
|
||||||
| complexity 3 | [E03](#e03-clarify-policy-preparation-and-merge-evidence-application) |
|
|
||||||
| complexity 4 | [C03](#c03-separate-glob-entry-preparation-from-traversal) |
|
|
||||||
| complexity 5 | [F07](#f07-simplify-redundant-facade-to-engine-transport), with bounded bridge removal in [F05](#f05-remove-uncalled-engine-helpers) |
|
|
||||||
| concurrency 1 | [B01](#b01-deactivate-callback-contexts-during-panic-unwinding), corroboration |
|
|
||||||
| resources 1 | [A04](#a04-enforce-existing-node-budgets-before-container-reservations) |
|
|
||||||
| resources 2 | [D01](#d01-account-for-the-complete-published-history-graph) |
|
|
||||||
| resources 3 | [B02](#b02-close-the-owned-import-root-when-a-version-callback-panics) |
|
|
||||||
| error-handling 1 | [C01](#c01-preserve-directory-failure-phase-and-strengthen-its-tests) |
|
|
||||||
| security 1 | [C02](#c02-avoid-reexpanding-completed-finite-symlink-dependencies) |
|
|
||||||
| testing 1 | Regression acceptance under A01–A04, B01–B02, C02, and D01 |
|
|
||||||
| testing 2 | [F09](#f09-cover-public-historical-path-enumeration) |
|
|
||||||
| testing 3 | [C01](#c01-preserve-directory-failure-phase-and-strengthen-its-tests), including shared fixture assertions |
|
|
||||||
| testing 4 | [F10](#f10-strengthen-fuzz-detachment-assertions) |
|
|
||||||
| testing 5 | [F11](#f11-replace-exact-stat-call-choreography-with-observable-ordering) |
|
|
||||||
| testing 6 | [F12](#f12-consolidate-facade-scaffolding-around-public-behavior), with F04 and F09 |
|
|
||||||
| deadcode 1 | [D02](#d02-remove-the-unconsumed-override-result-pre-cli-copy) |
|
|
||||||
| deadcode 2 | [D03](#d03-remove-the-unqueried-published-structural-input-copy) |
|
|
||||||
| deadcode 3 | [F04](#f04-remove-the-disconnected-root-record-clone-family) |
|
|
||||||
| deadcode 4 | [F05](#f05-remove-uncalled-engine-helpers) |
|
|
||||||
| deadcode 5 | [D04](#d04-stop-constructing-unconsumed-combination-evidence) |
|
|
||||||
| deadcode 6 | [F06](#f06-trim-unread-private-result-and-transport-members) |
|
|
||||||
| deadcode 7 | [F03](#f03-retire-the-parallel-test-only-import-directive-parser) |
|
|
||||||
| deadcode 8 | [F01](#f01-remove-the-test-only-eager-acquisition-mode), duplicate of complexity 1 |
|
|
||||||
|
|
||||||
## Settled Scope Decisions
|
|
||||||
|
|
||||||
### OQ-1. What Should The First Implementation Scope Include?
|
|
||||||
|
|
||||||
**Settled:** include all nine reported defects with their regression coverage,
|
|
||||||
the bounded cleanup, and the identified independent test improvements.
|
|
||||||
|
|
||||||
| Selected work | Finding IDs |
|
|
||||||
| --- | --- |
|
|
||||||
| Reported defects and accompanying regression coverage | A01–A04, B01–B02, C01–C02, D01 |
|
|
||||||
| Removal of unused copies, evidence, helpers, fields, and test-only production paths | D02–D04, F01, F03–F06 |
|
|
||||||
| Public coverage and stronger or less redundant test assertions | F09–F12 |
|
|
||||||
|
|
||||||
This selects 21 review entries. Reconfirm cleanup evidence on the implementation
|
|
||||||
baseline before removal, and preserve distinct behavioral protection when
|
|
||||||
replacing tests. Coordinate unused history copies with D01's retained-graph
|
|
||||||
accounting; verify callback-state closure and host-root closure independently.
|
|
||||||
Keep fixes and cleanup in focused, reviewable commits, even if delivered in one
|
|
||||||
sprint. Cleanup must not delay a consequential fix.
|
|
||||||
|
|
||||||
The combined scope restores existing contracts and removes demonstrated
|
|
||||||
maintenance burden while avoiding repeated work at the same ownership and test
|
|
||||||
boundaries. Delivering defects first and cleanup separately would reduce the
|
|
||||||
initial review surface, but would retain redundant work and revisit those
|
|
||||||
boundaries. The combined approach is selected.
|
|
||||||
|
|
||||||
Efficiency and broader structural choices are addressed separately below. This
|
|
||||||
decision does not select new public behavior or establish an implementation
|
|
||||||
sequence.
|
|
||||||
|
|
||||||
### OQ-2. Which Efficiency Candidates Should Accompany Remediation?
|
|
||||||
|
|
||||||
**Settled:** include the four localized efficiency improvements below. E01/E02
|
|
||||||
are addressed separately with collection-policy preparation and evidence
|
|
||||||
ownership (E03) under the settled OQ-3 decision.
|
|
||||||
|
|
||||||
| Selected improvement | Scope and dependency |
|
|
||||||
| --- | --- |
|
|
||||||
| E04: tier-history projection lookups | Reuse a local lookup index or equivalent traversal while preserving authored coordinates and projection evidence. |
|
|
||||||
| E05: compound identity restoration | Avoid repeated mapping scans while preserving child identity, mapping order, and detached nodes. |
|
|
||||||
| A05: dynamic-type eligibility reuse | Reuse only completed successful checks within the operation, after A02 repairs descriptor construction failure state. |
|
|
||||||
| F08: secret-failure location lookup | Include at lower priority; prepare lookup state lazily so successful loads avoid unnecessary indexing. |
|
|
||||||
|
|
||||||
Together with OQ-1, this selects 25 review entries. These local changes address
|
|
||||||
source-demonstrated repeated work without requiring public APIs, persistent
|
|
||||||
caches, or a new architectural model. Preserve their behavioral acceptance
|
|
||||||
cases and keep implementation proportionate. E01/E02 involve policy retirement
|
|
||||||
timing and mutable ownership, so their scope belongs with the E03 discussion.
|
|
||||||
|
|
||||||
Deferring all six until consumer profiling was considered. That would reduce
|
|
||||||
initial scope and avoid optimizing uncommon workloads, but would retain known
|
|
||||||
growth patterns with relatively direct remedies in the four selected cases.
|
|
||||||
The selected approach does not promise a measured startup improvement; practical
|
|
||||||
benefit depends on configuration shape and workload. Broader experiments in
|
|
||||||
[future.md](future.md#5-performance-experiments) remain deferred. C02's availability
|
|
||||||
fix and A04/D01's existing-budget enforcement remain selected independently.
|
|
||||||
|
|
||||||
### OQ-3. How Much Structural Refactoring Should Be Selected?
|
|
||||||
|
|
||||||
**Settled:** select E01, E02, E03, and F02. Defer C03 and F07 to the first two
|
|
||||||
entries of [future.md](future.md) for consideration after the rest of this roadmap
|
|
||||||
is complete. F01's removal of the test-only protocol remains selected under OQ-1.
|
|
||||||
|
|
||||||
| Selected work | Scope |
|
|
||||||
| --- | --- |
|
|
||||||
| E01/E02/E03: collection-policy retirement, copying, and evidence ownership | Address the related costs and preparation/application boundaries together, retaining distinct scaling and behavioral acceptance cases. |
|
|
||||||
| F02: active acquisition and coordinator handoffs | Extract cohesive repeated operations with clear inputs/results while preserving visible phase order and ordinary/layered differences. |
|
|
||||||
|
|
||||||
For collection policy, distinguish read-only existing state, policy affected by
|
|
||||||
incoming declarations or replacements, and the changes/evidence retained by the
|
|
||||||
assembly. Preserve retirement timing, newly established policy, explicit-mode
|
|
||||||
persistence, and nested record ownership. A smaller function or fewer copies
|
|
||||||
alone does not establish success. For acquisition, moving blocks into helpers
|
|
||||||
that continue sharing mutable surrounding state would not clarify the handoff;
|
|
||||||
keep the existing loading model and named responsibilities.
|
|
||||||
|
|
||||||
The selected combination offers a focused improvement in efficiency and
|
|
||||||
maintainability at active ownership boundaries. Including C03/F07 now would
|
|
||||||
broaden review for more modest readability benefits; neither is necessary for
|
|
||||||
the selected fixes. They remain plausible follow-up work, not rejected proposals.
|
|
||||||
Deferring all structural work would reduce immediate change scope but retain the
|
|
||||||
policy inefficiencies and difficult acquisition handoffs. No new public API,
|
|
||||||
generic framework, or configuration semantics are selected.
|
|
||||||
|
|
||||||
Together, OQ-1 through OQ-3 select 29 entries. All scope questions are settled;
|
|
||||||
the next step is to prepare the implementation plan with sequencing and focused
|
|
||||||
validation. No implementation has been performed by these planning decisions.
|
|
||||||
@@ -6,14 +6,6 @@ plans, or release commitments. Adopting a candidate requires an explicit design
|
|||||||
decision and a separate plan. Significant architectural exclusions and their rationale belong
|
decision and a separate plan. Significant architectural exclusions and their rationale belong
|
||||||
in ADRs; this document does not maintain a list of rejected proposals.
|
in ADRs; this document does not maintain a list of rejected proposals.
|
||||||
|
|
||||||
## 1. Separate glob entry preparation from traversal
|
|
||||||
|
|
||||||
Implemented in the [internal-boundary roadmap](internal-boundaries.md#glob-entry-preparation) (C03).
|
|
||||||
|
|
||||||
## 2. Simplify redundant facade-to-engine transport
|
|
||||||
|
|
||||||
Implemented in the [internal-boundary roadmap](internal-boundaries.md#facade-to-engine-transport) (F07).
|
|
||||||
|
|
||||||
## 3. Recursive body merging for identified records
|
## 3. Recursive body merging for identified records
|
||||||
|
|
||||||
Status: deferred. The implemented
|
Status: deferred. The implemented
|
||||||
@@ -91,3 +83,22 @@ evidence are preserved in Git history at `8e91a2b` under `docs/roadmap/`.
|
|||||||
across loads without introducing aliasing?
|
across loads without introducing aliasing?
|
||||||
- What cache lifetime and concurrency design would make schema reuse worthwhile
|
- What cache lifetime and concurrency design would make schema reuse worthwhile
|
||||||
without excessive retained memory?
|
without excessive retained memory?
|
||||||
|
|
||||||
|
## 6. Public path comparison
|
||||||
|
|
||||||
|
Status: deferred pending a concrete consumer need. This preserves A2 from the
|
||||||
|
completed API review at `68400ff:docs/roadmap/review.md`.
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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,296 +0,0 @@
|
|||||||
# Import Discovery And Facade Simplification Implementation Plan
|
|
||||||
|
|
||||||
Status: complete.
|
|
||||||
Baseline: `bb5540b62e382106d59678e2edb7655acfd5c960`.
|
|
||||||
Audience: a Luna coding agent implementing the stages sequentially.
|
|
||||||
|
|
||||||
## Scope And Execution
|
|
||||||
|
|
||||||
Implement **C03** and **F07** from the
|
|
||||||
[internal-boundary roadmap](internal-boundaries.md). That document owns the target
|
|
||||||
state and preservation requirements; this plan owns concrete private choices,
|
|
||||||
steps, validation, and progress. The preceding 29-finding remediation sprint is
|
|
||||||
complete. Its implementation record remains at
|
|
||||||
`bb5540b:docs/roadmap/implementation.md`; this plan does not reopen it.
|
|
||||||
|
|
||||||
Read `AGENTS.md`, [agent policy](../policy/agents.md), the [development
|
|
||||||
guide](../development.md), and applicable architecture, testing, and documentation
|
|
||||||
policies. Use graph-first discovery and verify actual callers before deleting
|
|
||||||
helpers. Preserve unrelated work.
|
|
||||||
|
|
||||||
This is one end-to-end sprint. Intermediate stages may fail to compile, especially
|
|
||||||
the coordinated engine/facade cutover in Stages 2–3. Do not create adapters,
|
|
||||||
aliases, duplicate paths, or extra abstractions solely to make intermediate
|
|
||||||
stages green. Run focused checks once their affected surface is buildable; record
|
|
||||||
pending checks and finish them before final acceptance. Commit focused nonempty
|
|
||||||
units under repository policy and record stage status, actual validation, and
|
|
||||||
known commit identifiers here. Completion requires the integrated result, not
|
|
||||||
just checked-off stages.
|
|
||||||
|
|
||||||
### Boundaries
|
|
||||||
|
|
||||||
- Preserve public root-owned declarations and signatures, YAML syntax, loading
|
|
||||||
semantics, ordinary/layered differences, callback order, diagnostics, source
|
|
||||||
identity, inspection, confidentiality, and ownership.
|
|
||||||
- Preserve glob literal-versus-discovered rules and directory/glob differences.
|
|
||||||
Keep completed-link reuse, directory failure phases, containment, and existing
|
|
||||||
accounting. This is not a discovery algorithm or permission-policy change.
|
|
||||||
- Keep `resolvedImportPolicy`, registration compilation, normalized request
|
|
||||||
responsibilities, and conversions with real ownership or domain meaning.
|
|
||||||
Simplification is limited to defaults transport and placement construction.
|
|
||||||
- Add no dependencies, caller controls, caches, limits, background work, release
|
|
||||||
gates, or performance promises. Do not publish a release, update consumers, or
|
|
||||||
revise accepted ADR decisions.
|
|
||||||
|
|
||||||
### Selected Ownership Model
|
|
||||||
|
|
||||||
Use the existing engine `ImportDefaults` name and its exported `Required`,
|
|
||||||
`Follow`, `Hidden`, `Recursive`, and `Pattern` fields as the sole engine
|
|
||||||
input-defaults type. Root `ImportDefaults` stays separately defined. Root
|
|
||||||
conversion maps the fields without cloning them; the public loading wrappers
|
|
||||||
immediately enter engine preflight. `loadSettings` takes the per-load snapshot
|
|
||||||
before acquisition or callbacks, as it already does. The normalized request and
|
|
||||||
acquisition policy then borrow that load-owned read-only value.
|
|
||||||
|
|
||||||
Declaration resolution reads the owned defaults and retains its existing copies
|
|
||||||
of selected pattern/exclusion slices and destinations in `resolvedImportPolicy`.
|
|
||||||
This separates presence-aware load input from independently owned effective
|
|
||||||
declaration policy. No public copy-count or thread-safety guarantee follows.
|
|
||||||
|
|
||||||
For placement, expose the engine's existing enum as `PlacementMode uint8` with
|
|
||||||
`PlacementModeMapping` and `PlacementModeSequenceItem`, and expose placement
|
|
||||||
fields on engine input/options records for direct construction. These remain
|
|
||||||
internal-package implementation surfaces. Root types stay root-owned; direct
|
|
||||||
casts preserve unknown values for existing preflight validation.
|
|
||||||
|
|
||||||
### Validation Conventions
|
|
||||||
|
|
||||||
Run from the repository root with an installed compatible Go toolchain and
|
|
||||||
dependencies already available. Use `GOTOOLCHAIN=local`, `GOPROXY=off`, and
|
|
||||||
`GOTELEMETRY=off`. Keep disposable artifacts outside the repository and do not
|
|
||||||
change dependencies for tooling. Focused expressions identify behavior owners;
|
|
||||||
include a newly named regression explicitly if needed. Reuse unchanged successful
|
|
||||||
checks. No timing, allocation, exact syscall, or helper-shape assertions are
|
|
||||||
acceptance criteria.
|
|
||||||
|
|
||||||
## Stage 1 — Extract Patterned Glob Entry Preparation
|
|
||||||
|
|
||||||
Finding: **C03**. Owners: `internal/engine/glob.go`, existing glob selection,
|
|
||||||
link, and directory failure tests. Status: **complete**. Validation passed:
|
|
||||||
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Glob|Directory|Link|Filesystem|Resource'` and `git diff --check`.
|
|
||||||
|
|
||||||
1. Add `prepareGlobEntry` beside the selector. Take the filesystem access,
|
|
||||||
base path, parent `selectedImportCandidate`, `fs.DirEntry`, pattern segment,
|
|
||||||
`last` flag, and `resolvedImportPolicy`. Return a candidate, a boolean meaning
|
|
||||||
selected versus ordinary skip, and a nullable `*Diagnostic`. Use the parent's
|
|
||||||
existing `site`, which is propagated from the selector's root candidate.
|
|
||||||
No candidate-set, accounting, or traversal-state mutation belongs here.
|
|
||||||
2. Move only the patterned branch's per-entry body into this helper. Preserve
|
|
||||||
its order: `path.Match`; match/hidden filtering; lexical/access path creation
|
|
||||||
and containment; `importEntryTarget` and existing diagnostic translation;
|
|
||||||
follow filtering; intermediate-directory eligibility; host/native physical
|
|
||||||
path selection and canonicalization fallback; candidate label/site creation.
|
|
||||||
Return existing safe codes, paths, and sites. Do not turn inspection errors
|
|
||||||
into skips or skip validation merely because exclusion will later match.
|
|
||||||
3. Replace that inline body with the helper call and candidate insertion.
|
|
||||||
Leave pattern validation, `explicitGlobCandidate`, directory enumeration,
|
|
||||||
per-segment union/sorting, final-file validation, exclusions, requiredness,
|
|
||||||
and source-admission timing in the selector.
|
|
||||||
4. Keep directory entry preparation separate. Reuse existing filesystem/link
|
|
||||||
primitives; do not introduce a generic entry strategy or unify differing
|
|
||||||
missing-target, literal-link, filtering, or recursion rules. Remove imports
|
|
||||||
only when genuinely unused after extraction.
|
|
||||||
|
|
||||||
Acceptance: the selector now expresses traversal without embedding preparation
|
|
||||||
details. Reuse `glob_selection_test.go`, `glob_test.go`, filesystem-link tests,
|
|
||||||
and the preceding sprint's directory-phase regressions. They must still cover
|
|
||||||
literal hidden/link components versus wildcard filtering, aliases and native
|
|
||||||
spelling, escaping/cyclic links, matched-directory errors before exclusion,
|
|
||||||
selection order, and bounded accounting. Add a behavioral case only for a gap
|
|
||||||
identified during extraction, not a unit test asserting the new helper exists.
|
|
||||||
|
|
||||||
Focused check:
|
|
||||||
`go test ./internal/engine -run 'Glob|Directory|Link|Filesystem|Resource'`.
|
|
||||||
|
|
||||||
## Stage 2 — Consolidate The Engine Defaults Representation
|
|
||||||
|
|
||||||
Finding: **F07**, engine representation. Owners: `internal/engine/import_spec.go`,
|
|
||||||
`load.go`, `layered.go`, `load_request.go`, `acquisition.go`, `options.go`, and
|
|
||||||
their callers/fixtures. Status: **complete**. Validation passed:
|
|
||||||
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Import|Glob|Directory|SourceVersion'` and `git diff --check`. Root facade callers remain pending Stage 3.
|
|
||||||
|
|
||||||
1. Retain engine `ImportDefaults` with its existing five exported fields.
|
|
||||||
Remove private `importDefaults`, without a compatibility alias. Update
|
|
||||||
engine option/request/acquisition fields, helper signatures, readers, and
|
|
||||||
fixture literals to use the canonical type and exported default-field names.
|
|
||||||
Private containing fields such as `loadRequest.importDefaults` can keep their
|
|
||||||
names. Do not rename the distinct fields of `importDeclarationSpec` or
|
|
||||||
`resolvedImportPolicy` just because they have similar meanings.
|
|
||||||
2. Expose `ImportDefaults ImportDefaults` on engine `Options[T]` and
|
|
||||||
`LayeredOptions[T]`. Remove `OptionsWithImportDefaults`,
|
|
||||||
`LayeredOptionsWithImportDefaults`, and `privateImportDefaults`. Stage 3 changes
|
|
||||||
root callers; a temporary compile failure is preferable to an interim bridge.
|
|
||||||
3. Adapt `snapshotImportDefaults` to the canonical type, retaining nil boolean
|
|
||||||
pointers and nil versus non-nil empty pattern. Keep `cloneBool` and
|
|
||||||
`cloneStrings` where they still have real consumers. Keep current snapshot
|
|
||||||
call sites until Stage 3 establishes their final ownership placement.
|
|
||||||
4. Keep effective declaration policy resolution and validation semantics intact:
|
|
||||||
explicit false beats defaults, each control resolves independently, and
|
|
||||||
selector-specific applicability and depth checks stay unchanged. Do not
|
|
||||||
combine defaults with resolved policy or registration compilation.
|
|
||||||
|
|
||||||
Acceptance: one engine input-defaults type reaches every production path and
|
|
||||||
fixture; no equivalent private record or alias remains. Inspect ordinary,
|
|
||||||
layered, transitive-import, directory/glob, and test-helper call sites. The root
|
|
||||||
cutover and removal of redundant snapshots are explicitly pending until Stage 3.
|
|
||||||
|
|
||||||
Run the import/default focused tests after Stage 3 if the root cutover prevents
|
|
||||||
building the package here; do not report that deferred check as passed.
|
|
||||||
|
|
||||||
## Stage 3 — Simplify Facade Defaults Construction And Snapshot Ownership
|
|
||||||
|
|
||||||
Finding: **F07**, defaults handoff. Owners: root `conversion.go`, `lifecycle.go`,
|
|
||||||
`layered.go`; engine defaults/preflight/coordinator owners; `facade_import_defaults_test.go`
|
|
||||||
and `internal/engine/import_spec_test.go`. Status: **complete**. Validation passed:
|
|
||||||
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Import|Glob|Directory|SourceVersion'`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test . -run 'ImportDefaults|FacadeImport|SourceVersion'`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./...`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test -race ./...`, and `git diff --check`.
|
|
||||||
|
|
||||||
1. Set engine `ImportDefaults` fields directly in root `toEngineOptions` and
|
|
||||||
`toEngineLayeredOptions`. Retain `toEngineImportDefaults` as the explicit
|
|
||||||
root-to-engine type mapping, but map its booleans/pattern without copies.
|
|
||||||
Remove root `cloneImportDefaultBool` and `cloneImportDefaultStrings` after
|
|
||||||
confirming no other caller. Keep public root types and declaration contracts.
|
|
||||||
2. Keep `snapshotImportDefaults` in `loadSettings` as the load-entry ownership
|
|
||||||
operation for both APIs. Remove repeated snapshots in engine `LoadLayered`
|
|
||||||
request construction, `executeLoadRequest` acquisition-policy construction,
|
|
||||||
and `resolveImportPolicy`. Carry the owned defaults read-only thereafter.
|
|
||||||
Review all callers of those helpers, including test setup; direct declaration
|
|
||||||
resolution can read its input because it returns its own concrete policy.
|
|
||||||
3. Keep per-policy copies of the selected pattern, exclusions, and destination.
|
|
||||||
Do not borrow caller-owned storage beyond preflight, mutate the shared owned
|
|
||||||
defaults, or remove copying from unrelated option/record paths. Each new load
|
|
||||||
snapshots its own current inputs; add no reuse cache or ownership flag.
|
|
||||||
4. Replace the private conversion-snapshot fragment in
|
|
||||||
`TestImportDefaultsPreserveEmptyPatternAndRequiredness`, which converts options,
|
|
||||||
mutates inputs, and calls engine `Load`. It should no longer demand a snapshot
|
|
||||||
from a private conversion helper. Keep that test's public nil/empty,
|
|
||||||
requiredness, and inspection-evidence checks.
|
|
||||||
5. Protect the public snapshot contract with a compact ordinary/layered table:
|
|
||||||
use a main document with a version and imports, enable
|
|
||||||
`SourceVersionModeIfPresent`, and use a configuration type with version and
|
|
||||||
imported-value fields. In the main version-check callback, mutate the caller's
|
|
||||||
recursive default from true to false and its existing pattern element from
|
|
||||||
`*.cfg` to `*.skip`. This callback runs after preflight but before imported
|
|
||||||
discovery. Verify the original recursive/pattern selection still supplies a
|
|
||||||
value from a nested imported file. Use existing fixture helpers and no
|
|
||||||
concurrent mutation. A subsequent load with those changed defaults must omit
|
|
||||||
that imported contribution rather than reuse the prior snapshot. Keep fixtures
|
|
||||||
reusable or create fresh readers for each load.
|
|
||||||
6. Reuse or extend the small snapshot/policy tests for all four optional booleans,
|
|
||||||
explicit false, pattern mutation, nil, and non-nil empty pattern. Preserve
|
|
||||||
existing invalid-pattern-before-read/hook tests and declaration override,
|
|
||||||
transitive, underlay, and selected-overlay coverage. Do not assert exact copy
|
|
||||||
counts, pointer identities, or the private conversion boundary as public policy.
|
|
||||||
|
|
||||||
Acceptance: the engine/facade cutover builds; public defaults remain detached
|
|
||||||
before acquisition/callbacks, reused loads remain independent, and only genuine
|
|
||||||
ownership copies remain. Confirm the absence of full defaults snapshots at the
|
|
||||||
three removed downstream sites by source review, not production instrumentation.
|
|
||||||
|
|
||||||
Focused checks:
|
|
||||||
`go test ./internal/engine -run 'Import|Glob|Directory|SourceVersion'` and
|
|
||||||
`go test . -run 'ImportDefaults|FacadeImport|SourceVersion'`.
|
|
||||||
|
|
||||||
## Stage 4 — Construct Placement Directly Across The Facade
|
|
||||||
|
|
||||||
Finding: **F07**, placement handoff. Owners: engine `destination.go`, `load.go`,
|
|
||||||
`layered.go`, normalized input and placement consumers; root `lifecycle.go`,
|
|
||||||
`layered.go`, and placement fixtures. Status: **complete**. Validation passed:
|
|
||||||
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Destination|Placement|Item|SourceVersion'`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test . -run 'Placement|Destination|SourceInfo|SourceVersion|PublicFacadeBoundary|DependencyImportBoundary'`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./...`, and `git diff --check`.
|
|
||||||
|
|
||||||
1. Rename the existing engine enum and its constants to `PlacementMode`,
|
|
||||||
`PlacementModeMapping`, and `PlacementModeSequenceItem`, preserving underlying
|
|
||||||
`uint8` values and zero semantics. Use the same type throughout engine
|
|
||||||
placement parsing, normalization, acquisition, source evidence, and version
|
|
||||||
projection. Do not add a second raw-mode record or type alias.
|
|
||||||
2. Expose engine `Options[T].Destination` and `.PlacementMode`, and
|
|
||||||
`Input.PlacementMode`. Normalized `loadInput` and other private owner fields
|
|
||||||
can retain their names. Populate the normalized input from those fields.
|
|
||||||
Remove `OptionsWithPlacement` and `InputWithPlacement` and migrate all callers,
|
|
||||||
including the direct engine construction in root `records_test.go`.
|
|
||||||
3. In root `toEngineOptions` and `toEngineInput`, set destination using the existing
|
|
||||||
`toEnginePath` conversion and placement using
|
|
||||||
`engine.PlacementMode(options.PlacementMode)` or its input equivalent.
|
|
||||||
Preserve invalid enum values for `normalizeInput` to diagnose before reads.
|
|
||||||
Do not add validation/defaulting to the facade or a new setter/cast function.
|
|
||||||
4. Preserve normalization of literal/parsed destination keys, root-item rejection,
|
|
||||||
wrapper accounting, mounted/item source coordinates, and version projection.
|
|
||||||
Root `Path` is immutable and `toEnginePath` carries its private engine value;
|
|
||||||
keep normalization and actual mutable ownership copies. Do not redesign path
|
|
||||||
conversion or add redundant clones solely to replace the deleted decorator.
|
|
||||||
5. Leave source-result transport such as `SourceInfoPlacementModeForFacade`,
|
|
||||||
source-version conversion, sequence registration preparation, and opaque
|
|
||||||
records alone. They are not input setters targeted by this step. Adapt tests
|
|
||||||
mechanically to new engine construction without weakening public assertions.
|
|
||||||
|
|
||||||
Acceptance: both public loading APIs retain zero/mapping and sequence-item
|
|
||||||
placement, errors before input consumption, final values, source metadata, and
|
|
||||||
history. Reuse the public placement tests plus destination/accounting,
|
|
||||||
item-import, mounted-version, and invalid-option cases. Root signatures still
|
|
||||||
contain no engine types, and engine exports remain below the internal boundary.
|
|
||||||
|
|
||||||
Focused checks:
|
|
||||||
`go test ./internal/engine -run 'Destination|Placement|Item|SourceVersion'` and
|
|
||||||
`go test . -run 'Placement|Destination|SourceInfo|SourceVersion|PublicFacadeBoundary|DependencyImportBoundary'`.
|
|
||||||
|
|
||||||
## Stage 5 — Integrate, Document, And Verify The Result
|
|
||||||
|
|
||||||
Findings: final acceptance for **C03, F07**. Status: **complete**. Validation
|
|
||||||
passed on Linux/amd64 (Go 1.26.10):
|
|
||||||
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off ./scripts/check.sh`,
|
|
||||||
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test -race ./...`, and
|
|
||||||
`git diff --check`. Existing integration coverage was reviewed; no additional
|
|
||||||
case was needed.
|
|
||||||
|
|
||||||
1. Review both paths together: defaults resolving wildcard discovery in ordinary,
|
|
||||||
layered, and transitive imports; placement carrying the resulting sources
|
|
||||||
through composition and inspection; and failures stopping before later hooks.
|
|
||||||
Existing integration tests own these interactions. Add only a concrete missing
|
|
||||||
case rather than a second copy of the complete matrix or a new fixture framework.
|
|
||||||
2. Inspect the resulting handoffs. Confirm the glob selector no longer embeds
|
|
||||||
per-entry preparation, the engine has one defaults input type, direct fields
|
|
||||||
replaced the four input decorators, and the load snapshot is not repeated at
|
|
||||||
each private representation. Keep domain-specific policy/registration and
|
|
||||||
root conversion boundaries, necessary ownership copies, and existing budgets.
|
|
||||||
3. Update `docs/internal/imports.md` for the selector/preparation split and
|
|
||||||
`docs/internal/overview.md` for defaults conversion, preflight snapshot ownership,
|
|
||||||
and direct placement construction. Keep algorithms private and avoid public
|
|
||||||
performance or copy-count guarantees. No ADR/policy change is needed.
|
|
||||||
4. Replace C03/F07 detail in the first two `docs/roadmap/future.md` entries with
|
|
||||||
short pointers to `internal-boundaries.md`. Retain their heading anchors for
|
|
||||||
existing audit links; leave other future candidates unchanged. Correct old
|
|
||||||
roadmap references that would otherwise mistake this replacement plan for
|
|
||||||
the previous sprint's completed record: use
|
|
||||||
`bb5540b:docs/roadmap/implementation.md` for that historical plan. Do not reopen
|
|
||||||
the prior audit or retire unrelated planning records in this sprint.
|
|
||||||
5. Format changed Go files, finish deferred checks, run the final checks below,
|
|
||||||
and inspect the complete diff for API, dependency, semantics, or scope changes.
|
|
||||||
Record actual results and platform limitations here. Commit focused nonempty
|
|
||||||
units and hand off a clean workspace; do not create empty validation commits.
|
|
||||||
|
|
||||||
Final validation, following the existing development guide:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off ./scripts/check.sh
|
|
||||||
env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test -race ./...
|
|
||||||
git diff --check
|
|
||||||
```
|
|
||||||
|
|
||||||
The everyday check includes consumers, documentation links, and public/dependency
|
|
||||||
boundary tests. Confirm those existing tests ran rather than adding redundant
|
|
||||||
final invocations. Report filesystem/platform skips accurately; express macOS
|
|
||||||
validation, CI completion, release publication, extended fuzz campaigns, and
|
|
||||||
benchmarks are not additional acceptance requirements.
|
|
||||||
|
|
||||||
No product or architectural question remains open. The concrete choices above
|
|
||||||
complete the plan without changing the roadmap's public contracts; private
|
|
||||||
formatting and file organization can follow local idiom.
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
# Import Discovery And Facade Boundary Simplification
|
|
||||||
|
|
||||||
Status: target defined; execution is tracked in the [implementation plan](implementation.md).
|
|
||||||
Baseline: `80cf23e7102fca2c5d7c7621e75f930a1ef1ecad`.
|
|
||||||
|
|
||||||
This roadmap brings together the two refactors deferred until after the audit
|
|
||||||
remediation sprint: glob entry preparation (**C03**) and facade-to-engine
|
|
||||||
transport simplification (**F07**). Their original motivation is retained in the
|
|
||||||
[glob](future.md#1-separate-glob-entry-preparation-from-traversal) and
|
|
||||||
[transport](future.md#2-simplify-redundant-facade-to-engine-transport) future-work
|
|
||||||
entries and the [consolidated audit](audit.md).
|
|
||||||
|
|
||||||
The goal is clearer responsibility and ownership with fewer redundant internal
|
|
||||||
representations. Both changes preserve the public API and configuration model.
|
|
||||||
They are independent work areas within one maintenance effort, not a new loading
|
|
||||||
workflow. The [implementation plan](implementation.md) owns concrete edits,
|
|
||||||
stages, validation commands, and execution progress.
|
|
||||||
|
|
||||||
## Baseline And Targeted End State
|
|
||||||
|
|
||||||
The preceding sprint repaired directory-failure classification and shared link
|
|
||||||
dependency evaluation, clarified acquisition handoffs, and removed unused
|
|
||||||
placement bridges. Those changes provide the baseline; this work does not repeat
|
|
||||||
or redesign them.
|
|
||||||
|
|
||||||
| Area | Current baseline | Target |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Glob discovery | `selectGlobImportCandidates` interleaves traversal and set operations with substantial per-entry preparation. | The selector coordinates discovery; a cohesive private operation prepares a wildcard-discovered entry. |
|
|
||||||
| Facade transport | Import defaults pass through root conversion, an exported engine transport record, an equivalent private engine record, and repeated snapshots. Placement construction also uses setter-style bridges. | Root-owned public records convert explicitly to one engine defaults representation, with straightforward engine option/input construction and identifiable ownership copies. |
|
|
||||||
|
|
||||||
The [architecture policy](../policy/architecture.md), [configuration
|
|
||||||
contract](../config.md), and public declarations remain authoritative. [ADR
|
|
||||||
0006](../adr/0006-public-facade.md) continues to govern the facade boundary. These
|
|
||||||
refactors do not require a new architectural decision or changes to its accepted
|
|
||||||
decision. The [component overview](../internal/overview.md) and [import
|
|
||||||
internals](../internal/imports.md) own implemented structure.
|
|
||||||
|
|
||||||
## Glob Entry Preparation
|
|
||||||
|
|
||||||
### Target Responsibility Split
|
|
||||||
|
|
||||||
In [glob.go](../../internal/engine/glob.go), the selector continues to own pattern
|
|
||||||
validation, component advancement, literal-segment handling, directory
|
|
||||||
enumeration and its accounting, candidate union and sorting, final target
|
|
||||||
validation, exclusions, and requiredness.
|
|
||||||
|
|
||||||
A private glob-specific preparation operation owns the patterned branch's work
|
|
||||||
for one directory entry: matching and hidden-entry filtering, lexical/access path
|
|
||||||
construction, containment checks, target inspection and discovered-link policy,
|
|
||||||
intermediate-directory eligibility, host/native access-path selection, labels,
|
|
||||||
and candidate construction. Its result distinguishes a selected candidate, an
|
|
||||||
ordinary skip, and a diagnostic. It receives the facts needed for that entry
|
|
||||||
rather than mutating the selector's candidate set or surrounding traversal state.
|
|
||||||
|
|
||||||
Use the existing filesystem access and link-resolution helpers. Directory
|
|
||||||
selection already has `prepareImportDirectoryEntry`; it remains a separate owner
|
|
||||||
because its recursion, matching, absence handling, and traversal state differ.
|
|
||||||
Share a smaller operation only where it actually expresses the same semantics
|
|
||||||
and reduces duplication. No common visitor or discovery strategy is needed for
|
|
||||||
this extraction.
|
|
||||||
|
|
||||||
### Behavior To Preserve
|
|
||||||
|
|
||||||
Preserve the current distinctions and processing order documented by
|
|
||||||
[explicit imports](../config.md#explicit-imports) and exercised by the selection
|
|
||||||
tests:
|
|
||||||
|
|
||||||
- Literal components remain explicit selections; discovery filters apply at
|
|
||||||
their existing wildcard boundaries. Moving matching or hidden filtering after
|
|
||||||
filesystem inspection would change which errors are observable.
|
|
||||||
- Keep lexical candidate identity separate from resolved access identity.
|
|
||||||
Native source identity and relative imports retain namespace spelling; host
|
|
||||||
access retains its existing canonicalization and containment behavior.
|
|
||||||
- Keep discovered-link follow rules, intermediate-directory eligibility,
|
|
||||||
missing-target handling, and structured diagnostic sites/classifications.
|
|
||||||
Do not make glob and directory rules identical as a side effect of extraction.
|
|
||||||
- Preserve deterministic candidate and error order, union/deduplication,
|
|
||||||
directory-entry accounting, and the existing point of source admission.
|
|
||||||
Final selected-file validation remains before exclusions where it is today;
|
|
||||||
exclusions do not hide discovery or resource errors.
|
|
||||||
- Retain the preceding sprint's distinction between missing-on-open directories
|
|
||||||
and failures after opening, along with operation-local completed-link reuse.
|
|
||||||
|
|
||||||
The intended benefit is that a maintainer can understand traversal separately
|
|
||||||
from entry eligibility and identity preparation. This is a readability change;
|
|
||||||
it does not select a different search algorithm or claim a speedup.
|
|
||||||
|
|
||||||
## Facade-To-Engine Transport
|
|
||||||
|
|
||||||
### Target Representations
|
|
||||||
|
|
||||||
The root keeps its public `ImportDefaults`, `PlacementMode`, `Options`, `Input`,
|
|
||||||
and `LayeredOptions` types. Explicit conversions remain appropriate at this
|
|
||||||
boundary; consumers must not acquire engine type identity, fields, or methods.
|
|
||||||
|
|
||||||
Inside the engine, consolidate the equivalent `ImportDefaults` transport and
|
|
||||||
private `importDefaults` records into one presence-aware input-defaults
|
|
||||||
representation. Carry that representation through the ordinary and layered
|
|
||||||
options, normalized request, and acquisition policy instead of translating the
|
|
||||||
same five fields between equivalent engine records.
|
|
||||||
|
|
||||||
Keep `resolvedImportPolicy` distinct: it represents concrete per-declaration
|
|
||||||
choices, including selector-specific rules, rather than another copy of optional
|
|
||||||
load-wide input. Likewise, registration compilation and the normalized loading
|
|
||||||
request have real responsibilities and are not replacement targets.
|
|
||||||
|
|
||||||
Make placement and defaults straightforward to construct on the existing engine
|
|
||||||
option/input records. Remove decorator-style bridges made unnecessary by that
|
|
||||||
construction surface. Exporting fields or types within `internal/engine` where
|
|
||||||
needed by the root converter does not make them part of ConfigKit's supported
|
|
||||||
public API. Keep explicit enum conversion and preserve unknown values until the
|
|
||||||
existing option validation can report them; conversion must not silently choose
|
|
||||||
a default or move validation into facade setters.
|
|
||||||
|
|
||||||
### Ownership And Copying
|
|
||||||
|
|
||||||
Make the actual mutable ownership boundaries identifiable. Public load calls
|
|
||||||
continue to snapshot pointed-to default booleans and the pattern slice before
|
|
||||||
source acquisition, as specified by the [public
|
|
||||||
declaration](../../lifecycle.go). Preserve omitted, explicit false, nil pattern,
|
|
||||||
and non-nil empty pattern as distinct inputs where they are distinct today.
|
|
||||||
|
|
||||||
Review the entire handoff, not just the duplicate type definitions: root
|
|
||||||
conversion in [conversion.go](../../conversion.go), ordinary construction in
|
|
||||||
[lifecycle.go](../../lifecycle.go), layered construction in
|
|
||||||
[layered.go](../../layered.go), engine options and normalization, acquisition
|
|
||||||
policy construction, and declaration resolution in
|
|
||||||
[import_spec.go](../../internal/engine/import_spec.go).
|
|
||||||
|
|
||||||
Remove snapshots that merely recopy already-owned read-only defaults as they
|
|
||||||
cross those private representations. Keep copies needed when establishing
|
|
||||||
independent mutable ownership, including any independently owned resolved
|
|
||||||
policy. The target is explainable ownership and less redundant work, not an exact
|
|
||||||
copy count or removal of copying throughout the library. Shared defaults within
|
|
||||||
one load can be read-only by implementation discipline; this creates no new
|
|
||||||
caller immutability or concurrency requirement.
|
|
||||||
|
|
||||||
Preserve ordinary and layered option preflight before source reads and application
|
|
||||||
hooks, independent declaration overrides, transitive/underlay propagation,
|
|
||||||
selected-overlay behavior, destination normalization, and invalid-enum
|
|
||||||
diagnostics. Keep callback, source, path, secret, and returned-record conversions
|
|
||||||
whose ownership or domain meaning is not redundant transport.
|
|
||||||
|
|
||||||
The intended benefit is fewer concepts, field mappings, and construction paths
|
|
||||||
to maintain. Reduced copying is useful but does not establish an end-to-end
|
|
||||||
performance guarantee.
|
|
||||||
|
|
||||||
## Documentation Ownership
|
|
||||||
|
|
||||||
Internal documentation describes the resulting discovery and transport
|
|
||||||
boundaries once implemented. Public declarations continue to own the existing
|
|
||||||
snapshot and type contracts; this effort selects no new public behavior.
|
|
||||||
The corresponding C03/F07 future-work entries serve as navigation to this
|
|
||||||
roadmap rather than parallel target or progress inventories. Unrelated future
|
|
||||||
candidates remain separate. The implementation plan owns the documentation
|
|
||||||
updates and retirement of replaced private representations and helpers.
|
|
||||||
|
|
||||||
## Validation And Completion
|
|
||||||
|
|
||||||
Follow the [testing policy](../policy/testing.md) and [development
|
|
||||||
guide](../development.md#baseline-validation). Reuse the existing glob selection,
|
|
||||||
directory/link failure, import-default, placement, public facade, and dependency
|
|
||||||
boundary coverage. Add a focused behavioral case only where a concrete
|
|
||||||
extraction or ownership risk lacks protection.
|
|
||||||
|
|
||||||
For discovery, verify literal versus wildcard filters, host/native identity,
|
|
||||||
contained and escaping links, optional absence versus read/close failures,
|
|
||||||
ordering, exclusions, regular-file checks, and existing resource accounting. For
|
|
||||||
transport, verify nil/empty and explicit-false distinctions, mutation of caller
|
|
||||||
default storage after the load snapshot through a supported callback, invalid
|
|
||||||
options before input consumption, both loading APIs, and transitive/underlay
|
|
||||||
defaults. Existing conversion-focused tests may change with private construction;
|
|
||||||
preserve the useful ownership behavior rather than fossilizing a helper's shape.
|
|
||||||
|
|
||||||
Completion requires an easier-to-follow selector, one engine input-defaults
|
|
||||||
representation, fewer redundant mapping/construction sites, justified remaining
|
|
||||||
copies, and preserved public behavior under the relevant checks. Review source
|
|
||||||
and ownership flow as well as passing tests; moving complexity into a helper or
|
|
||||||
renaming a duplicate record alone is insufficient.
|
|
||||||
|
|
||||||
No benchmark campaign, complexity score, allocation threshold, exact filesystem
|
|
||||||
call trace, new resource limit, or additional release gate is required. This work
|
|
||||||
adds no dependencies, caches, background activity, or new consumer controls, and
|
|
||||||
does not reopen other audit findings or deferred performance experiments. Routine
|
|
||||||
helper signatures, names, and file placement belong to the implementation plan;
|
|
||||||
no significant product or architectural decision remains open for this scope.
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
# Audit Remediation Roadmap
|
|
||||||
|
|
||||||
Status: target accepted; completed execution is recorded in the historical plan
|
|
||||||
at `bb5540b:docs/roadmap/implementation.md`.
|
|
||||||
Prepared: 2026-10-25. Planning baseline: `e1e94ee`.
|
|
||||||
|
|
||||||
This roadmap describes the target state and related work for the 29 entries
|
|
||||||
selected in the [consolidated audit review](audit.md#settled-scope-decisions).
|
|
||||||
It is intended for maintainers reviewing the remediation's scope and architecture.
|
|
||||||
|
|
||||||
## Document Ownership
|
|
||||||
|
|
||||||
- [audit.md](audit.md) owns finding IDs, original evidence, detailed remedies and
|
|
||||||
acceptance cases, and the settled scope decisions. Its evidence has not been
|
|
||||||
independently reproduced by writing this roadmap.
|
|
||||||
- This document owns the combined target state, work areas, and design intent.
|
|
||||||
Finding IDs below provide traceability without repeating the full audit inventory.
|
|
||||||
- The historical implementation plan at
|
|
||||||
`bb5540b:docs/roadmap/implementation.md` records concrete stages, private
|
|
||||||
implementation choices, edits, validation commands, and completion evidence.
|
|
||||||
|
|
||||||
The [configuration contract](../config.md), public Go declarations, and
|
|
||||||
[architecture policy](../policy/architecture.md) remain the owners of behavior
|
|
||||||
and guarantees. Update their applicable implementation documentation when work
|
|
||||||
lands; this planning document does not redefine their contracts.
|
|
||||||
|
|
||||||
## Current Baseline And Targeted End State
|
|
||||||
|
|
||||||
The existing loading model remains the foundation. Public loading APIs delegate
|
|
||||||
to the shared engine lifecycle; acquisition retains source scopes for composition,
|
|
||||||
then precedence, inheritance, decoding, CLI overrides, application resolution,
|
|
||||||
secret lookup, and validation produce a detached result with inspection metadata.
|
|
||||||
The [component overview](../internal/overview.md) describes the current owners.
|
|
||||||
|
|
||||||
The audits identify failures at particular traversal, lifetime, error, and
|
|
||||||
accounting boundaries, alongside redundant representations and repeated work.
|
|
||||||
The target is to repair those boundaries and simplify their implementation while
|
|
||||||
preserving ConfigKit's public API and configuration model.
|
|
||||||
|
|
||||||
| Current gap | Targeted outcome |
|
|
||||||
| --- | --- |
|
|
||||||
| Recursive descriptors can fail to terminate, failed dynamic builds can leave partial state, and slice aliases can be mistaken for cycles. | Traversal terminates for the reported cases, failed construction leaves reusable state valid, and supported acyclic values remain accepted. |
|
|
||||||
| Callback and owned-root cleanup can be skipped during panic unwinding; directory errors lose their failure phase. | Cleanup follows the lifetime of each owned resource, and selected-directory failures retain the information needed for correct error handling. |
|
|
||||||
| Finite symlink dependencies can be expanded repeatedly. | Completed dependencies are reused within a resolution while active cycles remain detectable. |
|
|
||||||
| Projection reservations and published history can escape existing accounting. | Existing budgets govern the relevant allocations and complete retained document graph. |
|
|
||||||
| Collection-policy work repeatedly scans or copies unrelated state. | Policy preparation, application, and retained evidence have clear ownership, with work localized to affected policy. |
|
|
||||||
| Acquisition has redundant paths and difficult handoffs; several private representations have no production consumer. | Production paths and their tests share the active protocol, with cohesive handoffs and unused representations removed. |
|
|
||||||
| Some lookups repeat full scans, and some tests assert incidental representation or miss meaningful mutations. | Local indexing avoids the identified repeated work, and tests protect observable contracts at useful boundaries. |
|
|
||||||
|
|
||||||
This is a bounded remediation effort, not a redesign of configuration semantics.
|
|
||||||
Correcting erroneous acceptance, rejection, cleanup, or diagnostics in the
|
|
||||||
reported cases restores existing contracts. It does not select new loading APIs,
|
|
||||||
configuration options, limits, background work, or persistent caches.
|
|
||||||
|
|
||||||
## Selected Work
|
|
||||||
|
|
||||||
The following areas account for all 29 selected entries. Detailed examples and
|
|
||||||
acceptance cases remain with their linked audit group.
|
|
||||||
|
|
||||||
### 1. Safe Typed-Value Traversal And Descriptor Reuse
|
|
||||||
|
|
||||||
Selected entries: **A01, A02, A03, A04, A05** in
|
|
||||||
[schema and typed values](audit.md#a-schema-and-typed-values).
|
|
||||||
|
|
||||||
Make pointer-only descriptor traversal terminate in decoding and path resolution,
|
|
||||||
while preserving supported null values and ordinary recursive structures. Make
|
|
||||||
dynamic descriptor construction transactional: a failed build must not leave
|
|
||||||
partial descriptors available to subsequent operations. Staging successful
|
|
||||||
additions or rolling back the complete failed addition set are implementation
|
|
||||||
choices; both must handle recursive references and preserve prior valid entries.
|
|
||||||
|
|
||||||
Correct active-path cycle detection to distinguish finite slice views sharing
|
|
||||||
storage from actual cycles. Keep detachment and nil/empty behavior intact across
|
|
||||||
the callers that share this traversal machinery.
|
|
||||||
|
|
||||||
Enforce existing projection budgets before large container reservations. Include
|
|
||||||
map-key materialization and overflow-safe lower-bound checks in the review; a
|
|
||||||
check after the reservation does not address the reported failure. This work
|
|
||||||
does not introduce a general heap limit or reject large containers independently
|
|
||||||
of existing budgets.
|
|
||||||
|
|
||||||
Dynamic-type eligibility reuse depends on safe descriptor construction and retains
|
|
||||||
completed successful checks within the value operation. Keep failure diagnostics
|
|
||||||
specific to their current path and distinguish completed results from traversal visits.
|
|
||||||
This removes repeated descriptor-graph walks without a cross-load cache.
|
|
||||||
|
|
||||||
### 2. Callback, Filesystem, And Dependency Lifetimes
|
|
||||||
|
|
||||||
Selected entries: **B01, B02** in [callback lifetime](audit.md#b-callback-and-filesystem-lifetime)
|
|
||||||
and **C01, C02** in [filesystem traversal](audit.md#c-import-selection-and-link-resolution).
|
|
||||||
|
|
||||||
Tie callback-context deactivation to the callback invocation with deferred cleanup
|
|
||||||
so it runs on normal return and panic unwinding. Independently give the owned
|
|
||||||
import root a deferred cleanup fallback. Preserve normal close-error handling,
|
|
||||||
caller ownership, and propagation of the original callback panic. Fixing either
|
|
||||||
state lifetime or handle lifetime does not fix the other.
|
|
||||||
|
|
||||||
Preserve the phase of a directory failure. A genuinely absent directory before
|
|
||||||
successful opening remains subject to existing permissiveness rules; a later
|
|
||||||
read or close failure must not be treated as absence. Apply this distinction to
|
|
||||||
both supported directory-reading paths. Strengthen the shared filesystem tests
|
|
||||||
to check each handle's closure and structured, non-disclosing diagnostics,
|
|
||||||
including partial reads and failures masked by another successful directory.
|
|
||||||
Do not change regular-file close-error policy as a side effect.
|
|
||||||
|
|
||||||
Avoid repeatedly expanding completed finite symlink dependencies in both host
|
|
||||||
and native filesystem resolution. Use operation-local memoization of completed
|
|
||||||
targets, or an equivalent dependency evaluation, with keys that respect the
|
|
||||||
resolution namespace. Keep active-path cycle detection separate from completed
|
|
||||||
work. Preserve containment, supported finite reuse, and existing host-path
|
|
||||||
semantics. The fix must work for directory and glob selection without a new
|
|
||||||
arbitrary traversal cap or changed symlink defaults.
|
|
||||||
|
|
||||||
### 3. Retained History And Combination Evidence
|
|
||||||
|
|
||||||
Selected entries: **D01, D02, D03, D04** in
|
|
||||||
[history and retained state](audit.md#d-retained-history-and-unused-evidence).
|
|
||||||
|
|
||||||
Remove the unconsumed override-result pre-CLI copy and the unqueried published
|
|
||||||
structural-input copy. Preserve the separate pre-CLI state used by active replay
|
|
||||||
and the structural input still used before publication for metadata and secret
|
|
||||||
classification. Remove unused combination-evidence members and the work needed
|
|
||||||
solely to produce them, retaining projections, mappings, origins, and snapshots
|
|
||||||
that have active consumers.
|
|
||||||
|
|
||||||
With retained roots clarified, enforce the existing history allowance against
|
|
||||||
the complete graph published through inspection, including replay and inheritance
|
|
||||||
state and publication-created copies. Count shared nodes once by pointer identity;
|
|
||||||
distinct retained copies still consume the allowance. Account for required copies
|
|
||||||
before creating them where practical. Removing an unused tree alone does not
|
|
||||||
resolve the accounting omissions.
|
|
||||||
|
|
||||||
Keep required before/after evidence and existing inspection behavior. Validate
|
|
||||||
the complete publication path with small controlled allowances, including the
|
|
||||||
interaction of CLI, resolution replay, and inheritance, rather than relying only
|
|
||||||
on tests of an isolated counting helper.
|
|
||||||
|
|
||||||
### 4. Collection-Policy Preparation And Application
|
|
||||||
|
|
||||||
Selected entries: **E01, E02, E03** in
|
|
||||||
[collection policy and projection](audit.md#e-collection-processing-and-efficiency).
|
|
||||||
|
|
||||||
Distinguish the existing policy state used for lookup, changes introduced by an
|
|
||||||
incoming tier, and evidence retained by the assembly. Prepare incoming claims and
|
|
||||||
declarations against read-only lower state, then apply the owned changes at the
|
|
||||||
appropriate combination boundary. Avoid copying every unrelated policy for
|
|
||||||
small tiers and scanning all policies for each independent replacement.
|
|
||||||
|
|
||||||
Use a semantic-path index, localized updates, or equivalent bounded lookup to
|
|
||||||
find affected policy. Preserve retirement timing, policy established by the
|
|
||||||
incoming contribution, pending declarations, persistent explicit modes, and
|
|
||||||
nested record ownership. Copying remains appropriate wherever mutable ownership
|
|
||||||
requires it. Removing only one duplicate clone or shortening a function does not
|
|
||||||
address the identified growth patterns.
|
|
||||||
|
|
||||||
Clarify preparation, evidence application, and finalization responsibilities.
|
|
||||||
Share straightforward history-descriptor translation where useful; evaluate
|
|
||||||
shared record-selection logic while keeping overlay and inheritance origin and
|
|
||||||
mode lifetimes distinct. Retain the common sequence-combination owner. This work
|
|
||||||
does not call for a generic merging framework.
|
|
||||||
|
|
||||||
### 5. Active Acquisition Handoffs And Private Cleanup
|
|
||||||
|
|
||||||
Selected entries: **F01, F02, F03, F04, F05, F06** in
|
|
||||||
[acquisition, cleanup, and test quality](audit.md#f-remaining-maintenance-and-test-improvements).
|
|
||||||
|
|
||||||
Remove the test-only eager acquisition mode so acquisition retains scopes through
|
|
||||||
the active production protocol. Tests that need composition should invoke the
|
|
||||||
actual composition boundary explicitly. Retire the parallel test-only import
|
|
||||||
parser and route relevant fixtures through the active directive parser or the
|
|
||||||
appropriate declaration boundary, retaining malformed-input coverage.
|
|
||||||
|
|
||||||
Clarify active acquisition and coordinator handoffs around cohesive tasks:
|
|
||||||
parsed-input directive/version handling, standalone placement, and repeated tier
|
|
||||||
application/projection. Use explicit inputs and results consistent with existing
|
|
||||||
state owners. Extracting helpers that continue to share surrounding mutable state
|
|
||||||
does not establish a clearer handoff. Preserve ordinary/layered differences,
|
|
||||||
overlay and diff behavior, source identity, callback timing, diagnostics, and
|
|
||||||
accounting scopes.
|
|
||||||
|
|
||||||
Remove the disconnected root record-clone family, uncalled engine helpers, and
|
|
||||||
unread private transport/result members identified in the audit. Reconfirm
|
|
||||||
references on the implementation baseline, including function values and relevant
|
|
||||||
build configurations. Preserve live conversion paths and validation side effects;
|
|
||||||
the audit's absence of field reads is not a general rule for deleting state.
|
|
||||||
Replace meaningful ownership coverage through the public boundary before removing
|
|
||||||
tests that only exercise disconnected helpers.
|
|
||||||
|
|
||||||
### 6. Local Lookup Improvements
|
|
||||||
|
|
||||||
Selected entries: **E04, E05** in
|
|
||||||
[collection policy and projection](audit.md#e-collection-processing-and-efficiency)
|
|
||||||
and **F08** in [secret-failure lookup](audit.md#f08-index-secret-failure-location-queries-lazily).
|
|
||||||
|
|
||||||
Reuse a local document lookup or equivalent traversal for tier-history projection
|
|
||||||
instead of rescanning wide mappings for each sequence. Preserve authored
|
|
||||||
coordinates, sparse projections, and evidence ownership.
|
|
||||||
|
|
||||||
Restore compound identity fields using indexed mapping positions and membership
|
|
||||||
checks rather than rescanning child and merged mappings for every field. Preserve
|
|
||||||
child-authored values and locations, mapping order, and detached nodes.
|
|
||||||
|
|
||||||
Build secret-failure location lookup state lazily when failures need it. Preserve
|
|
||||||
all diagnostics, nearest-location fallbacks, and confidentiality; successful
|
|
||||||
loads should not incur an unnecessary index merely to support failure reporting.
|
|
||||||
These changes address specific repeated work and do not promise a measured
|
|
||||||
startup improvement.
|
|
||||||
|
|
||||||
### 7. Tests That Protect The Intended Behavior
|
|
||||||
|
|
||||||
Selected entries: **F09, F10, F11, F12** in
|
|
||||||
[acquisition, cleanup, and test quality](audit.md#f-remaining-maintenance-and-test-improvements),
|
|
||||||
in addition to the regression coverage attached to each reported defect.
|
|
||||||
|
|
||||||
Cover historical-path enumeration through the public API, including removed
|
|
||||||
paths, queryability, ordering, and detached returned slices. Use those assertions
|
|
||||||
to replace private representation checks where they address the same risk.
|
|
||||||
|
|
||||||
Strengthen fuzz detachment checks by mutating existing descendants or in-length
|
|
||||||
elements, so spare capacity cannot hide aliases. Replace exact filesystem stat
|
|
||||||
call sequences with observable candidate ordering and meaningful failure
|
|
||||||
selection, retaining relevant assertions such as not opening excluded inputs.
|
|
||||||
|
|
||||||
Consolidate facade scaffolding around public construction, loading, errors,
|
|
||||||
ownership, and lifecycle behavior. Preserve distinct integration risks and
|
|
||||||
reader-byte snapshot coverage. Existing coverage percentages or a desired test
|
|
||||||
count are not acceptance criteria for this work.
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
The historical implementation plan at `bb5540b:docs/roadmap/implementation.md`
|
|
||||||
defines the sequential, Luna-scoped stages and their dependencies, including
|
|
||||||
coordinated changes to descriptor state, history ownership/accounting, collection
|
|
||||||
policy, acquisition, and replacement public coverage. It also records validation
|
|
||||||
commands and the complete finding-to-stage map. The audit remains the owner of
|
|
||||||
original evidence and detailed acceptance cases.
|
|
||||||
|
|
||||||
## Validation And Completion
|
|
||||||
|
|
||||||
Validation follows the audit's detailed acceptance cases and the
|
|
||||||
[testing policy](../policy/testing.md). For fatal or hanging regressions, use bounded
|
|
||||||
subprocesses; for budget cases, use small controlled limits. For symlink expansion
|
|
||||||
and repeated lookup work, prefer bounded operation evidence and algorithm review
|
|
||||||
over timing thresholds. Use real filesystem fixtures where filesystem semantics
|
|
||||||
matter and narrow fault injection for read/close failures. State platform limits.
|
|
||||||
|
|
||||||
Applicable development checks come from the [development guide](../development.md),
|
|
||||||
including appropriate race coverage for changed ownership and lifetime boundaries.
|
|
||||||
The historical implementation plan owns their execution. Existing audit results
|
|
||||||
are baseline evidence, not validation of the eventual changes. No performance
|
|
||||||
benchmark campaign is required by this roadmap, and no new benchmark results are
|
|
||||||
claimed.
|
|
||||||
|
|
||||||
Completion means all 29 selected findings have implementation and acceptance
|
|
||||||
evidence, the affected public behavior remains consistent with its contracts,
|
|
||||||
and canonical internal documentation describes the resulting owners. The
|
|
||||||
historical implementation plan records execution status without duplicate
|
|
||||||
progress inventories. Retire completed planning records under the normal
|
|
||||||
documentation lifecycle.
|
|
||||||
|
|
||||||
## Scope Boundaries And Decision Status
|
|
||||||
|
|
||||||
All three audit scope questions are settled. No significant architectural or
|
|
||||||
product decision remains open for this roadmap. The historical implementation
|
|
||||||
plan records private design choices and sequencing, subject to the acceptance
|
|
||||||
cases above.
|
|
||||||
If that work exposes a need to change a public contract, bring the specific
|
|
||||||
decision back for review rather than treating it as an implicit part of remediation.
|
|
||||||
|
|
||||||
The glob preparation refactor (C03) and wider facade transport simplification
|
|
||||||
(F07) have since been selected in the
|
|
||||||
[internal-boundary roadmap](internal-boundaries.md). Other deferred features,
|
|
||||||
clone-traversal redesign, and broader performance experiments remain outside
|
|
||||||
this effort. Keep
|
|
||||||
existing ownership boundaries and loading APIs; no ADR or architecture-policy
|
|
||||||
change is proposed by this roadmap.
|
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExampleLoad() {
|
func ExampleLoad() {
|
||||||
@@ -186,7 +186,8 @@ func ExampleOverride() {
|
|||||||
},
|
},
|
||||||
Overrides: overrides,
|
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>
|
// Output: 0 --workers <nil>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Artifact is a Narratio-shaped generated output declaration.
|
// Artifact is a Narratio-shaped generated output declaration.
|
||||||
@@ -167,7 +167,7 @@ func LoadNotariusPaths(path, invocationDir string, overrides ...configkit.Overri
|
|||||||
resolveNotariusReference(configuration, resolution, invocationDir)
|
resolveNotariusReference(configuration, resolution, invocationDir)
|
||||||
return nil
|
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) {
|
if configuration.Reference == "" || filepath.IsAbs(configuration.Reference) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info, found := resolution.InputInfo(path)
|
info, found := resolution.EntryInfo(path)
|
||||||
if !found {
|
if !found {
|
||||||
resolution.Warning(path, "relative reference has no entry provenance")
|
resolution.Warning(path, "relative reference has no entry provenance")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
"go.maximumdirect.net/configkit/examples/consumers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Example_narratio() {
|
func Example_narratio() {
|
||||||
@@ -82,7 +82,7 @@ func Example_weatherStartup() {
|
|||||||
defer os.RemoveAll(directory)
|
defer os.RemoveAll(directory)
|
||||||
_ = os.WriteFile(filepath.Join(directory, blockedName), []byte("unused-example-value\n"), 0o600)
|
_ = os.WriteFile(filepath.Join(directory, blockedName), []byte("unused-example-value\n"), 0o600)
|
||||||
_ = os.WriteFile(filepath.Join(directory, tokenName), []byte("disposable-example-credential\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
|
var reportOutput bytes.Buffer
|
||||||
_, reportWriteErr := report.WriteTo(&reportOutput)
|
_, reportWriteErr := report.WriteTo(&reportOutput)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CollectionBundleConfig is a representative application-owned collection
|
// CollectionBundleConfig is a representative application-owned collection
|
||||||
@@ -86,7 +86,7 @@ func resolveCollectionResources(configuration *CollectionBundleConfig, resolutio
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
contentPath := configkit.Keys("collections", collectionID, "prompts", promptID, "content_file")
|
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 {
|
if !found || len(info.Origin.Locations) != 1 {
|
||||||
return fmt.Errorf("prompt resource has no single declaring source")
|
return fmt.Errorf("prompt resource has no single declaring source")
|
||||||
}
|
}
|
||||||
@@ -104,9 +104,9 @@ func resolveCollectionResources(configuration *CollectionBundleConfig, resolutio
|
|||||||
}
|
}
|
||||||
prompt.ResourceText = string(data)
|
prompt.ResourceText = string(data)
|
||||||
collection.Prompts[promptID] = prompt
|
collection.Prompts[promptID] = prompt
|
||||||
resolution.Record(
|
resolution.RecordNormalization(
|
||||||
configkit.Keys("collections", collectionID, "prompts", promptID, "resource_text"),
|
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
|
configuration.Collections[collectionID] = collection
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
"go.maximumdirect.net/configkit/examples/consumers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Example_inputDestination() {
|
func Example_inputDestination() {
|
||||||
@@ -60,7 +60,7 @@ func TestCollectionBundleAssemblesMountedPeersAndResources(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
versionInfo, ok := result.Metadata().Lookup(configkit.Keys("version"))
|
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)
|
t.Fatalf("main-owned version metadata = %#v, %v", versionInfo, ok)
|
||||||
}
|
}
|
||||||
collectionVersionPath := configkit.Keys("collections", "weatherreporter", "version")
|
collectionVersionPath := configkit.Keys("collections", "weatherreporter", "version")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileComparisonConfig is a small application-owned shape for comparing
|
// 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)
|
url := fmt.Sprintf("https://%s:%d", endpoint.Host, endpoint.Port)
|
||||||
if endpoint.URL != url {
|
if endpoint.URL != url {
|
||||||
endpoint.URL = url
|
endpoint.URL = url
|
||||||
resolution.Record(configkit.Keys("service", "endpoint", "url"),
|
resolution.RecordNormalization(configkit.Keys("service", "endpoint", "url"),
|
||||||
configkit.ChangeNormalization, "derived endpoint URL",
|
"derived endpoint URL",
|
||||||
configkit.Keys("service", "endpoint", "host"),
|
configkit.Keys("service", "endpoint", "host"),
|
||||||
configkit.Keys("service", "endpoint", "port"))
|
configkit.Keys("service", "endpoint", "port"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
"go.maximumdirect.net/configkit/examples/consumers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExampleCompareFiles() {
|
func ExampleCompareFiles() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type workingDirectoryPresetConfig struct {
|
type workingDirectoryPresetConfig struct {
|
||||||
@@ -40,6 +40,7 @@ func Example_workingDirectoryPreset() {
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
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
|
// Output: example service.yml
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package consumers
|
package consumers
|
||||||
|
|
||||||
import configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
import configkit "go.maximumdirect.net/configkit"
|
||||||
|
|
||||||
// PresetCatalog is ordinary application data; no inheritance fields or
|
// PresetCatalog is ordinary application data; no inheritance fields or
|
||||||
// registration hooks are required in the typed configuration.
|
// registration hooks are required in the typed configuration.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package consumers_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
"go.maximumdirect.net/configkit/examples/consumers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPresetCatalogOneLoadAndDerivation(t *testing.T) {
|
func TestPresetCatalogOneLoadAndDerivation(t *testing.T) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"path"
|
"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
|
//go:embed testdata/native-source testdata/layered-bundle testdata/collection-bundle testdata/ordinary-imports testdata/source-version
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
"go.maximumdirect.net/configkit/examples/consumers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Example_nativeFilesystem() {
|
func Example_nativeFilesystem() {
|
||||||
@@ -26,7 +26,7 @@ func Example_rootRelativeImports() {
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
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)
|
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>
|
// Output: Root import example 1.0 A root-loaded prompt. /prompts <nil>
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ func Example_sourceVersionAdoption() {
|
|||||||
}
|
}
|
||||||
checks, redacted := 0, true
|
checks, redacted := 0, true
|
||||||
for _, source := range result.Sources() {
|
for _, source := range result.Sources() {
|
||||||
if check := source.VersionCheck; check != nil {
|
if check := source.VersionEvidence; check != nil {
|
||||||
checks++
|
checks++
|
||||||
redacted = redacted && check.Redacted && check.Version.Text == ""
|
redacted = redacted && check.Redacted && check.Version.Text == ""
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ func TestVersionedServiceModesAndMountedProtection(t *testing.T) {
|
|||||||
t.Fatalf("versioned service sources = %#v", sources)
|
t.Fatalf("versioned service sources = %#v", sources)
|
||||||
}
|
}
|
||||||
for _, source := range sources {
|
for _, source := range sources {
|
||||||
check := source.VersionCheck
|
check := source.VersionEvidence
|
||||||
if check == nil || check.Path.String() != "/version" || !check.Version.Present ||
|
if check == nil || check.Path.String() != "/version" || !check.Version.Present ||
|
||||||
!check.Redacted || check.Version.Text != "" {
|
!check.Redacted || check.Version.Text != "" {
|
||||||
t.Fatalf("safe source version evidence = %#v", sources)
|
t.Fatalf("safe source version evidence = %#v", sources)
|
||||||
@@ -119,7 +119,7 @@ func TestCatalogRetainsPresenceAndSourceContext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, key := range []string{"enabled", "workers"} {
|
for _, key := range []string{"enabled", "workers"} {
|
||||||
info, found := result.Metadata().Lookup(configkit.Keys(key))
|
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)
|
t.Fatalf("%s presence = %#v, %v", key, info, found)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package consumers
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PromptCatalogConfig is an application-owned prompt collection shape.
|
// PromptCatalogConfig is an application-owned prompt collection shape.
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
"go.maximumdirect.net/configkit/examples/consumers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Example_identifiedPromptCatalog() {
|
func Example_identifiedPromptCatalog() {
|
||||||
@@ -24,8 +24,8 @@ func Example_identifiedPromptCatalog() {
|
|||||||
}
|
}
|
||||||
emptySearches := 0
|
emptySearches := 0
|
||||||
for _, source := range result.Sources() {
|
for _, source := range result.Sources() {
|
||||||
for _, selection := range source.ImportSelections {
|
for _, selection := range source.EmptyImportSelections {
|
||||||
if selection.Selector == configkit.ImportSelectorDirectory && selection.Outcome == configkit.ImportSelectionNone {
|
if selection.Selector == configkit.ImportSelectorDirectory {
|
||||||
emptySearches++
|
emptySearches++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SequenceCatalogConfig is a source-backed prompt catalog with an application
|
// 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))
|
filesystem := configkit.NewFilesystem("sequence catalog", os.DirFS(root))
|
||||||
filesystemContext := &configkit.FilesystemContext{BaseDir: ".", Root: ".", FS: &filesystem}
|
filesystemContext := &configkit.FilesystemContext{BaseDir: ".", Root: ".", FS: &filesystem}
|
||||||
mainSearch := configkit.SearchConfig("", configkit.Searches(
|
mainSearch := configkit.SearchConfig("", configkit.Searches(
|
||||||
configkit.Paths(absoluteMain),
|
configkit.SearchPaths(absoluteMain),
|
||||||
configkit.SearchDirectory(filepath.Join(root, "fallback"), configkit.DirectorySearchOptions{
|
configkit.SearchDirectory(filepath.Join(root, "fallback"), configkit.DirectorySearchOptions{
|
||||||
Filenames: []configkit.FilenameGroup{configkit.CustomFilenames(filepath.Base(absoluteMain))},
|
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 {
|
Resolve: func(configuration *SequenceCatalogConfig, resolution *configkit.Resolution) error {
|
||||||
configuration.ResolvedLabel = "for:" + configuration.RunLabel
|
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"))
|
"derive the resolved label from the selected run label", configkit.Keys("run_label"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
"go.maximumdirect.net/configkit/examples/consumers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Example_sequenceCatalog() {
|
func Example_sequenceCatalog() {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VersionedServiceConfig demonstrates a mounted main input whose source-local
|
// VersionedServiceConfig demonstrates a mounted main input whose source-local
|
||||||
@@ -47,7 +47,7 @@ func LoadVersionedService(mode configkit.SourceVersionMode, overrides ...configk
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Secret: func(path configkit.Path) bool {
|
Sensitive: func(path configkit.Path) bool {
|
||||||
return path.String() == "/service/version"
|
return path.String() == "/service/version"
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -23,7 +23,7 @@ var (
|
|||||||
|
|
||||||
func TestFacadeValuesRemainRootOwned(t *testing.T) {
|
func TestFacadeValuesRemainRootOwned(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
const packagePath = "gitea.maximumdirect.net/eric/ConfigKit"
|
const packagePath = "go.maximumdirect.net/configkit"
|
||||||
for _, candidate := range []reflect.Type{
|
for _, candidate := range []reflect.Type{
|
||||||
reflect.TypeFor[configkit.Path](), reflect.TypeFor[configkit.Source](),
|
reflect.TypeFor[configkit.Path](), reflect.TypeFor[configkit.Source](),
|
||||||
reflect.TypeFor[configkit.Secret](), reflect.TypeFor[configkit.Diagnostic](),
|
reflect.TypeFor[configkit.Secret](), reflect.TypeFor[configkit.Diagnostic](),
|
||||||
@@ -140,7 +140,7 @@ func TestFacadeSecretsBootstrapUsesFacadeRecords(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
report, err := configkit.LoadSecretsDir(directory)
|
report, err := configkit.LoadSecretsIntoEnvironment(directory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ func TestFacadeSecretsBootstrapUsesFacadeRecords(t *testing.T) {
|
|||||||
t.Fatalf("report rendering = %q, %v", rendered.String(), err)
|
t.Fatalf("report rendering = %q, %v", rendered.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
failedReport, err := configkit.LoadSecretsDir("")
|
failedReport, err := configkit.LoadSecretsIntoEnvironment("")
|
||||||
var secretsError *configkit.SecretsError
|
var secretsError *configkit.SecretsError
|
||||||
if failedReport == nil || !errors.As(err, &secretsError) || len(secretsError.Diagnostics()) != 1 || secretsError.Diagnostics()[0].Code != configkit.CodeSecretsInvalidPath {
|
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)
|
t.Fatalf("bootstrap failure = %#v / %#v", failedReport, err)
|
||||||
@@ -203,7 +203,7 @@ func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
|
|||||||
if !resolution.IsExplicit(configkit.Keys("input")) {
|
if !resolution.IsExplicit(configkit.Keys("input")) {
|
||||||
resolution.Error(configkit.Keys("input"), "input is required")
|
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 {
|
if !ok || !input.Explicit {
|
||||||
resolution.Error(configkit.Keys("input"), "missing input provenance")
|
resolution.Error(configkit.Keys("input"), "missing input provenance")
|
||||||
}
|
}
|
||||||
@@ -245,6 +245,8 @@ func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, context := range []func(){
|
for _, context := range []func(){
|
||||||
func() { retainedResolution.IsExplicit(configkit.Keys("input")) },
|
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() { retainedValidation.Warning(configkit.Keys("input"), "too late") },
|
||||||
} {
|
} {
|
||||||
func() {
|
func() {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
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 (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFacadeInactiveContextsPreserveClosedFailure(t *testing.T) {
|
func TestFacadeInactiveContextsPreserveClosedFailure(t *testing.T) {
|
||||||
@@ -24,10 +24,11 @@ func TestFacadeInactiveContextsPreserveClosedFailure(t *testing.T) {
|
|||||||
call func()
|
call func()
|
||||||
}{
|
}{
|
||||||
{"IsExplicit", func() { state.resolution.IsExplicit(path) }},
|
{"IsExplicit", func() { state.resolution.IsExplicit(path) }},
|
||||||
{"InputInfo", func() { state.resolution.InputInfo(path) }},
|
{"EntryInfo", func() { state.resolution.EntryInfo(path) }},
|
||||||
{"Source", func() { state.resolution.Source("source") }},
|
{"Source", func() { state.resolution.Source("source") }},
|
||||||
{"SourceContext", func() { state.resolution.SourceContext("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.Error", func() { state.resolution.Error(path, "message") }},
|
||||||
{"Resolution.Warning", func() { state.resolution.Warning(path, "message") }},
|
{"Resolution.Warning", func() { state.resolution.Warning(path, "message") }},
|
||||||
{"Default", func() { configkit.Default(state.resolution, path, &target, 1, "reason") }},
|
{"Default", func() { configkit.Default(state.resolution, path, &target, 1, "reason") }},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFacadeInputDestinationsAcrossSourceKinds(t *testing.T) {
|
func TestFacadeInputDestinationsAcrossSourceKinds(t *testing.T) {
|
||||||
@@ -352,7 +352,7 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
|
|||||||
Resolve: func(configuration *map[string]any, resolution *configkit.Resolution) error {
|
Resolve: func(configuration *map[string]any, resolution *configkit.Resolution) error {
|
||||||
callbacks.resolve++
|
callbacks.resolve++
|
||||||
resourcePath := configkit.Keys("catalog", "entries", "item", "resource")
|
resourcePath := configkit.Keys("catalog", "entries", "item", "resource")
|
||||||
input, ok := resolution.InputInfo(resourcePath)
|
input, ok := resolution.EntryInfo(resourcePath)
|
||||||
if !ok || len(input.Origin.Locations) != 1 {
|
if !ok || len(input.Origin.Locations) != 1 {
|
||||||
return fmt.Errorf("mounted resource input has no single source origin")
|
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 := facadeDestinationMap(t, facadeDestinationMap(t, facadeDestinationMap(t, *configuration, "catalog"), "entries"), "item")
|
||||||
entry["resource_text"] = string(data)
|
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
|
return nil
|
||||||
},
|
},
|
||||||
Validate: func(*map[string]any, *configkit.Validation) { callbacks.validate++ },
|
Validate: func(*map[string]any, *configkit.Validation) { callbacks.validate++ },
|
||||||
Secret: func(candidate configkit.Path) bool { return candidate.String() == tokenPath.String() },
|
Sensitive: func(candidate configkit.Path) bool { return candidate.String() == tokenPath.String() },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -405,7 +405,7 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
|
|||||||
if err != nil || string(data) != "source-relative summary" {
|
if err != nil || string(data) != "source-relative summary" {
|
||||||
t.Fatalf("completed relative resource = %q, %v", data, err)
|
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)
|
t.Fatalf("mounted title metadata = %#v, %v", info, ok)
|
||||||
}
|
}
|
||||||
if history := result.Metadata().History(titlePath); len(history) < 2 {
|
if history := result.Metadata().History(titlePath); len(history) < 2 {
|
||||||
|
|||||||
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(`configkit:
|
||||||
|
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{
|
||||||
|
"configkit": 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"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type facadeDiffConfig struct {
|
type facadeDiffConfig struct {
|
||||||
@@ -29,10 +29,10 @@ func TestFacadeDiffKeepsPublicSequenceChangesWithHistoricalDescendants(t *testin
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
result, err := configkit.Load(configkit.Bytes("historical-sequence.yml", []byte("items: [{secret: disposable-canary}, "+public+"]\n")),
|
result, err := configkit.Load(configkit.Bytes("historical-sequence.yml", []byte("items: [{secret: disposable-canary}, "+public+"]\n")),
|
||||||
configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
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 {
|
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
|
||||||
current.Items[0] = "public scalar"
|
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
|
return nil
|
||||||
},
|
},
|
||||||
}})
|
}})
|
||||||
@@ -71,10 +71,10 @@ func TestFacadeDiffKeepsPublicScalarAcrossHistoricalAbsence(t *testing.T) {
|
|||||||
Dynamic map[string]any `yaml:"dynamic"`
|
Dynamic map[string]any `yaml:"dynamic"`
|
||||||
}
|
}
|
||||||
options := configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
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 {
|
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
|
||||||
delete(current.Dynamic, "entry")
|
delete(current.Dynamic, "entry")
|
||||||
resolution.Record(configkit.Keys("dynamic", "entry"), configkit.ChangeNormalization, "remove entry")
|
resolution.RecordNormalization(configkit.Keys("dynamic", "entry"), "remove entry")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -221,7 +221,7 @@ func TestFacadeDiffIgnoresSourceAndResolutionHistory(t *testing.T) {
|
|||||||
resolve := func(value string) configkit.Options[config] {
|
resolve := func(value string) configkit.Options[config] {
|
||||||
return configkit.Options[config]{Definition: configkit.Definition[config]{Resolve: func(current *config, resolution *configkit.Resolution) error {
|
return configkit.Options[config]{Definition: configkit.Definition[config]{Resolve: func(current *config, resolution *configkit.Resolution) error {
|
||||||
current.Count = len(value) + 8
|
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
|
return nil
|
||||||
}}}
|
}}}
|
||||||
}
|
}
|
||||||
@@ -281,7 +281,7 @@ func TestFacadeDiffUsesFrozenFilesAndRunsCallbacksOnlyDuringLoad(t *testing.T) {
|
|||||||
|
|
||||||
func TestFacadeDiffCombinesSecretSensitivityAndLoadErrors(t *testing.T) {
|
func TestFacadeDiffCombinesSecretSensitivityAndLoadErrors(t *testing.T) {
|
||||||
t.Setenv("FACADE_DIFF_SECRET", "credential-one")
|
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"
|
return path.String() == "/secret"
|
||||||
}}}
|
}}}
|
||||||
left, err := configkit.Load[facadeDiffSecretConfig](configkit.Bytes("left.yml", []byte("secret: FACADE_DIFF_SECRET\n")), secretOptions)
|
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"}
|
names := []string{"config.yml"}
|
||||||
groups := []FilenameGroup{CustomFilenames(names...)}
|
groups := []FilenameGroup{CustomFilenames(names...)}
|
||||||
directoryOptions := []DirectorySearchOptions{{Filenames: groups}}
|
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})
|
source := SearchConfig("", search, SearchOptions{Required: true})
|
||||||
|
|
||||||
paths[0] = laterPath
|
paths[0] = laterPath
|
||||||
names[0] = "later.yml"
|
names[0] = "later.yml"
|
||||||
groups[0] = CustomFilenames("later.yml")
|
groups[0] = CustomFilenames("later.yml")
|
||||||
directoryOptions[0].Filenames[0] = CustomFilenames("later.yml")
|
directoryOptions[0].Filenames[0] = CustomFilenames("later.yml")
|
||||||
search = Paths(laterPath)
|
search = SearchPaths(laterPath)
|
||||||
|
|
||||||
result, err := Load[facadeDiscoveryConfig](source)
|
result, err := Load[facadeDiscoveryConfig](source)
|
||||||
if err != nil || result.Config.Value != "selected" {
|
if err != nil || result.Config.Value != "selected" {
|
||||||
@@ -110,7 +110,7 @@ func TestFacadeSearchOrderingRequirednessAndSnapshots(t *testing.T) {
|
|||||||
t.Fatalf("ordered search sources = %#v", sources)
|
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
|
var loadErr *LoadError
|
||||||
if !errors.As(err, &loadErr) {
|
if !errors.As(err, &loadErr) {
|
||||||
t.Fatalf("required exhausted search error = %v, want *LoadError", err)
|
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")
|
mainPath := writeFacadeImportFile(t, root, "main.yml", "settings: {main: value}\n")
|
||||||
underlayPath := writeFacadeImportFile(t, root, "underlay/base.yml", "settings: {underlay: value}\n")
|
underlayPath := writeFacadeImportFile(t, root, "underlay/base.yml", "settings: {underlay: value}\n")
|
||||||
result, err := LoadLayered(
|
result, err := LoadLayered(
|
||||||
Input{Source: SearchConfig("", Paths(mainPath))},
|
Input{Source: SearchConfig("", SearchPaths(mainPath))},
|
||||||
LayeredOptions[facadeDiscoveryConfig]{Underlays: []Underlay{{Inputs: []Input{{
|
LayeredOptions[facadeDiscoveryConfig]{Underlays: []Underlay{{Inputs: []Input{{
|
||||||
Source: SearchConfig("", SearchDirectory(filepath.Dir(underlayPath), DirectorySearchOptions{
|
Source: SearchConfig("", SearchDirectory(filepath.Dir(underlayPath), DirectorySearchOptions{
|
||||||
Filenames: []FilenameGroup{CustomFilenames("base.yml")},
|
Filenames: []FilenameGroup{CustomFilenames("base.yml")},
|
||||||
@@ -142,7 +142,7 @@ func TestFacadeSearchWorksForLayeredMainAndUnderlay(t *testing.T) {
|
|||||||
t.Fatalf("layered search = %#v, %v", result, err)
|
t.Fatalf("layered search = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
sources := result.Sources()
|
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 {
|
sources[1].Path != canonicalFacadePath(t, underlayPath) || sources[1].Layer != LayerUnderlay {
|
||||||
t.Fatalf("layered search sources = %#v", sources)
|
t.Fatalf("layered search sources = %#v", sources)
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ func TestFacadeSearchSelectedAndAbsentUseRootOnlyContext(t *testing.T) {
|
|||||||
filesystem := &FilesystemContext{Root: root}
|
filesystem := &FilesystemContext{Root: root}
|
||||||
options := Options[facadeDiscoveryConfig]{Overlay: "testing", Filesystem: filesystem}
|
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" {
|
if err != nil || selected.Config.Value != "overlay" {
|
||||||
t.Fatalf("selected search with root-only context = %#v, %v", selected, err)
|
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)
|
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)
|
searchedAbsent, err := Load(missing, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("exhausted search with root-only overlay context: %v", err)
|
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")
|
inside := writeFacadeImportFile(t, root, "inside.yml", "value: fallback\n")
|
||||||
outsideRoot := t.TempDir()
|
outsideRoot := t.TempDir()
|
||||||
outside := writeFacadeImportFile(t, outsideRoot, "outside.yml", "value: selected\n")
|
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}})
|
_, err := Load(search, Options[facadeDiscoveryConfig]{Filesystem: &FilesystemContext{Root: root}})
|
||||||
var loadErr *LoadError
|
var loadErr *LoadError
|
||||||
@@ -228,7 +228,7 @@ func TestFacadeSearchRunsCallbacksOnceAndKeepsInspectionWithSelectedFile(t *test
|
|||||||
selectedPath := writeFacadeImportFile(t, root, "selected.yml", "value: selected\n")
|
selectedPath := writeFacadeImportFile(t, root, "selected.yml", "value: selected\n")
|
||||||
writeFacadeImportFile(t, root, "later.yml", "value: later\n")
|
writeFacadeImportFile(t, root, "later.yml", "value: later\n")
|
||||||
defaultsCalls, resolveCalls, validateCalls := 0, 0, 0
|
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]{
|
Definition: Definition[facadeDiscoveryLifecycleConfig]{
|
||||||
Defaults: func() facadeDiscoveryLifecycleConfig {
|
Defaults: func() facadeDiscoveryLifecycleConfig {
|
||||||
defaultsCalls++
|
defaultsCalls++
|
||||||
@@ -237,7 +237,7 @@ func TestFacadeSearchRunsCallbacksOnceAndKeepsInspectionWithSelectedFile(t *test
|
|||||||
Resolve: func(configuration *facadeDiscoveryLifecycleConfig, resolution *Resolution) error {
|
Resolve: func(configuration *facadeDiscoveryLifecycleConfig, resolution *Resolution) error {
|
||||||
resolveCalls++
|
resolveCalls++
|
||||||
configuration.Resolved = "from:" + configuration.Value
|
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
|
return nil
|
||||||
},
|
},
|
||||||
Validate: func(_ *facadeDiscoveryLifecycleConfig, _ *Validation) { validateCalls++ },
|
Validate: func(_ *facadeDiscoveryLifecycleConfig, _ *Validation) { validateCalls++ },
|
||||||
@@ -272,7 +272,7 @@ func TestFacadeSearchSequenceItemAndAbsentInputPlacement(t *testing.T) {
|
|||||||
Destination: Keys("items"),
|
Destination: Keys("items"),
|
||||||
PlacementMode: PlacementModeSequenceItem,
|
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" {
|
if err != nil || len(selected.Config.Items) != 1 || selected.Config.Items[0].Name != "selected" {
|
||||||
t.Fatalf("selected sequence item = %#v, %v", selected, err)
|
t.Fatalf("selected sequence item = %#v, %v", selected, err)
|
||||||
}
|
}
|
||||||
@@ -283,7 +283,7 @@ func TestFacadeSearchSequenceItemAndAbsentInputPlacement(t *testing.T) {
|
|||||||
defaults := func() facadeDiscoveryPlacementConfig {
|
defaults := func() facadeDiscoveryPlacementConfig {
|
||||||
return facadeDiscoveryPlacementConfig{Items: []facadeDiscoveryPlacementItem{}}
|
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},
|
Definition: Definition[facadeDiscoveryPlacementConfig]{Defaults: defaults},
|
||||||
Destination: options.Destination,
|
Destination: options.Destination,
|
||||||
PlacementMode: options.PlacementMode,
|
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"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type equivalentConfig struct {
|
type equivalentConfig struct {
|
||||||
@@ -69,7 +69,7 @@ func TestEquivalentSourceFormsPreserveInputSemantics(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, field := range []string{"version", "name", "enabled", "count", "items", "optional"} {
|
for _, field := range []string{"version", "name", "enabled", "count", "items", "optional"} {
|
||||||
info, ok := result.Metadata().Lookup(configkit.Keys(field))
|
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)
|
t.Errorf("%s input info = %#v, %v", field, info, ok)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,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]{
|
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"})}}}},
|
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" {
|
if err != nil || result.Config.Secret != "new-secret-canary" {
|
||||||
t.Fatalf("secret load = %#v, %v", result, err)
|
t.Fatalf("secret load = %#v, %v", result, err)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed testdata/filesystem
|
//go:embed testdata/filesystem
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit"
|
"go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFinalSequenceIdentityTreatsLateExtendsFieldsAsPayload(t *testing.T) {
|
func TestFinalSequenceIdentityTreatsLateExtendsFieldsAsPayload(t *testing.T) {
|
||||||
@@ -29,7 +29,7 @@ func TestFinalSequenceIdentityTreatsLateExtendsFieldsAsPayload(t *testing.T) {
|
|||||||
if phase == "resolution" {
|
if phase == "resolution" {
|
||||||
definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
|
definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
|
||||||
(*value)["items"] = items
|
(*value)["items"] = items
|
||||||
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "supply application payload")
|
resolution.RecordNormalization(configkit.Keys("items"), "supply application payload")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ func TestFinalSequenceIdentityValidatesLateValues(t *testing.T) {
|
|||||||
case "resolution":
|
case "resolution":
|
||||||
options.Definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
|
options.Definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
|
||||||
(*value)["items"] = items
|
(*value)["items"] = items
|
||||||
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "create effective records")
|
resolution.RecordNormalization(configkit.Keys("items"), "create effective records")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ func TestFinalSequenceIdentityMatchesLateDynamicNamespaces(t *testing.T) {
|
|||||||
case "resolution":
|
case "resolution":
|
||||||
options.Definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
|
options.Definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
|
||||||
(*value)["collections"] = map[string]any{name: map[string]any{"items": items}}
|
(*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
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPublicHistoricalPathsEnumerateContextualShapes(t *testing.T) {
|
func TestPublicHistoricalPathsEnumerateContextualShapes(t *testing.T) {
|
||||||
@@ -40,12 +40,16 @@ func TestPublicHistoricalPathsEnumerateContextualShapes(t *testing.T) {
|
|||||||
if len(metadata.History(paths[6])) == 0 {
|
if len(metadata.History(paths[6])) == 0 {
|
||||||
t.Fatal("enumerated numeric path lost history")
|
t.Fatal("enumerated numeric path lost history")
|
||||||
}
|
}
|
||||||
|
assertPublicShapeHistory(t, metadata, configkit.LayerCLI)
|
||||||
if _, ok := metadata.Lookup(configkit.Keys("shape", "0")); ok {
|
if _, ok := metadata.Lookup(configkit.Keys("shape", "0")); ok {
|
||||||
t.Fatal("mapping-key path resolved against the effective sequence")
|
t.Fatal("mapping-key path resolved against the effective sequence")
|
||||||
}
|
}
|
||||||
if len((configkit.Metadata{}).HistoricalPaths()) != 0 {
|
if len((configkit.Metadata{}).HistoricalPaths()) != 0 {
|
||||||
t.Fatal("zero metadata contains historical paths")
|
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")
|
paths[1] = configkit.Keys("mutated")
|
||||||
if again := metadata.HistoricalPaths(); !reflect.DeepEqual(pathStrings(again), want) {
|
if again := metadata.HistoricalPaths(); !reflect.DeepEqual(pathStrings(again), want) {
|
||||||
@@ -53,6 +57,60 @@ func TestPublicHistoricalPathsEnumerateContextualShapes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func pathStrings(paths []configkit.Path) []string {
|
||||||
result := make([]string, len(paths))
|
result := make([]string, len(paths))
|
||||||
for index, path := range paths {
|
for index, path := range paths {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFacadeNativeDiscoveryPreservesAliasIdentity(t *testing.T) {
|
func TestFacadeNativeDiscoveryPreservesAliasIdentity(t *testing.T) {
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ main: main
|
|||||||
|
|
||||||
func TestImportDefaultsPreserveEmptyPatternAndRequiredness(t *testing.T) {
|
func TestImportDefaultsPreserveEmptyPatternAndRequiredness(t *testing.T) {
|
||||||
type config struct {
|
type config struct {
|
||||||
Value string `yaml:"value"`
|
Value string `yaml:"value"`
|
||||||
|
Hidden string `yaml:"hidden"`
|
||||||
}
|
}
|
||||||
directory := t.TempDir()
|
directory := t.TempDir()
|
||||||
mainPath := writeFacadeImportFile(t, directory, "main.yml", `configkit:
|
mainPath := writeFacadeImportFile(t, directory, "main.yml", `configkit:
|
||||||
@@ -101,17 +102,23 @@ func TestImportDefaultsPreserveEmptyPatternAndRequiredness(t *testing.T) {
|
|||||||
- dir: snippets
|
- dir: snippets
|
||||||
`)
|
`)
|
||||||
writeFacadeImportFile(t, directory, "snippets/value.cfg", "value: imported\n")
|
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})
|
unrestricted, err := Load(File(mainPath), Options[config]{Imports: true})
|
||||||
if err != nil || unrestricted.Config.Value != "imported" {
|
if err != nil || unrestricted.Config.Value != "imported" {
|
||||||
t.Fatalf("unspecified pattern = %#v, %v", unrestricted, err)
|
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{}}})
|
empty, err := Load(File(mainPath), Options[config]{Imports: true, ImportDefaults: ImportDefaults{Pattern: []string{}}})
|
||||||
if err != nil || empty.Config.Value != "" {
|
if err != nil || empty.Config.Value != "" {
|
||||||
t.Fatalf("empty pattern = %#v, %v", empty, err)
|
t.Fatalf("empty pattern = %#v, %v", empty, err)
|
||||||
}
|
}
|
||||||
emptySources := empty.Sources()
|
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() != "/configkit/imports/0" || emptySources[0].EmptyImportSelections[0].Selector != ImportSelectorDirectory {
|
||||||
t.Fatalf("empty-pattern evidence = %#v", emptySources)
|
t.Fatalf("empty-pattern evidence = %#v", emptySources)
|
||||||
}
|
}
|
||||||
required := true
|
required := true
|
||||||
@@ -120,7 +127,7 @@ func TestImportDefaultsPreserveEmptyPatternAndRequiredness(t *testing.T) {
|
|||||||
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != CodeImportMissing {
|
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != CodeImportMissing {
|
||||||
t.Fatalf("required empty selection error = %T %v", err, err)
|
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)
|
t.Fatalf("required empty selection was recorded as optional: %#v", sources)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,15 +237,15 @@ value: loaded
|
|||||||
callbackPath := ""
|
callbackPath := ""
|
||||||
loaded, err := Load(File(mainPath), Options[config]{Imports: true, Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
loaded, err := Load(File(mainPath), Options[config]{Imports: true, Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||||
first, ok := resolution.Source("source-1")
|
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)
|
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")
|
second, ok := resolution.Source("source-1")
|
||||||
if !ok || second.ImportSelections[0].Path.String() != "/configkit/imports/0" {
|
if !ok || second.EmptyImportSelections[0].Path.String() != "/configkit/imports/0" {
|
||||||
t.Fatalf("resolution snapshot changed after mutation: %#v", second)
|
t.Fatalf("resolution snapshot changed after mutation: %#v", second)
|
||||||
}
|
}
|
||||||
callbackPath = second.ImportSelections[0].Path.String()
|
callbackPath = second.EmptyImportSelections[0].Path.String()
|
||||||
return nil
|
return nil
|
||||||
}}})
|
}}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -248,11 +255,11 @@ value: loaded
|
|||||||
t.Fatalf("callback path = %q", callbackPath)
|
t.Fatalf("callback path = %q", callbackPath)
|
||||||
}
|
}
|
||||||
sources := loaded.Sources()
|
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)
|
t.Fatalf("result source evidence = %#v", sources)
|
||||||
}
|
}
|
||||||
sources[0].ImportSelections[0].Path = Keys("result mutation")
|
sources[0].EmptyImportSelections[0].Path = Keys("result mutation")
|
||||||
if got := loaded.Sources()[0].ImportSelections[0].Path.String(); got != "/configkit/imports/0" {
|
if got := loaded.Sources()[0].EmptyImportSelections[0].Path.String(); got != "/configkit/imports/0" {
|
||||||
t.Fatalf("result snapshot changed after mutation: %q", got)
|
t.Fatalf("result snapshot changed after mutation: %q", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,11 +275,11 @@ value: loaded
|
|||||||
t.Fatalf("failed load error = %T %v", err, err)
|
t.Fatalf("failed load error = %T %v", err, err)
|
||||||
}
|
}
|
||||||
errorSources := loadError.Sources()
|
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() != "/configkit/imports/0" {
|
||||||
t.Fatalf("error source evidence = %#v", errorSources)
|
t.Fatalf("error source evidence = %#v", errorSources)
|
||||||
}
|
}
|
||||||
errorSources[0].ImportSelections[0].Path = Keys("error mutation")
|
errorSources[0].EmptyImportSelections[0].Path = Keys("error mutation")
|
||||||
if got := loadError.Sources()[0].ImportSelections[0].Path.String(); got != "/configkit/imports/0" {
|
if got := loadError.Sources()[0].EmptyImportSelections[0].Path.String(); got != "/configkit/imports/0" {
|
||||||
t.Fatalf("error snapshot changed after mutation: %q", got)
|
t.Fatalf("error snapshot changed after mutation: %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ entries:
|
|||||||
Definition: Definition[map[string]any]{
|
Definition: Definition[map[string]any]{
|
||||||
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
|
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
|
||||||
SequenceIdentities: []SequenceIdentity{SequenceIdentityAt(Keys("services", "records"), "id")},
|
SequenceIdentities: []SequenceIdentity{SequenceIdentityAt(Keys("services", "records"), "id")},
|
||||||
Secret: func(candidate Path) bool {
|
Sensitive: func(candidate Path) bool {
|
||||||
return candidate.String() == "/services/records/0/version"
|
return candidate.String() == "/services/records/0/version"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -67,6 +67,14 @@ entries:
|
|||||||
}
|
}
|
||||||
|
|
||||||
sources := result.Sources()
|
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))
|
filesByID := make(map[string]string, len(sources))
|
||||||
for _, source := range sources {
|
for _, source := range sources {
|
||||||
filesByID[source.ID] = source.Path
|
filesByID[source.ID] = source.Path
|
||||||
@@ -77,8 +85,8 @@ entries:
|
|||||||
itemSource = source
|
itemSource = source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if itemSource.VersionCheck == nil || !itemSource.VersionCheck.Version.Present || !itemSource.VersionCheck.Redacted || itemSource.VersionCheck.Version.Text != "" {
|
if itemSource.VersionEvidence == nil || !itemSource.VersionEvidence.Version.Present || !itemSource.VersionEvidence.Redacted || itemSource.VersionEvidence.Version.Text != "" {
|
||||||
t.Fatalf("consumed header inspection = %#v", itemSource.VersionCheck)
|
t.Fatalf("consumed header inspection = %#v", itemSource.VersionEvidence)
|
||||||
}
|
}
|
||||||
idInfo, ok := result.Metadata().Lookup(Keys("services", "records").Index(0).Key("id"))
|
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" {
|
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]{
|
Definition: Definition[map[string]any]{
|
||||||
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
|
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
|
||||||
SequenceIdentities: []SequenceIdentity{SequenceIdentityAt(Keys("services", "records"), "version")},
|
SequenceIdentities: []SequenceIdentity{SequenceIdentityAt(Keys("services", "records"), "version")},
|
||||||
Secret: func(candidate Path) bool {
|
Sensitive: func(candidate Path) bool {
|
||||||
return candidate.String() == "/services/records/0/version"
|
return candidate.String() == "/services/records/0/version"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -129,7 +137,7 @@ entries:
|
|||||||
break
|
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)
|
t.Fatalf("failed-load source inspection exposed or omitted header evidence: %#v", failedItemSource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFacadeDirectoryRootBelowContainedAbsoluteSymlink(t *testing.T) {
|
func TestFacadeDirectoryRootBelowContainedAbsoluteSymlink(t *testing.T) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type inheritedEntry struct {
|
type inheritedEntry struct {
|
||||||
@@ -42,7 +42,7 @@ func TestFacadeInheritancePreservesPresenceOriginsAndHistory(t *testing.T) {
|
|||||||
if !resolution.IsExplicit(configkit.Keys("entries", "child", "enabled")) {
|
if !resolution.IsExplicit(configkit.Keys("entries", "child", "enabled")) {
|
||||||
t.Error("inherited false is not explicit")
|
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 {
|
if !found || len(info.Origin.Inheritance) != 1 {
|
||||||
t.Errorf("resolver inherited origin = %#v, %v", info, found)
|
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]{
|
result, err := configkit.Load(filesystem.File("config/identified.yml"), configkit.Options[configuration]{
|
||||||
Definition: configkit.Definition[configuration]{
|
Definition: configkit.Definition[configuration]{
|
||||||
SequenceIdentities: registrations,
|
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"
|
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]{
|
_, err := configkit.Load(configkit.Bytes("identified-failure.yml", input), configkit.Options[configuration]{
|
||||||
Definition: configkit.Definition[configuration]{
|
Definition: configkit.Definition[configuration]{
|
||||||
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("jobs"), "id")},
|
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) {
|
if err == nil || strings.Contains(err.Error(), canary) {
|
||||||
@@ -468,7 +468,7 @@ func TestFacadeInheritedSecretsStayRedactedAfterLookup(t *testing.T) {
|
|||||||
Entries map[string]entry `yaml:"entries"`
|
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]{
|
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 {
|
if err != nil || result.Config.Entries["child"].Credential.Value() != canary {
|
||||||
t.Fatalf("inherited credential = %#v, %v", result, err)
|
t.Fatalf("inherited credential = %#v, %v", result, err)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFacadeItemScopesDoNotReinterpretGlobalRegistrations(t *testing.T) {
|
func TestFacadeItemScopesDoNotReinterpretGlobalRegistrations(t *testing.T) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFacadeItemDomainVersionsKeepEffectiveScope(t *testing.T) {
|
func TestFacadeItemDomainVersionsKeepEffectiveScope(t *testing.T) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type layeredFacadeConfig struct {
|
type layeredFacadeConfig struct {
|
||||||
@@ -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"))
|
configkit.Default(resolution, configkit.Keys("derived"), &config.Derived, config.Settings["native"], "derive from native", configkit.Keys("settings", "native"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Validate: func(config *layeredFacadeConfig, validation *configkit.Validation) { checks.validation++ },
|
Validate: func(config *layeredFacadeConfig, validation *configkit.Validation) { checks.validation++ },
|
||||||
Secret: func(path configkit.Path) bool { return path.String() == "/settings/secret" },
|
Sensitive: func(path configkit.Path) bool { return path.String() == "/settings/secret" },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -80,7 +80,7 @@ func TestFacadeLayeredMixedSourcesAndInspection(t *testing.T) {
|
|||||||
if len(sources) != 8 {
|
if len(sources) != 8 {
|
||||||
t.Fatalf("sources = %#v", sources)
|
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}
|
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}
|
wantKinds := []configkit.SourceKind{configkit.SourceKindFile, configkit.SourceKindFilesystem, configkit.SourceKindFilesystem, configkit.SourceKindMapping, configkit.SourceKindFilesystem, configkit.SourceKindMapping, configkit.SourceKindFilesystem, configkit.SourceKindCLI}
|
||||||
for index, source := range sources {
|
for index, source := range sources {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mappingFacadeConfig struct {
|
type mappingFacadeConfig struct {
|
||||||
@@ -36,7 +36,7 @@ func TestFacadeMappingSnapshotsAndMatchesYAML(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(result.Config, want) {
|
if !reflect.DeepEqual(result.Config, want) {
|
||||||
t.Fatalf("mapping config = %#v, want %#v", 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)
|
t.Fatalf("mapping sources = %#v", sources)
|
||||||
}
|
}
|
||||||
info, ok := result.Metadata().Lookup(configkit.Keys("name"))
|
info, ok := result.Metadata().Lookup(configkit.Keys("name"))
|
||||||
@@ -89,7 +89,7 @@ func TestFacadeMappingImportsUseExplicitContext(t *testing.T) {
|
|||||||
if err != nil || result.Config.Name != "imported" || !result.Config.Enabled {
|
if err != nil || result.Config.Name != "imported" || !result.Config.Enabled {
|
||||||
t.Fatalf("mapping import = %#v, %v", result, err)
|
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)
|
t.Fatalf("mapping import sources = %#v", sources)
|
||||||
}
|
}
|
||||||
checkMappingCode(t, source, configkit.Options[mappingFacadeConfig]{Imports: true}, configkit.CodeOptionsInvalid)
|
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"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type observedResourceFS struct {
|
type observedResourceFS struct {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFacadeSequenceIdentitySnapshotsSelectorsAndFields(t *testing.T) {
|
func TestFacadeSequenceIdentitySnapshotsSelectorsAndFields(t *testing.T) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFacadeNoSourcePreservesAbsenceWithSequencePolicy(t *testing.T) {
|
func TestFacadeNoSourcePreservesAbsenceWithSequencePolicy(t *testing.T) {
|
||||||
@@ -125,7 +125,7 @@ func TestFacadeRegisteredReplacementExplainsRemovedDescendants(t *testing.T) {
|
|||||||
t.Fatalf("removal not explained: %#v", explanation)
|
t.Fatalf("removal not explained: %#v", explanation)
|
||||||
}
|
}
|
||||||
last := explanation.History[len(explanation.History)-1]
|
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)
|
t.Fatalf("wrong removal transition/origin: %#v", last)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -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" {
|
if got := ordinary.Config["entries"].([]any); len(got) != 1 || got[0].(map[string]any)["id"] != "one" {
|
||||||
t.Fatalf("ordinary item placement = %#v", ordinary.Config)
|
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)
|
t.Fatalf("ordinary item source = %#v", sources)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +230,7 @@ func TestFacadeSequencePlacementControlsAcrossLoadEntryPoints(t *testing.T) {
|
|||||||
t.Fatalf("layered item placement = %#v", layered.Config)
|
t.Fatalf("layered item placement = %#v", layered.Config)
|
||||||
}
|
}
|
||||||
for _, source := range layered.Sources() {
|
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)
|
t.Errorf("layered item source = %#v", source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,7 +334,7 @@ items:
|
|||||||
t.Fatalf("callback snapshots = %#v, contexts %#v", callbackSources, callbackContexts)
|
t.Fatalf("callback snapshots = %#v, contexts %#v", callbackSources, callbackContexts)
|
||||||
}
|
}
|
||||||
for index, source := range callbackSources {
|
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)
|
t.Errorf("callback source = %#v", source)
|
||||||
}
|
}
|
||||||
if callbackContexts[index].FS == nil || callbackContexts[index].BaseDir != "records" {
|
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)
|
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)
|
t.Fatalf("final item sources = %#v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,9 +372,9 @@ func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
|
|||||||
t.Fatalf("early failure = %v", err)
|
t.Fatalf("early failure = %v", err)
|
||||||
}
|
}
|
||||||
sources := loadError.Sources()
|
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[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 {
|
sources[1].PlacementMode != configkit.PlacementModeMapping {
|
||||||
t.Fatalf("early source snapshots = %#v", sources)
|
t.Fatalf("early source snapshots = %#v", sources)
|
||||||
}
|
}
|
||||||
@@ -399,13 +399,13 @@ func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
|
|||||||
t.Fatalf("post-placement failure = %v", err)
|
t.Fatalf("post-placement failure = %v", err)
|
||||||
}
|
}
|
||||||
sources = loadError.Sources()
|
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[1].Destination.String() != "/items/0/metadata" || sources[1].DestinationUnresolved {
|
sources[1].Destination.String() != "/items/0/metadata" || sources[1].DestinationIsFallback {
|
||||||
t.Fatalf("post-placement source snapshots = %#v", sources)
|
t.Fatalf("post-placement source snapshots = %#v", sources)
|
||||||
}
|
}
|
||||||
firstSnapshot := sources[1]
|
firstSnapshot := sources[1]
|
||||||
sources[1].Destination = configkit.Keys("mutated")
|
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)
|
t.Fatalf("failure source snapshots were aliased: %#v", again)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +420,7 @@ func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("successful item load: %v", err)
|
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)
|
t.Fatalf("callback source snapshot = %#v", callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,13 +450,13 @@ func TestFacadeNestedItemFallbackUsesNearestResolvedContainer(t *testing.T) {
|
|||||||
if len(sources) != 3 {
|
if len(sources) != 3 {
|
||||||
t.Fatalf("nested source snapshots = %#v", sources)
|
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)
|
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)
|
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)
|
t.Errorf("nested helper fallback = %#v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,7 +496,7 @@ func TestFacadeSequenceIdentityRegistrationIsDetached(t *testing.T) {
|
|||||||
|
|
||||||
func TestFacadeSourceInfoPlacementCopiesAndZeroValues(t *testing.T) {
|
func TestFacadeSourceInfoPlacementCopiesAndZeroValues(t *testing.T) {
|
||||||
zero := configkit.SourceInfo{}
|
zero := configkit.SourceInfo{}
|
||||||
if zero.PlacementMode != configkit.PlacementModeMapping || zero.DestinationUnresolved {
|
if zero.PlacementMode != configkit.PlacementModeMapping || zero.DestinationIsFallback {
|
||||||
t.Fatalf("zero source info = %#v", zero)
|
t.Fatalf("zero source info = %#v", zero)
|
||||||
}
|
}
|
||||||
loaded, err := configkit.Load[map[string]any](configkit.Mapping("mapping", map[string]any{"value": true}))
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
sources := loaded.Sources()
|
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)
|
t.Fatalf("mapping source evidence = %#v", sources)
|
||||||
}
|
}
|
||||||
first := sources[0]
|
first := sources[0]
|
||||||
first.Destination = configkit.Keys("caller")
|
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)
|
t.Fatalf("source evidence changed after caller mutation: %#v", again)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -598,7 +598,7 @@ entries:
|
|||||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.File(underlayPath), Imports: true}}}},
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.File(underlayPath), Imports: true}}}},
|
||||||
Definition: configkit.Definition[configuration]{
|
Definition: configkit.Definition[configuration]{
|
||||||
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("entries"), "id")},
|
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"
|
return path.String() == "/entries/0/token" || path.String() == "/entries/1/token"
|
||||||
},
|
},
|
||||||
Resolve: func(value *configuration, resolution *configkit.Resolution) error {
|
Resolve: func(value *configuration, resolution *configkit.Resolution) error {
|
||||||
@@ -611,8 +611,8 @@ entries:
|
|||||||
if resolution.IsExplicit(configkit.Keys("entries").Index(0).Key("omitted")) {
|
if resolution.IsExplicit(configkit.Keys("entries").Index(0).Key("omitted")) {
|
||||||
t.Error("resolver treated an omitted field as explicit")
|
t.Error("resolver treated an omitted field as explicit")
|
||||||
}
|
}
|
||||||
info, found := resolution.InputInfo(configkit.Keys("entries").Index(0).Key("count"))
|
info, found := resolution.EntryInfo(configkit.Keys("entries").Index(0).Key("count"))
|
||||||
if !found || info.Origin.Layer != configkit.LayerBase {
|
if !found || info.Origin.Layer != configkit.LayerMain {
|
||||||
t.Errorf("resolver current-record input info = %#v, %v", info, found)
|
t.Errorf("resolver current-record input info = %#v, %v", info, found)
|
||||||
}
|
}
|
||||||
for _, id := range []string{"source-3", "source-4"} {
|
for _, id := range []string{"source-3", "source-4"} {
|
||||||
@@ -646,7 +646,7 @@ entries:
|
|||||||
t.Fatalf("callback sources = %#v; contexts = %#v", callbackItems, callbackContexts)
|
t.Fatalf("callback sources = %#v; contexts = %#v", callbackItems, callbackContexts)
|
||||||
}
|
}
|
||||||
for _, source := range callbackItems {
|
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)
|
t.Errorf("callback item source = %#v", source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type movedVersionItem struct {
|
type movedVersionItem struct {
|
||||||
@@ -64,7 +64,7 @@ func TestMainVersionDecodingFollowsSequenceMovement(t *testing.T) {
|
|||||||
t.Fatal("unrelated underlay payload was treated as the main declaration")
|
t.Fatal("unrelated underlay payload was treated as the main declaration")
|
||||||
}
|
}
|
||||||
for _, source := range loaded.Sources() {
|
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")
|
t.Fatal("effective movement changed source-local version evidence")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFacadeSourceVersionEvidenceFollowsAssembledSequenceRanges(t *testing.T) {
|
func TestFacadeSourceVersionEvidenceFollowsAssembledSequenceRanges(t *testing.T) {
|
||||||
@@ -25,7 +25,7 @@ func TestFacadeSourceVersionEvidenceFollowsAssembledSequenceRanges(t *testing.T)
|
|||||||
definition := configkit.Definition[map[string]any]{
|
definition := configkit.Definition[map[string]any]{
|
||||||
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("records"), "id")},
|
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 }},
|
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"
|
first := "records: [{id: first, version: first-version}]\n"
|
||||||
child := "records: [{id: child, version: PRIVATE-VERSION-CANARY}]\n"
|
child := "records: [{id: child, version: PRIVATE-VERSION-CANARY}]\n"
|
||||||
@@ -61,7 +61,7 @@ func TestFacadeSourceVersionEvidenceFollowsAssembledSequenceRanges(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sources := result.Sources()
|
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 {
|
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)
|
t.Fatalf("source-local evidence changed: %#v", check)
|
||||||
}
|
}
|
||||||
@@ -209,7 +209,7 @@ func TestFacadeSourceVersionEvidenceRedactsCollidingShapes(t *testing.T) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
foundMapping = true
|
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)
|
t.Errorf("reverse=%v: schema-tagged header was not redacted", reverse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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")},
|
{"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} {
|
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]{
|
loaded, err := configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte(test.main))}, configkit.LayeredOptions[map[string]any]{
|
||||||
SourceVersionMode: mode,
|
SourceVersionMode: mode,
|
||||||
Definition: configkit.Definition[map[string]any]{
|
Definition: configkit.Definition[map[string]any]{
|
||||||
@@ -275,7 +275,7 @@ func TestFacadeSourceVersionAbsenceKeepsCompleteEvidencePath(t *testing.T) {
|
|||||||
if !found {
|
if !found {
|
||||||
t.Fatal("missing callback source")
|
t.Fatal("missing callback source")
|
||||||
}
|
}
|
||||||
callbackEvidence = source.VersionCheck
|
callbackEvidence = source.VersionEvidence
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -283,7 +283,7 @@ func TestFacadeSourceVersionAbsenceKeepsCompleteEvidencePath(t *testing.T) {
|
|||||||
Source: configkit.Bytes("underlay", []byte(test.underlay)), Destination: configkit.Keys("under"),
|
Source: configkit.Bytes("underlay", []byte(test.underlay)), Destination: configkit.Keys("under"),
|
||||||
}}}},
|
}}}},
|
||||||
})
|
})
|
||||||
var evidence *configkit.SourceVersionCheck
|
var evidence *configkit.SourceVersionEvidence
|
||||||
if mode == configkit.SourceVersionModeRequired {
|
if mode == configkit.SourceVersionModeRequired {
|
||||||
var failure *configkit.LoadError
|
var failure *configkit.LoadError
|
||||||
if loaded != nil || !errors.As(err, &failure) {
|
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() {
|
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)
|
t.Fatalf("%s: required diagnostics=%#v", test.name, diagnostics)
|
||||||
}
|
}
|
||||||
evidence = failure.Sources()[1].VersionCheck
|
evidence = failure.Sources()[1].VersionEvidence
|
||||||
} else {
|
} else {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%s: %v", test.name, err)
|
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() {
|
if callbackEvidence == nil || callbackEvidence.Path.String() != test.path.String() {
|
||||||
t.Errorf("%s: callback path is incomplete", test.name)
|
t.Errorf("%s: callback path is incomplete", test.name)
|
||||||
}
|
}
|
||||||
@@ -365,7 +365,7 @@ func TestFacadeSourceVersionMissingHeaderDoesNotOverlapControl(t *testing.T) {
|
|||||||
if child["value"] != "good" {
|
if child["value"] != "good" {
|
||||||
t.Fatal("valid sibling inheritance was not applied")
|
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() {
|
if check == nil || check.Version.Present || check.Path.String() != path.String() {
|
||||||
t.Fatal("optional omission evidence was not retained")
|
t.Fatal("optional omission evidence was not retained")
|
||||||
}
|
}
|
||||||
@@ -544,16 +544,16 @@ func TestFacadeSourceVersionEvidenceIsSanitizedAndDetached(t *testing.T) {
|
|||||||
var callbackSource configkit.SourceInfo
|
var callbackSource configkit.SourceInfo
|
||||||
var callbackContext configkit.ResourceContext
|
var callbackContext configkit.ResourceContext
|
||||||
definition := configkit.Definition[map[string]any]{
|
definition := configkit.Definition[map[string]any]{
|
||||||
Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }},
|
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" },
|
Sensitive: func(candidate configkit.Path) bool { return candidate.String() == "/service/version" },
|
||||||
Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
|
Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
|
||||||
var found bool
|
var found bool
|
||||||
callbackSource, found = resolution.Source("source-1")
|
callbackSource, found = resolution.Source("source-1")
|
||||||
if !found {
|
if !found {
|
||||||
t.Fatal("resolution callback could not query the acquired source")
|
t.Fatal("resolution callback could not query the acquired source")
|
||||||
}
|
}
|
||||||
if callbackSource.VersionCheck == nil || !callbackSource.VersionCheck.Redacted || callbackSource.VersionCheck.Version.Text != "" {
|
if callbackSource.VersionEvidence == nil || !callbackSource.VersionEvidence.Redacted || callbackSource.VersionEvidence.Version.Text != "" {
|
||||||
t.Fatalf("callback source did not conservatively redact evidence: %#v", callbackSource.VersionCheck)
|
t.Fatalf("callback source did not conservatively redact evidence: %#v", callbackSource.VersionEvidence)
|
||||||
}
|
}
|
||||||
callbackContext, found = resolution.SourceContext("source-1")
|
callbackContext, found = resolution.SourceContext("source-1")
|
||||||
if !found || callbackContext.FS == nil || callbackContext.BaseDir != "." {
|
if !found || callbackContext.FS == nil || callbackContext.BaseDir != "." {
|
||||||
@@ -568,17 +568,17 @@ func TestFacadeSourceVersionEvidenceIsSanitizedAndDetached(t *testing.T) {
|
|||||||
if err != nil || loaded == nil {
|
if err != nil || loaded == nil {
|
||||||
t.Fatalf("LoadLayered = %#v, %v", loaded, err)
|
t.Fatalf("LoadLayered = %#v, %v", loaded, err)
|
||||||
}
|
}
|
||||||
if callbackSource.VersionCheck == nil || !callbackSource.VersionCheck.Redacted || callbackSource.VersionCheck.Version.Text != "" {
|
if callbackSource.VersionEvidence == nil || !callbackSource.VersionEvidence.Redacted || callbackSource.VersionEvidence.Version.Text != "" {
|
||||||
t.Fatalf("callback evidence lost its conservative snapshot: %#v", callbackSource.VersionCheck)
|
t.Fatalf("callback evidence lost its conservative snapshot: %#v", callbackSource.VersionEvidence)
|
||||||
}
|
}
|
||||||
callbackSource.VersionCheck.Path = configkit.Keys("mutated-callback")
|
callbackSource.VersionEvidence.Path = configkit.Keys("mutated-callback")
|
||||||
callbackSource.VersionCheck.Version.Text = canary
|
callbackSource.VersionEvidence.Version.Text = canary
|
||||||
callbackSource.VersionCheck.Location.Line = 98
|
callbackSource.VersionEvidence.Location.Line = 98
|
||||||
finalSources := loaded.Sources()
|
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)
|
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 ||
|
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 {
|
check.Location == nil || check.Location.SourceID != finalSources[0].ID || check.Location.Line != 1 {
|
||||||
t.Fatalf("final classified evidence = %#v", check)
|
t.Fatalf("final classified evidence = %#v", check)
|
||||||
@@ -594,9 +594,9 @@ func TestFacadeSourceVersionEvidenceIsSanitizedAndDetached(t *testing.T) {
|
|||||||
check.Redacted = false
|
check.Redacted = false
|
||||||
check.Version.Text = "mutated"
|
check.Version.Text = "mutated"
|
||||||
repeated := loaded.Sources()
|
repeated := loaded.Sources()
|
||||||
if len(repeated) != 1 || repeated[0].VersionCheck.Path.String() != "/version" ||
|
if len(repeated) != 1 || repeated[0].VersionEvidence.Path.String() != "/version" ||
|
||||||
repeated[0].VersionCheck.Location.Line != 1 || !repeated[0].VersionCheck.Redacted ||
|
repeated[0].VersionEvidence.Location.Line != 1 || !repeated[0].VersionEvidence.Redacted ||
|
||||||
repeated[0].VersionCheck.Version.Text != "" {
|
repeated[0].VersionEvidence.Version.Text != "" {
|
||||||
t.Fatalf("source evidence was aliased or re-read after source removal: %#v", repeated)
|
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)
|
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)
|
t.Fatalf("unclassified evidence = %#v", got)
|
||||||
}
|
}
|
||||||
if publicCallback.VersionCheck == nil || !publicCallback.VersionCheck.Redacted || publicCallback.VersionCheck.Version.Text != "" {
|
if publicCallback.VersionEvidence == nil || !publicCallback.VersionEvidence.Redacted || publicCallback.VersionEvidence.Version.Text != "" {
|
||||||
t.Fatalf("unclassified callback evidence was not initially conservative: %#v", publicCallback.VersionCheck)
|
t.Fatalf("unclassified callback evidence was not initially conservative: %#v", publicCallback.VersionEvidence)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,7 +650,7 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
|
|||||||
if checked != 1 {
|
if checked != 1 {
|
||||||
t.Fatalf("main declaration checker calls = %d; error %v", checked, err)
|
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 != "" {
|
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())
|
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)
|
t.Fatalf("malformed version error = %v", err)
|
||||||
}
|
}
|
||||||
sources := loadError.Sources()
|
sources := loadError.Sources()
|
||||||
if len(sources) != 1 || sources[0].VersionCheck == nil || !sources[0].VersionCheck.Redacted ||
|
if len(sources) != 1 || sources[0].VersionEvidence == nil || !sources[0].VersionEvidence.Redacted ||
|
||||||
!sources[0].VersionCheck.Version.Present || sources[0].VersionCheck.Version.Text != "" || sources[0].VersionCheck.Path.String() != "/version" {
|
!sources[0].VersionEvidence.Version.Present || sources[0].VersionEvidence.Version.Text != "" || sources[0].VersionEvidence.Path.String() != "/version" {
|
||||||
t.Fatalf("failed-load source evidence = %#v", sources)
|
t.Fatalf("failed-load source evidence = %#v", sources)
|
||||||
}
|
}
|
||||||
sources[0].VersionCheck.Path = configkit.Keys("mutated-error")
|
sources[0].VersionEvidence.Path = configkit.Keys("mutated-error")
|
||||||
sources[0].VersionCheck.Version.Text = "mutated"
|
sources[0].VersionEvidence.Version.Text = "mutated"
|
||||||
sources[0].VersionCheck.Location.Line = 99
|
sources[0].VersionEvidence.Location.Line = 99
|
||||||
if again := loadError.Sources()[0].VersionCheck; again.Path.String() != "/version" || again.Version.Text != "" || again.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)
|
t.Fatalf("LoadError source evidence was aliased: %#v", again)
|
||||||
}
|
}
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
@@ -707,7 +707,7 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
|
|||||||
if strings.Contains(err.Error(), test.canary) {
|
if strings.Contains(err.Error(), test.canary) {
|
||||||
t.Fatalf("late failure disclosed source-version canary: %v", err)
|
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 != "" {
|
if check == nil || !check.Redacted || !check.Version.Present || check.Version.Text != "" {
|
||||||
t.Fatalf("late-failure evidence was not conservatively redacted: %#v", check)
|
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]{})
|
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)
|
t.Fatalf("default source version check = %#v, %v", defaultResult, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,7 +728,7 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
|
|||||||
}
|
}
|
||||||
sawCLI := false
|
sawCLI := false
|
||||||
for _, source := range withCLI.Sources() {
|
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)
|
t.Fatalf("synthetic CLI source acquired declaration evidence: %#v", source)
|
||||||
}
|
}
|
||||||
sawCLI = sawCLI || source.Kind == configkit.SourceKindCLI
|
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)
|
t.Fatalf("checker calls = %#v; absent contribution must skip while null is checked", checked)
|
||||||
}
|
}
|
||||||
sources := loaded.Sources()
|
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)
|
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 {
|
if absent.Version.Present || absent.Path.String() != "/version" || absent.Location == nil || absent.Redacted {
|
||||||
t.Fatalf("optional omission evidence = %#v", absent)
|
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" {
|
if !null.Version.Present || !null.Version.Null || null.Version.Text != "" || null.Path.String() != "/version" {
|
||||||
t.Fatalf("null declaration evidence = %#v", null)
|
t.Fatalf("null declaration evidence = %#v", null)
|
||||||
}
|
}
|
||||||
@@ -807,7 +807,7 @@ func TestFacadeSourceVersionLifecycleKeepsConsumedHeadersOutOfPayloadHistory(t *
|
|||||||
},
|
},
|
||||||
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
|
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
|
||||||
current.Service.Resolved = "resolved once"
|
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
|
return nil
|
||||||
},
|
},
|
||||||
Validate: func(current *configuration, validation *configkit.Validation) {
|
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())
|
t.Fatalf("consumed declarations affected effective rendering:\n%s\n%s", firstEffective.String(), secondEffective.String())
|
||||||
}
|
}
|
||||||
sourcesA, sourcesB := first.Sources(), second.Sources()
|
sourcesA, sourcesB := first.Sources(), second.Sources()
|
||||||
if len(sourcesA) != 2 || len(sourcesB) != 2 || sourcesA[1].VersionCheck == nil || sourcesB[1].VersionCheck == nil ||
|
if len(sourcesA) != 2 || len(sourcesB) != 2 || sourcesA[1].VersionEvidence == nil || sourcesB[1].VersionEvidence == nil ||
|
||||||
sourcesA[1].VersionCheck.Version.Text != "underlay-release-a" || sourcesB[1].VersionCheck.Version.Text != "underlay-release-b" {
|
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)
|
t.Fatalf("sanitized evidence did not retain distinct accepted declarations: %#v / %#v", sourcesA, sourcesB)
|
||||||
}
|
}
|
||||||
versionExplanation, found := first.Explain(configkit.Keys("service", "version"))
|
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"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUnderlaySequenceDescendantOrigins(t *testing.T) {
|
func TestUnderlaySequenceDescendantOrigins(t *testing.T) {
|
||||||
@@ -31,7 +31,7 @@ func TestUnderlaySequenceDescendantOrigins(t *testing.T) {
|
|||||||
Definition: configkit.Definition[map[string]any]{Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
|
Definition: configkit.Definition[map[string]any]{Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
|
||||||
for _, pointer := range paths {
|
for _, pointer := range paths {
|
||||||
path, _ := configkit.ParsePath(pointer)
|
path, _ := configkit.ParsePath(pointer)
|
||||||
info, ok := resolution.InputInfo(path)
|
info, ok := resolution.EntryInfo(path)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("missing resolver input %s", pointer)
|
t.Errorf("missing resolver input %s", pointer)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type versionCountingReader struct {
|
type versionCountingReader struct {
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module gitea.maximumdirect.net/eric/ConfigKit
|
module go.maximumdirect.net/configkit
|
||||||
|
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ func TestDependencyImportBoundary(t *testing.T) {
|
|||||||
production := !strings.HasSuffix(path, "_test.go")
|
production := !strings.HasSuffix(path, "_test.go")
|
||||||
rootProduction := production && filepath.Dir(path) == "."
|
rootProduction := production && filepath.Dir(path) == "."
|
||||||
internalProduction := production && strings.HasPrefix(filepath.ToSlash(path), "internal/")
|
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)
|
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)
|
t.Errorf("%s imports the public root package", path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,21 @@ package configkit
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
"go.maximumdirect.net/configkit/internal/engine"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValueSnapshot is a detached, deterministic YAML representation of one value.
|
// ValueSnapshot describes a saved value as consistently formatted YAML text.
|
||||||
// Present distinguishes absence or deletion from an explicit null value.
|
// 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 {
|
type ValueSnapshot struct {
|
||||||
Present bool
|
Present bool
|
||||||
Redacted bool
|
Redacted bool
|
||||||
YAML string
|
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 {
|
type ValueChange struct {
|
||||||
Before ValueSnapshot
|
Before ValueSnapshot
|
||||||
After ValueSnapshot
|
After ValueSnapshot
|
||||||
@@ -29,7 +32,8 @@ type SecretReferenceInfo struct {
|
|||||||
Name string
|
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 {
|
type Explanation struct {
|
||||||
Path Path
|
Path Path
|
||||||
Effective ValueSnapshot
|
Effective ValueSnapshot
|
||||||
@@ -38,9 +42,10 @@ type Explanation struct {
|
|||||||
SecretReference *SecretReferenceInfo
|
SecretReference *SecretReferenceInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explain returns safe effective and chronological values for a known final or
|
// Explain returns the saved final value and earlier values for a known current
|
||||||
// historical path. Structural inheritance appears between source applications
|
// or historical path, hiding sensitive information. History follows processing
|
||||||
// and typed reconciliation. Unknown and malformed paths return false.
|
// 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) {
|
func (r *Result[T]) Explain(path Path) (Explanation, bool) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return Explanation{}, false
|
return Explanation{}, false
|
||||||
@@ -49,8 +54,9 @@ func (r *Result[T]) Explain(path Path) (Explanation, bool) {
|
|||||||
return fromEngineExplanation(value), ok
|
return fromEngineExplanation(value), ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteEffective writes deterministic redacted YAML from the frozen resolved
|
// WriteEffective writes consistently formatted YAML for the saved final
|
||||||
// snapshot. The output is intended for inspection and is not promised reloadable.
|
// 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.
|
// Writer errors, including short writes, are returned and may leave partial output.
|
||||||
func (r *Result[T]) WriteEffective(output io.Writer) error {
|
func (r *Result[T]) WriteEffective(output io.Writer) error {
|
||||||
if r == nil {
|
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.
|
// 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 {
|
func (r *Result[T]) WriteExplanation(output io.Writer, path Path) error {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
var inspection *engine.Inspection
|
var inspection *engine.Inspection
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkComposeWideMappings(b *testing.B) {
|
func BenchmarkComposeWideMappings(b *testing.B) {
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ func TestCallbackAssignmentsAreDetachedAcrossLoads(t *testing.T) {
|
|||||||
definition := Definition[cfg]{Resolve: func(c *cfg, r *Resolution) error {
|
definition := Definition[cfg]{Resolve: func(c *cfg, r *Resolution) error {
|
||||||
Normalize(r, Keys("map"), &c.Map, shared, "populate")
|
Normalize(r, Keys("map"), &c.Map, shared, "populate")
|
||||||
c.Pointer = external
|
c.Pointer = external
|
||||||
r.Record(Keys("pointer"), ChangeNormalization, "populate")
|
r.RecordNormalization(Keys("pointer"), "populate")
|
||||||
c.Secret = SecretEnv("CONFIGKIT_CALLBACK_SECRET")
|
c.Secret = SecretEnv("CONFIGKIT_CALLBACK_SECRET")
|
||||||
r.Record(Keys("secret"), ChangeNormalization, "reference")
|
r.RecordNormalization(Keys("secret"), "reference")
|
||||||
return nil
|
return nil
|
||||||
}}
|
}}
|
||||||
if validatorAssignment {
|
if validatorAssignment {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOverlayBroadCollectionPoliciesAndRetainedDeclarations(t *testing.T) {
|
func TestOverlayBroadCollectionPoliciesAndRetainedDeclarations(t *testing.T) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package engine
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
type collectionPolicyScope struct {
|
type collectionPolicyScope struct {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCollectionPolicyWithoutValueRetainsIdentityAndMode(t *testing.T) {
|
func TestCollectionPolicyWithoutValueRetainsIdentityAndMode(t *testing.T) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package engine
|
package engine
|
||||||
|
|
||||||
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
import "go.maximumdirect.net/configkit/internal/document"
|
||||||
|
|
||||||
// A true entry replaces the collection itself as well as its descendants;
|
// A true entry replaces the collection itself as well as its descendants;
|
||||||
// false records ordinary ancestor replacement without selecting a collection
|
// false records ordinary ancestor replacement without selecting a collection
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSourceReplacementRetiresIdentityForEveryValueKind(t *testing.T) {
|
func TestSourceReplacementRetiresIdentityForEveryValueKind(t *testing.T) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package engine
|
|||||||
import (
|
import (
|
||||||
"maps"
|
"maps"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
// composedDocument is the private handoff between source assembly and later
|
// composedDocument is the private handoff between source assembly and later
|
||||||
@@ -223,7 +223,7 @@ func composeDocumentsWithCollectionPolicy(
|
|||||||
rangesBySource[assigned.sourceIndex] = append(rangesBySource[assigned.sourceIndex], assigned)
|
rangesBySource[assigned.sourceIndex] = append(rangesBySource[assigned.sourceIndex], assigned)
|
||||||
}
|
}
|
||||||
for sourceIndex, input := range inputs {
|
for sourceIndex, input := range inputs {
|
||||||
application := historyApplication{layer: LayerBase, value: input}
|
application := historyApplication{layer: LayerMain, value: input}
|
||||||
for _, assigned := range rangesBySource[sourceIndex] {
|
for _, assigned := range rangesBySource[sourceIndex] {
|
||||||
pointer := assigned.path.String()
|
pointer := assigned.path.String()
|
||||||
mode := sequenceHistoryAppend
|
mode := sequenceHistoryAppend
|
||||||
@@ -255,7 +255,7 @@ func composeDocumentsWithCollectionPolicy(
|
|||||||
func completeCompositionOrigins(node *document.Node, path Path, origins map[string]Origin) {
|
func completeCompositionOrigins(node *document.Node, path Path, origins map[string]Origin) {
|
||||||
pointer := path.String()
|
pointer := path.String()
|
||||||
if _, present := origins[pointer]; !present {
|
if _, present := origins[pointer]; !present {
|
||||||
origins[pointer] = Origin{Layer: LayerBase, Locations: []Location{publicLocation(node.Location)}}
|
origins[pointer] = Origin{Layer: LayerMain, Locations: []Location{publicLocation(node.Location)}}
|
||||||
}
|
}
|
||||||
switch node.Kind {
|
switch node.Kind {
|
||||||
case document.Mapping:
|
case document.Mapping:
|
||||||
@@ -513,7 +513,7 @@ func projectedCompositionOrigins(inputs []*document.Node, ranges []sequenceRange
|
|||||||
walkHistoryApplication(application, func(_, effective Path, node *document.Node) {
|
walkHistoryApplication(application, func(_, effective Path, node *document.Node) {
|
||||||
pointer := effective.String()
|
pointer := effective.String()
|
||||||
origin := origins[pointer]
|
origin := origins[pointer]
|
||||||
origin.Layer = LayerBase
|
origin.Layer = LayerMain
|
||||||
origin.Locations = append(origin.Locations, publicLocation(node.Location))
|
origin.Locations = append(origin.Locations, publicLocation(node.Location))
|
||||||
origins[pointer] = origin
|
origins[pointer] = origin
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestComposeWideRegisteredCollectionsPreservesEvidenceAndReplay(t *testing.T) {
|
func TestComposeWideRegisteredCollectionsPreservesEvidenceAndReplay(t *testing.T) {
|
||||||
@@ -92,7 +92,7 @@ values: [one, two]
|
|||||||
{SourceID: "source-1", Line: 2, Column: 3},
|
{SourceID: "source-1", Line: 2, Column: 3},
|
||||||
{SourceID: "source-2", Line: 2, Column: 3},
|
{SourceID: "source-2", Line: 2, Column: 3},
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(serviceOrigin, Origin{Layer: LayerBase, Locations: wantLocations}) {
|
if !reflect.DeepEqual(serviceOrigin, Origin{Layer: LayerMain, Locations: wantLocations}) {
|
||||||
t.Fatalf("services origin = %#v, want composite origin", serviceOrigin)
|
t.Fatalf("services origin = %#v, want composite origin", serviceOrigin)
|
||||||
}
|
}
|
||||||
wantClaimPaths := []string{"", "/services", "/services/api", "/services/api/enabled", "/empty", "", "/services", "/services/worker", "/services/worker/enabled", "/values"}
|
wantClaimPaths := []string{"", "/services", "/services/api", "/services/api/enabled", "/empty", "", "/services", "/services/worker", "/services/worker/enabled", "/values"}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml"
|
yamladapter "go.maximumdirect.net/configkit/internal/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FuzzCompositionAndOverlay(f *testing.F) {
|
func FuzzCompositionAndOverlay(f *testing.F) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
var integerPattern = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`)
|
var integerPattern = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`)
|
||||||
@@ -448,7 +448,7 @@ func collectPresence(node *document.Node, path Path, presence map[string]inputPr
|
|||||||
|
|
||||||
func collectOrigins(node *document.Node, path Path, presence map[string]inputPresence, origins map[string]Origin) {
|
func collectOrigins(node *document.Node, path Path, presence map[string]inputPresence, origins map[string]Origin) {
|
||||||
if input, ok := presence[path.String()]; ok {
|
if input, ok := presence[path.String()]; ok {
|
||||||
origins[path.String()] = Origin{Layer: LayerBase, Locations: []Location{{SourceID: input.location.SourceID, Line: input.location.Line, Column: input.location.Column}}}
|
origins[path.String()] = Origin{Layer: LayerMain, Locations: []Location{{SourceID: input.location.SourceID, Line: input.location.Line, Column: input.location.Column}}}
|
||||||
} else {
|
} else {
|
||||||
origins[path.String()] = Origin{Layer: LayerDefault}
|
origins[path.String()] = Origin{Layer: LayerDefault}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
type namedSmall int8
|
type namedSmall int8
|
||||||
@@ -141,7 +141,7 @@ custom: accepted
|
|||||||
if _, ok := decoded.presence[Keys("nested", "a").String()]; ok {
|
if _, ok := decoded.presence[Keys("nested", "a").String()]; ok {
|
||||||
t.Fatal("parent mapping made omitted child explicit")
|
t.Fatal("parent mapping made omitted child explicit")
|
||||||
}
|
}
|
||||||
if decoded.origins[Keys("nested", "a").String()].Layer != LayerDefault || decoded.origins[Keys("nested", "b").String()].Layer != LayerBase {
|
if decoded.origins[Keys("nested", "a").String()].Layer != LayerDefault || decoded.origins[Keys("nested", "b").String()].Layer != LayerMain {
|
||||||
t.Fatalf("origins = %#v", decoded.origins)
|
t.Fatalf("origins = %#v", decoded.origins)
|
||||||
}
|
}
|
||||||
if decoded.origins[Keys("nested", "b").String()].Locations[0].Line == 0 {
|
if decoded.origins[Keys("nested", "b").String()].Locations[0].Line == 0 {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package engine
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDestinationPlacementCountsAgainstSharedNodeBudget(t *testing.T) {
|
func TestDestinationPlacementCountsAgainstSharedNodeBudget(t *testing.T) {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ func TestRelativeImportsFeedFinalInheritanceAndInspectionLifecycle(t *testing.T)
|
|||||||
child := config.Catalog.Entries["child"]
|
child := config.Catalog.Entries["child"]
|
||||||
child.Derived = child.Label + ":" + child.Dynamic + ":" + fmt.Sprint(child.Height)
|
child.Derived = child.Label + ":" + child.Dynamic + ":" + fmt.Sprint(child.Height)
|
||||||
config.Catalog.Entries["child"] = child
|
config.Catalog.Entries["child"] = child
|
||||||
resolution.Record(Keys("catalog", "entries", "child", "derived"), ChangeNormalization, "derive final child summary", Keys("catalog", "entries", "child", "label"), Keys("catalog", "entries", "child", "dynamic"), Keys("catalog", "entries", "child", "height"))
|
resolution.RecordNormalization(Keys("catalog", "entries", "child", "derived"), "derive final child summary", Keys("catalog", "entries", "child", "label"), Keys("catalog", "entries", "child", "dynamic"), Keys("catalog", "entries", "child", "height"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Validate: func(config *configuration, validation *Validation) {
|
Validate: func(config *configuration, validation *Validation) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
type importDestinationConfig struct {
|
type importDestinationConfig struct {
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ func TestLayeredDestinationLifecycleAndSensitiveInspectionUseFinalShape(t *testi
|
|||||||
Resolve: func(configuration *destinationLifecycleConfig, resolution *Resolution) error {
|
Resolve: func(configuration *destinationLifecycleConfig, resolution *Resolution) error {
|
||||||
observed.resolve++
|
observed.resolve++
|
||||||
configuration.Tenant.Derived = configuration.Tenant.Dynamic
|
configuration.Tenant.Derived = configuration.Tenant.Dynamic
|
||||||
resolution.Record(Keys("tenant", "derived"), ChangeNormalization, "derive from mounted input", Keys("tenant", "dynamic"))
|
resolution.RecordNormalization(Keys("tenant", "derived"), "derive from mounted input", Keys("tenant", "dynamic"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Validate: func(configuration *destinationLifecycleConfig, _ *Validation) {
|
Validate: func(configuration *destinationLifecycleConfig, _ *Validation) {
|
||||||
@@ -425,7 +425,7 @@ func TestLayeredDestinationDiffUsesBilateralSensitivity(t *testing.T) {
|
|||||||
definition: Definition[config]{
|
definition: Definition[config]{
|
||||||
Resolve: func(configuration *config, resolution *Resolution) error {
|
Resolve: func(configuration *config, resolution *Resolution) error {
|
||||||
configuration.Tenant.Derived = configuration.Tenant.Dynamic
|
configuration.Tenant.Derived = configuration.Tenant.Dynamic
|
||||||
resolution.Record(Keys("tenant", "derived"), ChangeNormalization, "derive from dynamic input", Keys("tenant", "dynamic"))
|
resolution.RecordNormalization(Keys("tenant", "derived"), "derive from dynamic input", Keys("tenant", "dynamic"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Secret: func(path Path) bool { return classify && path.String() == "/tenant/dynamic" },
|
Secret: func(path Path) bool { return classify && path.String() == "/tenant/dynamic" },
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNormalizeDestinationUsesLiteralMappingKeys(t *testing.T) {
|
func TestNormalizeDestinationUsesLiteralMappingKeys(t *testing.T) {
|
||||||
|
|||||||
@@ -50,6 +50,16 @@ func (e *LoadError) Sources() []SourceInfo {
|
|||||||
return cloneSourceInfos(e.sources)
|
return cloneSourceInfos(e.sources)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source returns a detached record from the source evidence retained before
|
||||||
|
// loading failed. IDs are local to the failed load. Empty or unknown IDs return
|
||||||
|
// false.
|
||||||
|
func (e *LoadError) Source(id string) (SourceInfo, bool) {
|
||||||
|
if e == nil {
|
||||||
|
return SourceInfo{}, false
|
||||||
|
}
|
||||||
|
return lookupSourceInfo(e.sources, id)
|
||||||
|
}
|
||||||
|
|
||||||
// WriteDiagnostics writes diagnostics in canonical order. It escapes terminal
|
// WriteDiagnostics writes diagnostics in canonical order. It escapes terminal
|
||||||
// control characters and propagates writer errors, including short writes.
|
// control characters and propagates writer errors, including short writes.
|
||||||
func WriteDiagnostics(output io.Writer, diagnostics []Diagnostic) error {
|
func WriteDiagnostics(output io.Writer, diagnostics []Diagnostic) error {
|
||||||
@@ -88,7 +98,9 @@ func (a *diagnosticAccumulator) loadError() *LoadError {
|
|||||||
func canonicalDiagnostics(diagnostics []Diagnostic) []Diagnostic {
|
func canonicalDiagnostics(diagnostics []Diagnostic) []Diagnostic {
|
||||||
canonical := cloneDiagnostics(diagnostics)
|
canonical := cloneDiagnostics(diagnostics)
|
||||||
for i := range canonical {
|
for i := range canonical {
|
||||||
slices.SortFunc(canonical[i].Related, compareLocation)
|
if canonical[i].Code != CodeComposeConflict {
|
||||||
|
slices.SortFunc(canonical[i].Related, compareLocation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
type orderedDiagnostic struct {
|
type orderedDiagnostic struct {
|
||||||
diagnostic Diagnostic
|
diagnostic Diagnostic
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -110,6 +111,44 @@ func TestCanonicalDiagnosticsSortEscapedPaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCanonicalDiagnosticsPreservesCompositionClaimantOrder(t *testing.T) {
|
||||||
|
composition := Diagnostic{
|
||||||
|
Code: CodeComposeConflict,
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: Keys("value"),
|
||||||
|
Primary: &Location{SourceID: "source-4"},
|
||||||
|
Related: []Location{
|
||||||
|
{SourceID: "source-5"},
|
||||||
|
{SourceID: "source-3"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
other := Diagnostic{
|
||||||
|
Code: "decode.type",
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: Keys("other"),
|
||||||
|
Related: []Location{
|
||||||
|
{SourceID: "source-5"},
|
||||||
|
{SourceID: "source-3"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
canonical := canonicalDiagnostics([]Diagnostic{composition, other})
|
||||||
|
var compositionResult, genericResult Diagnostic
|
||||||
|
for _, diagnostic := range canonical {
|
||||||
|
if diagnostic.Code == CodeComposeConflict {
|
||||||
|
compositionResult = diagnostic
|
||||||
|
} else {
|
||||||
|
genericResult = diagnostic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := []string{compositionResult.Related[0].SourceID, compositionResult.Related[1].SourceID}; !slices.Equal(got, []string{"source-5", "source-3"}) {
|
||||||
|
t.Fatalf("composition related locations = %v, want claimant order", got)
|
||||||
|
}
|
||||||
|
if got := []string{genericResult.Related[0].SourceID, genericResult.Related[1].SourceID}; !slices.Equal(got, []string{"source-3", "source-5"}) {
|
||||||
|
t.Fatalf("generic related locations = %v, want sorted order", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWriteDiagnosticsEscapesControls(t *testing.T) {
|
func TestWriteDiagnosticsEscapesControls(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml"
|
yamladapter "go.maximumdirect.net/configkit/internal/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errDiffInspectionUnavailable = errors.New("configkit: diff inspection unavailable")
|
var errDiffInspectionUnavailable = errors.New("configkit: diff inspection unavailable")
|
||||||
@@ -34,11 +34,11 @@ func prepareDiffRegions(left, right *Inspection) ([]diffRegion, error) {
|
|||||||
deltas := compareConfigurationValues(left.final, right.final)
|
deltas := compareConfigurationValues(left.final, right.final)
|
||||||
regions := make([]diffRegion, 0, len(deltas))
|
regions := make([]diffRegion, 0, len(deltas))
|
||||||
for _, delta := range deltas {
|
for _, delta := range deltas {
|
||||||
leftNode, leftPresent := documentAtPathWithLookup(left.final, delta.path, left.immutableLookup)
|
leftNode, leftPresent := documentAtPathWithLookup(left.final, delta.path, left.documentLookup())
|
||||||
if !leftPresent {
|
if !leftPresent {
|
||||||
leftNode = nil
|
leftNode = nil
|
||||||
}
|
}
|
||||||
rightNode, rightPresent := documentAtPathWithLookup(right.final, delta.path, right.immutableLookup)
|
rightNode, rightPresent := documentAtPathWithLookup(right.final, delta.path, right.documentLookup())
|
||||||
if !rightPresent {
|
if !rightPresent {
|
||||||
rightNode = nil
|
rightNode = nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package engine
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPreparePairedDiffSnapshotReportsWhollyMaskedChanges(t *testing.T) {
|
func TestPreparePairedDiffSnapshotReportsWhollyMaskedChanges(t *testing.T) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
type diffIntegrationConfig struct {
|
type diffIntegrationConfig struct {
|
||||||
@@ -29,7 +29,7 @@ func TestDiffUsesFinalValuesAndDependencyDerivedSensitivity(t *testing.T) {
|
|||||||
Resolve: func(configuration *diffIntegrationConfig, resolution *Resolution) error {
|
Resolve: func(configuration *diffIntegrationConfig, resolution *Resolution) error {
|
||||||
resolveCalls++
|
resolveCalls++
|
||||||
configuration.Derived = configuration.Dynamic["visible"].(string)
|
configuration.Derived = configuration.Dynamic["visible"].(string)
|
||||||
resolution.Record(Keys("derived"), ChangeNormalization, "derive from effective values", Keys("credential"))
|
resolution.RecordNormalization(Keys("derived"), "derive from effective values", Keys("credential"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -53,7 +53,7 @@ func TestDiffUsesFinalValuesAndDependencyDerivedSensitivity(t *testing.T) {
|
|||||||
!right.Inspection.secrets["/credential"] || !right.Inspection.secrets["/derived"] {
|
!right.Inspection.secrets["/credential"] || !right.Inspection.secrets["/derived"] {
|
||||||
t.Fatalf("typed or dependency-derived sensitivity was not retained: left=%#v right=%#v", left.Inspection.secrets, right.Inspection.secrets)
|
t.Fatalf("typed or dependency-derived sensitivity was not retained: left=%#v right=%#v", left.Inspection.secrets, right.Inspection.secrets)
|
||||||
}
|
}
|
||||||
if node, present := documentAtPathWithLookup(left.Inspection.final, Keys("derived"), left.Inspection.immutableLookup); !present || node.Text != "before" {
|
if node, present := documentAtPathWithLookup(left.Inspection.final, Keys("derived"), left.Inspection.documentLookup()); !present || node.Text != "before" {
|
||||||
t.Fatalf("left frozen derived value = %#v, present=%v", node, present)
|
t.Fatalf("left frozen derived value = %#v, present=%v", node, present)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,10 +138,10 @@ func TestDiffUsesReferenceNamesAndFrozenSecretState(t *testing.T) {
|
|||||||
if err != nil || len(differences) != 0 {
|
if err != nil || len(differences) != 0 {
|
||||||
t.Fatalf("credential rotation behind one reference = %#v, err = %v", differences, err)
|
t.Fatalf("credential rotation behind one reference = %#v, err = %v", differences, err)
|
||||||
}
|
}
|
||||||
if node, present := documentAtPathWithLookup(left.Inspection.final, Keys("optional"), left.Inspection.immutableLookup); !present || node.Text != "DIFF_ROTATING_SECRET" {
|
if node, present := documentAtPathWithLookup(left.Inspection.final, Keys("optional"), left.Inspection.documentLookup()); !present || node.Text != "DIFF_ROTATING_SECRET" {
|
||||||
t.Fatalf("frozen reference = %#v, present=%v", node, present)
|
t.Fatalf("frozen reference = %#v, present=%v", node, present)
|
||||||
}
|
}
|
||||||
if node, present := documentAtPathWithLookup(right.Inspection.final, Keys("optional"), right.Inspection.immutableLookup); !present || node.Text != "DIFF_ROTATING_SECRET" {
|
if node, present := documentAtPathWithLookup(right.Inspection.final, Keys("optional"), right.Inspection.documentLookup()); !present || node.Text != "DIFF_ROTATING_SECRET" {
|
||||||
t.Fatalf("right frozen reference = %#v, present=%v", node, present)
|
t.Fatalf("right frozen reference = %#v, present=%v", node, present)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ func TestDiffUsesReferenceNamesAndFrozenSecretState(t *testing.T) {
|
|||||||
if absent.Config.Optional != nil {
|
if absent.Config.Optional != nil {
|
||||||
t.Fatalf("missing optional reference decoded as %#v", absent.Config.Optional)
|
t.Fatalf("missing optional reference decoded as %#v", absent.Config.Optional)
|
||||||
}
|
}
|
||||||
if node, present := documentAtPathWithLookup(absent.Inspection.final, Keys("optional"), absent.Inspection.immutableLookup); !present || node.Kind != document.Null {
|
if node, present := documentAtPathWithLookup(absent.Inspection.final, Keys("optional"), absent.Inspection.documentLookup()); !present || node.Kind != document.Null {
|
||||||
t.Fatalf("optional nil projection = %#v, present=%v", node, present)
|
t.Fatalf("optional nil projection = %#v, present=%v", node, present)
|
||||||
}
|
}
|
||||||
differences, err = Diff(absent.Inspection, left.Inspection)
|
differences, err = Diff(absent.Inspection, left.Inspection)
|
||||||
@@ -203,14 +203,14 @@ func TestDiffProtectsHistoricalSensitiveRemovalAndReplacement(t *testing.T) {
|
|||||||
t.Run("removed historical value", func(t *testing.T) {
|
t.Run("removed historical value", func(t *testing.T) {
|
||||||
left := load("dynamic: {token: removed-canary}\n", func(configuration *configuration, resolution *Resolution) error {
|
left := load("dynamic: {token: removed-canary}\n", func(configuration *configuration, resolution *Resolution) error {
|
||||||
delete(configuration.Dynamic, "token")
|
delete(configuration.Dynamic, "token")
|
||||||
resolution.Record(secretPath, ChangeNormalization, "remove token")
|
resolution.RecordNormalization(secretPath, "remove token")
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
right := load("dynamic: {token: current-canary}\n", nil)
|
right := load("dynamic: {token: current-canary}\n", nil)
|
||||||
if !left.Inspection.secrets[secretPath.String()] {
|
if !left.Inspection.secrets[secretPath.String()] {
|
||||||
t.Fatal("removed historical selector was not retained")
|
t.Fatal("removed historical selector was not retained")
|
||||||
}
|
}
|
||||||
if _, present := documentAtPathWithLookup(left.Inspection.final, secretPath, left.Inspection.immutableLookup); present {
|
if _, present := documentAtPathWithLookup(left.Inspection.final, secretPath, left.Inspection.documentLookup()); present {
|
||||||
t.Fatal("removed token remains in the effective left tree")
|
t.Fatal("removed token remains in the effective left tree")
|
||||||
}
|
}
|
||||||
differences, err := Diff(left.Inspection, right.Inspection)
|
differences, err := Diff(left.Inspection, right.Inspection)
|
||||||
@@ -228,7 +228,7 @@ func TestDiffProtectsHistoricalSensitiveRemovalAndReplacement(t *testing.T) {
|
|||||||
t.Run("replaced historical value", func(t *testing.T) {
|
t.Run("replaced historical value", func(t *testing.T) {
|
||||||
left := load("dynamic: {token: original-canary}\n", func(configuration *configuration, resolution *Resolution) error {
|
left := load("dynamic: {token: original-canary}\n", func(configuration *configuration, resolution *Resolution) error {
|
||||||
configuration.Dynamic["token"] = "replacement-canary"
|
configuration.Dynamic["token"] = "replacement-canary"
|
||||||
resolution.Record(secretPath, ChangeNormalization, "replace token")
|
resolution.RecordNormalization(secretPath, "replace token")
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
right := load("dynamic: {token: other-canary}\n", nil)
|
right := load("dynamic: {token: other-canary}\n", nil)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package engine
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
type pairedDiffSnapshot struct {
|
type pairedDiffSnapshot struct {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package engine
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPreparePairedDiffSnapshotRedactsAlignedMappingFromEitherSide(t *testing.T) {
|
func TestPreparePairedDiffSnapshotRedactsAlignedMappingFromEitherSide(t *testing.T) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package engine
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPrepareDiffRegionsUsesCanonicalConfigurationComparison(t *testing.T) {
|
func TestPrepareDiffRegionsUsesCanonicalConfigurationComparison(t *testing.T) {
|
||||||
@@ -127,7 +127,10 @@ type expectedDiffRegion struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func diffTestInspection(root *document.Node) *Inspection {
|
func diffTestInspection(root *document.Node) *Inspection {
|
||||||
return &Inspection{final: root, immutableLookup: buildDocumentLookup(root)}
|
builder := newHistoryBuilder()
|
||||||
|
builder.model.final = root
|
||||||
|
model := builder.freeze()
|
||||||
|
return &Inspection{final: root, history: model, metadata: Metadata{historyModel: model}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertDiffRegionNode(t *testing.T, side string, node *document.Node, wantPresent bool, wantKind document.Kind) {
|
func assertDiffRegionNode(t *testing.T, side string, node *document.Node, wantPresent bool, wantKind document.Kind) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
const directiveKey = "configkit"
|
const directiveKey = "configkit"
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ func resolveSearchInput(source Source, sourceID string, helpers discoveryDirecto
|
|||||||
if len(diagnostics) == 0 || selected.kind != sourceFile {
|
if len(diagnostics) == 0 || selected.kind != sourceFile {
|
||||||
return selected, nil, diagnostics
|
return selected, nil, diagnostics
|
||||||
}
|
}
|
||||||
info := &SourceInfo{ID: sourceID, Label: selected.label, Path: selected.path, Layer: LayerBase, Kind: SourceKindFile}
|
info := &SourceInfo{ID: sourceID, Label: selected.label, Path: selected.path, Layer: LayerMain, Kind: SourceKindFile}
|
||||||
for index := range diagnostics {
|
for index := range diagnostics {
|
||||||
diagnostics[index].Primary = &Location{SourceID: sourceID}
|
diagnostics[index].Primary = &Location{SourceID: sourceID}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ settings: {underlay: value}
|
|||||||
Keys("settings", "main"), Keys("settings", "underlay"), Keys("settings", "imported"))
|
Keys("settings", "main"), Keys("settings", "underlay"), Keys("settings", "imported"))
|
||||||
|
|
||||||
sources := searched.Inspection.Sources()
|
sources := searched.Inspection.Sources()
|
||||||
if len(sources) != 3 || sources[0].ID != "source-1" || sources[0].Layer != LayerBase || sources[1].ID != "source-2" || sources[1].Layer != LayerUnderlay || sources[2].Path != canonicalTestPath(t, importPath) || sources[2].UnderlayIndex != 1 {
|
if len(sources) != 3 || sources[0].ID != "source-1" || sources[0].Layer != LayerMain || sources[1].ID != "source-2" || sources[1].Layer != LayerUnderlay || sources[2].Path != canonicalTestPath(t, importPath) || sources[2].UnderlayIndex != 1 {
|
||||||
t.Fatalf("layered search source evidence = %#v", sources)
|
t.Fatalf("layered search source evidence = %#v", sources)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package engine
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
"slices"
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package engine
|
package engine
|
||||||
|
|
||||||
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
import "go.maximumdirect.net/configkit/internal/document"
|
||||||
|
|
||||||
// documentLookup contains read-only mapping positions for a completed document.
|
// documentLookup contains read-only mapping positions for a completed document.
|
||||||
type documentLookup struct {
|
type documentLookup struct {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user