64 Commits

Author SHA1 Message Date
d818b08556 Record successful remote vanity verification 2026-11-01 14:24:36 +00:00
587f407a32 Migrate ConfigKit to its vanity module path 2026-11-01 14:19:37 +00:00
335bd7bc31 Settle the Forge publication endpoint 2026-11-01 14:04:15 +00:00
331195202e Document the vanity module migration roadmap 2026-11-01 14:02:28 +00:00
21c9bc0ad0 Retire completed roadmaps before the next release 2026-10-27 19:15:40 +00:00
68400ffdfb Fix source lookup tests with symlinked temporary directories 2026-10-27 18:42:19 +00:00
5d5d51ab11 Record the public API cleanup review 2026-10-27 16:25:33 +00:00
69bd9ead53 Complete the public API cleanup roadmap 2026-10-27 15:44:00 +00:00
2ff588eb64 Update guidance for the cleaned public API 2026-10-27 15:40:22 +00:00
c96197c417 Add detached source lookup to results and errors 2026-10-27 15:26:19 +00:00
24ab623cb2 Rename the retained history limit diagnostic 2026-10-27 15:18:57 +00:00
1ce147c9f6 Use main for the main provenance layer 2026-10-27 15:15:39 +00:00
9833ca3aa3 Replace generic resolution recording with intent methods 2026-10-27 15:12:38 +00:00
eafbd32489 Clarify search and secret bootstrap names 2026-10-27 15:09:04 +00:00
9ac0053590 Clarify import default field names 2026-10-27 15:07:15 +00:00
791cf16136 Simplify empty import selection evidence 2026-10-27 15:05:22 +00:00
31ae30372e Clarify source version evidence names 2026-10-27 15:01:54 +00:00
7ee9e92ef5 Clarify resolution metadata and sensitivity names 2026-10-27 15:00:04 +00:00
556b10b2cd Plan the public API cleanup 2026-10-27 14:55:22 +00:00
07e4ae5d92 Expand accepted API decisions into an implementation roadmap 2026-10-27 14:43:08 +00:00
7548fa101a Accept descriptive import default field names 2026-10-27 14:35:42 +00:00
197567177e Include failed-load source lookup in A1 2026-10-27 14:31:25 +00:00
7330107389 Include obsolete recording diagnostic removal in D6 2026-10-27 14:29:35 +00:00
ad6d3748d9 Accept history terminology for the retained evidence limit 2026-10-27 14:27:56 +00:00
1ee508ba02 Accept main terminology for provenance 2026-10-27 14:26:54 +00:00
e9155fced6 Accept the discovery path constructor rename 2026-10-27 14:25:18 +00:00
14c83a6515 Record additional public API review opportunities 2026-10-27 14:21:01 +00:00
3ebb848ead Accept direct source lookup on results 2026-10-27 14:10:51 +00:00
4289f9ff7f Record accepted public API cleanup decisions 2026-10-27 14:06:45 +00:00
a050986916 Catalog the public API for version one 2026-10-27 13:34:26 +00:00
fb71421956 Prepare sensitivity dependencies once per recorded operation 2026-10-27 03:18:55 +00:00
b281adb751 Index explanation selections once per query 2026-10-27 03:15:01 +00:00
1448f9b19d Deduplicate parsed sensitivity paths correctly 2026-10-27 03:12:21 +00:00
731149fc6d Localize history selection to affected paths 2026-10-27 03:10:07 +00:00
9ff873f54c Index stable history trees during normalization 2026-10-27 03:08:34 +00:00
3f389662e9 Keep explanation origins scoped to the query 2026-10-27 03:05:15 +00:00
906ca34b6d Avoid duplicate intermediate source history 2026-10-27 03:04:31 +00:00
17216a40f3 Preserve final dynamic secret tags in inspection 2026-10-27 03:02:17 +00:00
896c1cde08 Record additional history scaling findings 2026-10-27 03:00:14 +00:00
91769e9f9b Record interface remediation review findings 2026-10-27 02:52:47 +00:00
98c8b7c842 Complete interface contract remediation 2026-10-26 18:23:58 +00:00
1954052631 Preserve composition conflict claimant order 2026-10-26 18:17:04 +00:00
3ffaef7ede Remove superseded history attribution paths 2026-10-26 18:11:04 +00:00
6a1447c327 Reconcile retained history accounting 2026-10-26 17:46:31 +00:00
12fd0a7191 Publish inspection through one frozen history model 2026-10-26 17:36:36 +00:00
8155465322 Move sensitivity classification to shared history 2026-10-26 17:25:11 +00:00
408dd13c24 Integrate history prefixes into load handoffs 2026-10-26 17:15:30 +00:00
738c8a9cc7 Replay explanations through shared history selections 2026-10-26 17:01:32 +00:00
4b4e15fc58 Cut metadata history over to shared model 2026-10-26 16:34:13 +00:00
76521f7cd8 Complete historical inventory and query services 2026-10-26 16:09:32 +00:00
cacc558288 Normalize original resolution history evidence 2026-10-26 15:49:15 +00:00
231ae66857 Normalize typed CLI history evidence 2026-10-26 15:36:42 +00:00
da74d4190d Normalize inheritance history evidence 2026-10-26 15:25:02 +00:00
11f7154874 Normalize projected collection history evidence 2026-10-26 15:09:12 +00:00
3828b117e8 Normalize baseline and source history evidence 2026-10-26 15:01:54 +00:00
2419e63717 Add shape-aware history selection core 2026-10-26 14:47:18 +00:00
a893c7716c Preserve explicitness across resolution shape changes 2026-10-26 14:36:15 +00:00
28ef9b6200 Use entry metadata for resolution presence 2026-10-26 14:30:25 +00:00
0dd16aa8a9 Plan the interface remediation sprint 2026-10-26 14:24:07 +00:00
f6d54a094a Clarify shared history migration and sprint acceptance 2026-10-26 14:00:18 +00:00
f9ec66e867 Adopt shared history ownership in the interface roadmap 2026-10-26 12:36:17 +00:00
be5cdeb502 Plan interface contract remediation 2026-10-26 02:40:21 +00:00
1f64673d0d Record API contract audit findings 2026-10-26 01:36:20 +00:00
01b08e76a3 Retire the completed consolidated audit 2026-10-26 01:25:35 +00:00
224 changed files with 5619 additions and 3395 deletions

View File

@@ -11,7 +11,9 @@ 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.
validated configuration with detached metadata and safe diagnostics. Successful
results and load errors also provide passive `Source(id)` lookups alongside source
enumeration.
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
@@ -51,6 +53,8 @@ descriptions and pass `SearchConfig` to the existing load APIs. See the
and [ADR 0019](docs/adr/0019-explicit-configuration-discovery.md).
```go
import "go.maximumdirect.net/configkit"
type Config struct {
Enabled bool `yaml:"enabled"`
Workers int `yaml:"workers"`
@@ -65,6 +69,9 @@ func ReadConfig(path string) (Config, error) {
}
```
Existing consumers should follow the
[module-path migration guidance](docs/consumers/README.md#vanity-module-path-migration).
## Design philosophy
ConfigKit favors one coherent, predictable configuration model over flexibility

View File

@@ -1,6 +1,6 @@
package configkit
import "gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
import "go.maximumdirect.net/configkit/internal/engine"
func fromEngineLocation(value engine.Location) Location {
return Location{SourceID: value.SourceID, Line: value.Line, Column: value.Column}
@@ -46,21 +46,21 @@ func fromEngineSourceInfo(value engine.SourceInfo) SourceInfo {
return SourceInfo{
ID: value.ID, Label: value.Label, Path: value.Path, Destination: fromEnginePath(value.Destination),
PlacementMode: PlacementMode(engine.SourceInfoPlacementModeForFacade(value)),
DestinationUnresolved: engine.SourceInfoDestinationUnresolvedForFacade(value),
DestinationIsFallback: engine.SourceInfoDestinationUnresolvedForFacade(value),
Layer: Layer(value.Layer), Kind: SourceKind(value.Kind), UnderlayIndex: value.UnderlayIndex,
ImportedFrom: fromEngineLocationPointer(value.ImportedFrom), VersionCheck: fromEngineSourceVersionCheck(value.VersionCheck),
ImportSelections: fromEngineImportSelections(engine.SourceInfoImportSelectionsForFacade(value)),
ImportedFrom: fromEngineLocationPointer(value.ImportedFrom), VersionEvidence: fromEngineSourceVersionEvidence(value.VersionCheck),
EmptyImportSelections: fromEngineEmptyImportSelections(engine.SourceInfoImportSelectionsForFacade(value)),
}
}
func fromEngineImportSelections(values []engine.ImportSelectionForFacade) []ImportSelection {
func fromEngineEmptyImportSelections(values []engine.ImportSelectionForFacade) []EmptyImportSelection {
if values == nil {
return nil
}
result := make([]ImportSelection, len(values))
result := make([]EmptyImportSelection, len(values))
for index, value := range values {
result[index] = ImportSelection{
result[index] = EmptyImportSelection{
Path: fromEnginePath(value.Path), Location: fromEngineLocation(value.Location),
Selector: ImportSelector(value.Selector), Outcome: ImportSelectionOutcome(value.Outcome),
Selector: ImportSelector(value.Selector),
}
}
return result
@@ -82,18 +82,18 @@ func toEngineOverrides(values []Override) []engine.Override {
func toEngineImportDefaults(value ImportDefaults) engine.ImportDefaults {
return engine.ImportDefaults{
Required: value.Required,
Follow: value.Follow,
Hidden: value.Hidden,
Follow: value.FollowSymlinks,
Hidden: value.IncludeHidden,
Recursive: value.Recursive,
Pattern: value.Pattern,
}
}
func fromEngineSourceVersionCheck(value *engine.SourceVersionCheck) *SourceVersionCheck {
func fromEngineSourceVersionEvidence(value *engine.SourceVersionCheck) *SourceVersionEvidence {
if value == nil {
return nil
}
return &SourceVersionCheck{
return &SourceVersionEvidence{
Path: fromEnginePath(value.Path),
Version: Version{Present: value.Version.Present, Null: value.Version.Null, Text: value.Version.Text},
Location: fromEngineLocationPointer(value.Location), Redacted: value.Redacted,

View File

@@ -3,7 +3,7 @@ package configkit
import (
"io"
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
"go.maximumdirect.net/configkit/internal/engine"
)
// LoadError reports safe structured diagnostics for a failed configuration load.
@@ -50,6 +50,21 @@ func (e *LoadError) Sources() []SourceInfo {
return result
}
// Source returns a detached record from the sanitized source evidence retained
// before loading failed, including placement evidence available at that point.
// IDs are local to the failed load. It does not acquire sources or access the
// filesystem. Empty or unknown IDs return false.
func (e *LoadError) Source(id string) (SourceInfo, bool) {
if e == nil || e.value == nil {
return SourceInfo{}, false
}
value, ok := e.value.Source(id)
if !ok {
return SourceInfo{}, false
}
return fromEngineSourceInfo(value), true
}
// WriteDiagnostics writes diagnostics in canonical order. It escapes terminal
// control characters and propagates writer errors, including short writes.
func WriteDiagnostics(output io.Writer, diagnostics []Diagnostic) error {

View File

@@ -1,6 +1,6 @@
package configkit
import "gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
import "go.maximumdirect.net/configkit/internal/engine"
// Difference describes one canonical changed region between two successful
// loads. Left is the earlier value and Right is the later value. Each snapshot

View File

@@ -1,6 +1,6 @@
package configkit
import "gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
import "go.maximumdirect.net/configkit/internal/engine"
// Search is an immutable ordered description of possible host-file inputs.
// It is consumed only when passed to SearchConfig and used by a load.
@@ -52,8 +52,8 @@ func SearchDirectory(directory string, supplied ...DirectorySearchOptions) Searc
return Search{value: engine.SearchDirectory(directory, options...)}
}
// Paths describes literal host-file candidates in the supplied order.
func Paths(paths ...string) Search { return Search{value: engine.Paths(paths...)} }
// SearchPaths describes literal host-file candidates in the supplied order.
func SearchPaths(paths ...string) Search { return Search{value: engine.Paths(paths...)} }
// Searches concatenates descriptions in order and snapshots their entries.
func Searches(searches ...Search) Search {

View File

@@ -46,9 +46,9 @@ literal single filename components; it does not expand application-name
placeholders. An application slug is required only when a selected directory or
filename group uses it. Literal custom paths are not expanded for `~`, variables,
or wildcards; relative paths are resolved from the working directory at load time.
The zero `Search`, `Searches()`, `Paths()`, and an empty custom filename group
contribute no candidates. An explicitly supplied empty path or filename is
invalid. Slugs and custom filenames must be usable single host filename
The zero `Search`, `Searches()`, `SearchPaths()`, and an empty custom filename
group contribute no candidates. An explicitly supplied empty path or filename
is invalid. Slugs and custom filenames must be usable single host filename
components; ConfigKit preserves their case and spelling.
Search expansion is ordered concatenation. For each directory search, ConfigKit
@@ -172,6 +172,18 @@ the returned configuration or inspection records cannot alter ConfigKit's privat
snapshot. ConfigKit performs no logging. `Result.WriteEffective` exposes only the
frozen redacted projection.
`Result.Source(id)` returns the detached record for an acquired source already
listed by `Result.Sources()`. Source IDs are local to one load; empty and unknown
IDs return false. This passive lookup uses retained evidence and does not reacquire
the source, access the filesystem, or look up credentials. A source ID can be
obtained from the source records or exact origin locations.
`LoadError.Source(id)` performs the same lookup over the partial, already-sanitized
inventory returned by `LoadError.Sources()`. It preserves the source evidence and
placement available when loading failed, which may be incomplete; it does not
promise a record for every attempted source. Nil or zero-valued errors and empty
or unknown IDs return false.
### Completion and resource lifetime
`Load` is synchronous and complete on return. Success means input assembly,
@@ -278,7 +290,7 @@ the application root and are not moved beneath the main input's destination.
`SourceInfo.Destination` reports each acquired source's effective mapping or
sequence-container path, including imports. `PlacementMode` reports how that
source was placed; mapping is the zero value, and CLI/overlay sources report
mapping unless item placement was applied. `DestinationUnresolved` is true only
mapping unless item placement was applied. `DestinationIsFallback` is true only
when an enclosing item index needed to name the source's destination has not yet
been assigned. In that case `Destination` reports the nearest resolved enclosing
collection container and omits the unresolved local suffix. A source's own item
@@ -445,10 +457,10 @@ successful result. Callback panics remain programming failures.
`Options.SourceVersionMode` and `LayeredOptions.SourceVersionMode` select how
one `Definition.Version` policy applies across source documents. The zero value
`SourceVersionModeDefault` preserves the existing main-only check at its
effective path and existing protected-path behavior. `SourceVersionModeIfPresent`
checks each participating document that declares a version and permits absent
non-main declarations. `SourceVersionModeRequired` rejects a participating
`SourceVersionModeMainOnly` (the zero value) preserves the existing main-only
check at its effective path and existing protected-path behavior.
`SourceVersionModeIfPresent` checks each participating document that declares a
version and permits absent non-main declarations. `SourceVersionModeRequired` rejects a participating
document without a declaration. Enabled modes require a usable `VersionRule`
and checker; an unknown mode or unusable rule is an options error before source
access or callbacks.
@@ -485,7 +497,7 @@ declaration locations use source-local coordinates; ordinary payload
diagnostics continue to use effective paths. Default-mode diagnostics are
unchanged.
`SourceInfo.VersionCheck` is nil when enabled declaration processing did not
`SourceInfo.VersionEvidence` is nil when enabled declaration processing did not
occur. Otherwise it records a source-local `Path`, the observed `Version` state
(`Present`, `Null`, and, when disclosed, exact `Text`), an actual or nearest
available `Location`, and whether text was redacted. A non-nil record with
@@ -561,7 +573,7 @@ reports it absent. Queries return detached records.
### Secret classification and effective rendering
Secret classification combines `configkit:"secret"` field tags with the optional
pure `Definition.Secret(Path) bool` predicate. Tags on concrete structs inside
pure `Definition.Sensitive(Path) bool` predicate. Tags on concrete structs inside
supported dynamic values are retained in typed projections, including superseded
defaults and pre-resolution values. A tagged or predicate-classified
container makes all descendants sensitive. ConfigKit evaluates final and historical
@@ -624,7 +636,7 @@ writes remain visible. When structural replay differs from the pre-CLI typed inp
a `ChangeDecoding` event records retained defaults and canonical decoding. CLI
contributions then retain their raw input representation while effective and
resolution values use the typed projection. This built-in inspection operation is
not accepted by `Resolution.Record`.
not exposed as a manual recording method.
Base, overlay, and CLI history preserves the policy-validated input representation,
including a superseded value that could not have decoded into the final Go field.
@@ -659,11 +671,11 @@ unredacted mode.
Retained history consists of immutable source applications and audited subtree
deltas, rather than a full tree copy per field. Shared nodes count once toward a
4,000,000-node retained-history limit; exceeding it fails loading with
`explain.limit_exceeded`. Replaying one requested path is performed on demand, and
`history.limit_exceeded`. Replaying one requested path is performed on demand, and
the returned YAML strings and nested records cannot mutate later explanations.
Application commands should render the separate `SecretsReport` immediately after
an explicit `LoadSecretsDir` bootstrap, then use `WriteDiagnostics`,
an explicit `LoadSecretsIntoEnvironment` bootstrap, then use `WriteDiagnostics`,
`WriteEffective`, or `WriteExplanation` on the later load result as appropriate.
ConfigKit does not infer bootstrap outcomes from a secret reference and does not
bundle these independent reports into a command framework.
@@ -758,11 +770,12 @@ reference state without printing its value.
## Explicit secrets-directory bootstrap
`LoadSecretsDir(directory)` is a separate, opt-in startup operation. It is never
called by `Load`, is not a YAML option, and does not participate in configuration
validation. Applications choose the native directory path and should invoke the
operation once before starting concurrent work. The selected path may itself be a
symlink; ConfigKit resolves it to an existing canonical directory before examining
`LoadSecretsIntoEnvironment(directory)` is a separate, opt-in startup operation.
It is never called by `Load`, is not a YAML option, and does not participate in
configuration validation. Applications choose the native directory path and
should invoke the operation once before starting concurrent work. The selected
path may itself be a symlink; ConfigKit resolves it to an existing canonical
directory before examining
its immediate entries.
The bootstrap enumerates at most 1,024 entries, including dot-prefixed entries.
@@ -802,7 +815,7 @@ per-load `Resolution` context. The context's
`IsExplicit` query uses surviving input presence, so explicit false, zero, empty, and
null are never confused with absence or static defaults.
`Resolution.InputInfo(path)` returns detached effective metadata from the fixed
`Resolution.EntryInfo(path)` returns detached effective metadata from the fixed
post-decode entry state, including explicit CLI overrides, before application
resolution runs. It includes retained static defaults, preserves explicit null, and
uses the same exact semantic path matching as `Metadata.Lookup`; missing,
@@ -825,18 +838,20 @@ exit; changing them cannot affect later queries, results, or another load.
`Default` assigns and records a derived value only when its path is not explicit.
`Normalize` always assigns and records. For transformations performed directly by
application code, `Resolution.Record` declares a derived-default or normalization
operation; a recorded container covers changes to its descendants. Records require
a schema-valid path, valid kind, safe nonempty reason, and schema-valid dependency
paths. Repeated records at one path are coalesced: reasons and dependencies retain
their first occurrence, the last helper value is authoritative, and any normalization
makes the combined operation a normalization.
application code, `Resolution.RecordDerivedDefault` and
`Resolution.RecordNormalization` declare the operation already performed; a
recorded container covers changes to its descendants. These methods do not assign
values, and a derived-default record does not suppress work for explicit input.
Records require a schema-valid path, safe nonempty reason, and schema-valid
dependency paths. Repeated records at one path are coalesced: reasons and
dependencies retain their first occurrence, the last helper value is authoritative,
and any normalization makes the combined operation a normalization.
ConfigKit projects the configuration before and after the callback. Every net change
must have an exact or ancestor record. Helper-assigned values must agree with the
final value at their declared path, which supports ordinary map-entry copy/writeback
while detecting forgotten writeback or unrelated targets. Invalid paths, kinds,
values, records, helper targets, and unrecorded mutations fail the load. Equal-valued
while detecting forgotten writeback or unrelated targets. Invalid paths, values,
records, helper targets, and unrecorded mutations fail the load. Equal-valued
derived assignments may still record application intent even when there is no
structural change.
@@ -1214,6 +1229,8 @@ replaces rather than extends the default pattern. Nil `Pattern` leaves directory
filenames unrestricted; a non-nil empty slice selects none. Defaults do not make
a field applicable to selectors that do not support it, and load calls snapshot
the supplied pointers and slice before source acquisition.
The Go fields are named `FollowSymlinks` and `IncludeHidden`; the YAML keys
remain `follow` and `hidden`.
A source's `configkit.sequences` declarations apply independently of import
enablement; a `configkit.imports` declaration still requires `Options.Imports`.
@@ -1249,15 +1266,15 @@ or glob selection fail with `import.missing`; it also makes missing named paths
fail. Requiredness does not suppress invalid paths, containment failures,
directories selected as files, permissions, parse errors, or composition errors.
For inspection, `SourceInfo.ImportSelections` records one outcome for each
completed optional declaration whose entire selected set is empty. The record is
attached to its declaring source, keeps the declaration path local to that source,
identifies the declaration mapping (including its `SourceID`) and selector, and
uses outcome `none`. Records remain in declaration order and are available
through the existing result, load
error, and callback source snapshots. They do not enumerate candidates, explain
For inspection, `SourceInfo.EmptyImportSelections` records each completed optional
declaration whose entire selected set is empty. The record is attached to its
declaring source, keeps the declaration path local to that source, and identifies
the declaration mapping (including its `SourceID`) and selector. Records remain
in declaration order and are available
through the existing result, load-error, and callback source snapshots. They do
not enumerate candidates, explain
why a path was absent, or describe partial omissions. Empty `path: []` succeeds
regardless of requiredness: it produces a `none` record when effectively optional,
regardless of requiredness: it produces a record when effectively optional,
and no record when effectively required. Snapshots detach the record paths and
slices.

View File

@@ -6,6 +6,9 @@ applications. Consumers still own their configuration structs, defaults, semanti
validation, schema-version policy, CLI parsing, runtime matching and conversion,
resource checks, remote acquisition, and service initialization.
Import the public root package as `"go.maximumdirect.net/configkit"`; its package
name remains `configkit`.
## Choosing a loading entry point
- Use [`Load`](../../lifecycle.go) with one source for an ordinary configuration.
@@ -99,15 +102,20 @@ records why discovery is explicitly selected.
`Options.ImportDefaults` and `LayeredOptions.ImportDefaults` apply one
load-wide policy to enabled main, underlay, and transitive imports. Each field
is resolved independently: a declaration's explicit value wins, then the
application default, then the library default. `Required`, `Follow`, `Hidden`,
and `Recursive` use nil pointers for unspecified values. `Pattern` applies to
application default, then the library default. `Required`, `FollowSymlinks`,
`IncludeHidden`, and `Recursive` use nil pointers for unspecified values.
`Pattern` applies to
directory searches: nil leaves filenames unrestricted, while a non-nil empty
slice selects no files. A declaration's own `pattern` replaces the application
default. The defaults do not enable imports or change selected-overlay behavior.
The Go fields `FollowSymlinks` and `IncludeHidden` correspond to the unchanged
YAML keys `follow` and `hidden`.
`Result.Sources`, `LoadError.Sources`, and `Resolution.Source` expose each
declaring source's `ImportSelections`. These records identify the source-local
declaration path, its authored location and selector, and the `none` outcome;
declaring source's `EmptyImportSelections`. `Result.Source(id)` and
`LoadError.Source(id)` retrieve one retained record directly when its load-local
ID is known. These records identify the source-local declaration path, its
authored location and selector;
they do not include candidate names or a guessed reason for absence. Returned
records are detached snapshots. The [configuration contract](../config.md#explicit-imports)
owns selector applicability and exact absence semantics.
@@ -117,7 +125,7 @@ owns selector applicability and exact absence semantics.
[`LoadVersionedService`](../../examples/consumers/source_version.go) demonstrates
if-present and required modes with one `Definition.Version` rule across a mounted
main input and an underlay. The example reads only sanitized
`SourceInfo.VersionCheck` fields; its executable test is in
`SourceInfo.VersionEvidence` fields; its executable test is in
[`layered_test.go`](../../examples/consumers/layered_test.go). The
[configuration contract](../config.md#shared-source-version-checks) defines
participation, source-local lookup and diagnostics, non-main header consumption,
@@ -264,7 +272,7 @@ conversion API.
## Source-aware application path policy
[`LoadNotariusPaths`](../../examples/consumers/consumers.go) is the maintained
path-bearing example. Its application resolver uses `Resolution.InputInfo` and
path-bearing example. Its application resolver uses `Resolution.EntryInfo` and
`Resolution.Source` before normalizing: relative CLI input uses an explicitly
supplied invocation directory, retained defaults use the canonical main source,
and a file-authored scalar uses its one declaring source. Absolute paths remain
@@ -301,10 +309,67 @@ The examples preserve domain meaning while adopting one configuration convention
parsers translate only explicitly set configuration flags into `Override` records.
Separate invocation state remains outside configuration.
- Credential fields migrate to environment-name `Secret` references. Applications
explicitly call `LoadSecretsDir` before `Load` when they choose file bootstrap;
explicitly call `LoadSecretsIntoEnvironment` before `Load` when they choose
file bootstrap;
blocked empty variables remain blocked, and ordinary `Load` never changes the
process environment.
### Vanity module path migration
ConfigKit's module identifier changed from
`gitea.maximumdirect.net/eric/ConfigKit` to `go.maximumdirect.net/configkit` before
v1.0.0. Update the dependency requirement in your application's `go.mod` and its
Go imports together, selecting a revision or release that declares the new module
path. The package name remains `configkit`; the configuration model and public
symbol names are unchanged by this identity migration.
Old and new import paths identify distinct Go packages, including distinct named
types. Coordinate adoption with any dependencies that exchange ConfigKit values
with your application; changing a requirement alone does not make those types
interchangeable. 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. Consumers that compare literal strings
or keep serialized diagnostic/provenance expectations may need to update those
alongside Go references. This repository guidance does not claim that downstream
applications have migrated and does not select a release version for this
breaking pre-v1 change. `ChangeKind` and its inspection constants remain; use
`Default` and `Normalize` when the helper should both assign and record a value.
### Selected-overlay migration
To adopt the current selected-overlay API, rename `Options.Profile`,
@@ -330,7 +395,7 @@ focused behavioral owner:
| `Load`, `NoSource`, validation, versions, resolution, `SecretEnv`, overrides, and explanation | Executable examples in [`example_test.go`](../../example_test.go) |
| Source/import/selected-overlay pipeline rules and artifact defaults | Narratio-shaped executable example and fixture |
| Canonical module objects and dynamic option handling | WeatherReporter- and Notarius-shaped examples and migration tests |
| `LoadSecretsDir`, bootstrap reporting, typed lookup, CLI explanation | `Example_weatherStartup` |
| `LoadSecretsIntoEnvironment`, bootstrap reporting, typed lookup, CLI explanation | `Example_weatherStartup` |
| Native filesystem, grouped underlays, programmatic mapping, retained resource context | `Example_nativeFilesystem`, `Example_layeredCatalog`, and their focused consumer tests |
| Literal input destinations, root-default compatibility, and mounted peer collections | `Example_inputDestination`, `LoadCollectionBundle`, and `destinations_test.go` |
| Named sibling inheritance, presence, and derivation | `Example_inheritedPresets` and `TestPresetCatalogOneLoadAndDerivation` |

View File

@@ -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 |
| 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 |
| 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 |
| Examples or copyable assets | The owning implemented contract and [consumer adoption guide](consumers/README.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).
The [consolidated audit review](roadmap/audit.md) owns the open findings from
the nine 2026-10-25 audits, their evidence, and settled scope decisions. The
[remediation roadmap](roadmap/roadmap.md) owns the combined target state and
high-level dependency order. The original reports are preserved at `ecd5d5d`
under `docs/roadmap/`.
The completed audit-remediation, internal-boundary simplification, shared-history,
and public API cleanup roadmaps, reviews, and resolved findings are preserved at
`68400ff` under `docs/roadmap/`. For example:
```sh
git show 68400ff:docs/roadmap/api.md
git show 68400ff:docs/roadmap/remediation.md
```
Their earlier completed implementation plans are preserved at
`bb5540b:docs/roadmap/implementation.md` (audit remediation),
`f6d54a0:docs/roadmap/implementation.md` (internal-boundary simplification), and
`07e4ae5:docs/roadmap/implementation.md` (shared history). The plan at
`68400ff:docs/roadmap/implementation.md` records the public API cleanup.
Current behavior and migration information belong to the
[configuration contract](config.md), public declaration comments, and
[consumer guide](consumers/README.md#pre-v1-public-api-cleanup).
The consolidated audit, including
findings, evidence, and settled scope decisions, is preserved at
`453da68:docs/roadmap/audit.md`; the original nine reports are preserved at
`ecd5d5d` under `docs/roadmap/`. Retrieve the consolidated record with:
```sh
git show 453da68:docs/roadmap/audit.md
```
Install the Go version required by `go.mod` or a compatible newer toolchain.
Acquire declared dependencies explicitly before entering an offline

View File

@@ -151,6 +151,27 @@ descriptor caching and type policy, and
inline field assembly. [`decode.go`](../../internal/engine/decode.go) consumes
those descriptors for typed decoding.
History and inspection use one private evidence owner. The
[`history_model.go`](../../internal/engine/history_model.go) builder receives
baseline, source-composition, inheritance, typed-checkpoint, and CLI evidence
in [`preparePostDecodeOverrideContext`](../../internal/engine/override_context.go),
before application resolution begins. The callback retains its separate fixed
entry view; it does not read a builder that continues to change. After successful
resolution auditing, [`resolveLoadInput`](../../internal/engine/load_result.go)
adds the original resolution records and their net replay groups. At the existing
pre-secret admission boundary, [`prepareInspectionHistory`](../../internal/engine/load_result.go)
freezes that builder and counts the retained document roots before secret lookup
and validation.
The frozen model is shared by `Metadata` and `Inspection`. Its shape-aware
selection in [`history_select.go`](../../internal/engine/history_select.go)
drives metadata history, explanation, and historical-path membership; the model
also supplies historical diagnostic locations and sensitivity/dependency
evidence. `Inspection` replays selected payloads to produce detached, safely
rendered values, while metadata queries return detached attribution records.
History and explanation behavior is covered by focused `history_*_test.go`
tests alongside metadata, secret, inheritance, and facade integration tests.
Behavioral ownership follows the same handoffs: imports and grouped lifetimes are
covered by `imports_test.go`, `load_request_test.go`, and focused source,
discovery, and destination tests; inheritance by the inheritance test files;

View File

@@ -7,10 +7,10 @@ container image, generated package, or separate registry publication.
## Release identity and support
- Module: `gitea.maximumdirect.net/eric/ConfigKit`
- Module: `go.maximumdirect.net/configkit`
- Primary branch: `main`
- Authoritative remote: `origin`
- Authoritative push URL: `https://gitea.maximumdirect.net/eric/ConfigKit.git`
- Authoritative push URL: `ssh://git@forge.maximumdirect.net:22222/base-layer/configkit.git`
- Tags: stable Semantic Versions such as `v0.5.0`; no prereleases
- Minimum Go version: the first stable patch of the selected Go series, or a
higher patch when required by a dependency or specific correctness fix, as
@@ -24,6 +24,9 @@ Release validation runs one offline race-enabled Go suite, one full vet pass,
and CGO-disabled package compilation for both Linux targets. macOS and CI are
not release gates; do not wait for or poll CI.
The vanity module path identifies Go imports. Source/tag browser links and public
Go retrieval use the Forge HTTPS repository independently of SSH publication.
## 1. Select Go and prepare the tool
Use a clean checkout with Git, Bash 3.2 or newer, and a native compiler for race
@@ -91,7 +94,7 @@ executable first on `PATH` through the publication command.
Verify access to the authoritative remote without displaying credentials:
```sh
git ls-remote https://gitea.maximumdirect.net/eric/ConfigKit.git HEAD refs/heads/main
git ls-remote ssh://git@forge.maximumdirect.net:22222/base-layer/configkit.git HEAD refs/heads/main
```
Publication requires explicit authorization. Local implementation commits do
@@ -111,7 +114,7 @@ Create `docs/releases/<version>.md`. Its first line must be exactly
`# ConfigKit <version>`, and it must end with the license and attribution footer:
```markdown
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/VERSION/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/VERSION/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/VERSION/docs/oss/attribution.md)
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/configkit/src/tag/VERSION/COPYRIGHT) | [LICENSE](https://forge.maximumdirect.net/base-layer/configkit/src/tag/VERSION/LICENSE) | [ATTRIBUTION](https://forge.maximumdirect.net/base-layer/configkit/src/tag/VERSION/docs/oss/attribution.md)
```
Replace `VERSION` with the full tag and confirm the three target files exist in
@@ -127,7 +130,7 @@ Push only that committed candidate to the literal authoritative URL. This does
not follow local tags:
```sh
git push --no-follow-tags https://gitea.maximumdirect.net/eric/ConfigKit.git HEAD:refs/heads/main
git push --no-follow-tags ssh://git@forge.maximumdirect.net:22222/base-layer/configkit.git HEAD:refs/heads/main
```
ReleaseKit requires authoritative `main` to equal this candidate before it
@@ -228,8 +231,8 @@ consumer_dir=$(mktemp -d)
(
cd "$consumer_dir"
go mod init example.invalid/configkit-release-check
GOPRIVATE=gitea.maximumdirect.net GOPROXY=direct go get "gitea.maximumdirect.net/eric/ConfigKit@$release_version"
go list -m -f '{{.Path}} {{.Version}}' gitea.maximumdirect.net/eric/ConfigKit
GOWORK=off GOPROXY=direct go get "go.maximumdirect.net/configkit@$release_version"
GOWORK=off go list -m -f '{{.Path}} {{.Version}}' go.maximumdirect.net/configkit
)
```

View File

@@ -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.

View File

@@ -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
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
Status: deferred. The implemented
@@ -91,3 +83,22 @@ evidence are preserved in Git history at `8e91a2b` under `docs/roadmap/`.
across loads without introducing aliasing?
- What cache lifetime and concurrency design would make schema reuse worthwhile
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.

View File

@@ -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.

View File

@@ -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.

268
docs/roadmap/migration.md Normal file
View File

@@ -0,0 +1,268 @@
# Vanity Module Path Migration
Status: repository implementation and exact-revision remote consumer verification
complete.
Planning baseline: `21c9bc0ad0315c4ac96268b487a52376558a18d7`.
This roadmap defines the target and work needed to adopt
`go.maximumdirect.net/configkit` before ConfigKit's initial v1.0.0 release.
It is not a staged implementation plan or authorization to push or publish.
The [configuration contract](../config.md), public declaration comments, and
[release policy](../policy/release.md) continue to own behavior outside the
module-identity migration.
## Purpose And Scope
Give ConfigKit a stable, project-owned Go module identifier independent of its
repository host, while retaining its existing package name, public facade,
configuration behavior, and implementation boundaries.
The migration changes the module path and therefore Go package/type identities.
It also brings ConfigKit's release endpoint references into agreement with its
current Forge hosting. These are related identity updates, not changes to the
loading lifecycle, YAML syntax, composition, provenance, history, or secrets.
No runtime API redesign or compatibility-shim system is needed for this scope.
## Current Baseline
| Area | Baseline | Target |
| --- | --- | --- |
| Module declaration | `gitea.maximumdirect.net/eric/ConfigKit` in [go.mod](../../go.mod) | `go.maximumdirect.net/configkit` |
| Self-imports | Old module prefix throughout production packages, tests, benchmarks, and examples | New module prefix with the same package layout |
| Public package | Root-owned `configkit` facade over private internal packages | Same name and ownership under the new module identifier |
| Vanity discovery | Root and nested requests already advertise the Forge repository | Existing discovery continues to locate the migrated module |
| Git remote | `origin` uses `ssh://git@forge.maximumdirect.net:22222/base-layer/configkit.git` | Retain this SSH endpoint and align release configuration with it |
| Release configuration | [releasekit.json](../../releasekit.json), its test, and [runbook](../release.md) still name the old Gitea HTTPS endpoint | Consistent Forge publication references and vanity-path consumer commands |
Path-sensitive test expectations include
[import_boundary_test.go](../../import_boundary_test.go),
[facade_candidate_test.go](../../facade_candidate_test.go), and
[releasekit_profile_test.go](../../releasekit_profile_test.go). Compilation alone
will not identify every stale literal: a boundary test using the old prefix can
silently stop checking the intended imports.
### Preliminary External Verification
Checks against the baseline, using Go 1.26.10 and disposable modules/caches
outside the repository, established:
- HTTPS discovery at the module root and a nested package path returns a
`go-import` mapping from `go.maximumdirect.net/configkit` to
`https://forge.maximumdirect.net/base-layer/configkit`.
- Anonymous HTTPS Git access returns the expected `main` revision and release
tags, including v0.10.2.
- Go resolves and downloads v0.10.2 through direct retrieval and the standard
public-proxy/fallback configuration, with checksum verification enabled.
- A disposable consumer's `go get go.maximumdirect.net/configkit@v0.10.2`
reaches the repository but rejects its old module declaration. That is the
expected remaining mismatch, not a discovery failure.
These observations demonstrate that discovery and retrieval infrastructure work;
they do not establish that the migrated module builds or can be installed yet,
nor promise continuing external-service availability. No vanity-service change
was identified as necessary by these checks.
## Targeted End State
### Canonical Go Identity
The module declares `go.maximumdirect.net/configkit`. Production imports,
external-package tests, maintained examples, and path-sensitive expectations use
that prefix consistently. The package name remains `configkit`, and the root
remains the sole supported consumer import surface. Existing internal packages
stay internal; the migration does not expose them or change dependency direction.
The v1 module uses the unsuffixed path, with ordinary root-level release tags such
as `v1.0.0`. There is no `/v1` import suffix or module subdirectory to introduce.
The [Go module reference](https://go.dev/ref/mod#major-version-suffixes) owns the
language/toolchain conventions behind that choice.
Existing dependency and tool module identities are separate from ConfigKit's
identity. In particular, this work does not automatically rename or upgrade the
ReleaseKit requirement/tool declaration because the vanity service also advertises
a ReleaseKit path. Any dependency-path change should be assessed separately
against that upstream module's declared identity and available versions. The
current Go minimum and dependency versions are not selected for change here.
### Consumer Migration
Current installation/import examples identify the vanity path. The consumer guide
explains updating the module requirement and Go imports together, including
transitive libraries that exchange ConfigKit types with an application. Packages
loaded under the old and new prefixes have different Go identities; changing a
requirement alone does not make those types interchangeable.
This is a coordinated pre-v1 identity change. Old tags keep their original module
declarations and contents. The migration does not rewrite them to make historical
versions installable under the new path. Requesting an older tag through the
vanity identifier can still produce the observed module-declaration mismatch
even after this migration; consumers need a revision declaring the new path.
Maintained examples in this repository migrate with the library. Updating other
applications or maintaining a separate old-path compatibility module is outside
this effort; neither is required to declare the repository changes complete.
### Repository And Publication Identity
The vanity path identifies Go packages. Forge URLs identify the source repository,
browser pages, and publication transport. They remain distinct: Go HTTPS discovery
can use the public Forge HTTPS repository while maintainers push through SSH.
The authoritative publication endpoint remains
`ssh://git@forge.maximumdirect.net:22222/base-layer/configkit.git`. Retaining the
existing SSH `origin` avoids an unnecessary transport/credential change.
ReleaseKit's exact `expected_push_url`, its existing profile test, and the
runbook's literal publication/recovery commands agree with that endpoint.
Browser/tag links in current release guidance refer to the Forge HTTPS project,
not to the SSH transport or vanity import path. Public Go retrieval continues
using the verified Forge HTTPS repository independently. Authenticated write
capability still belongs to ordinary authorized release preparation; anonymous
HTTPS read success does not prove push access.
The existing release sequence, validation profile, permissions, immutable tags,
and successful-publication completion point are unchanged. This migration does
not introduce CI waiting, proxy-propagation waiting, native macOS checks, or
consumer installation as permanent release gates.
## Work Needed From The Baseline
### Module Declaration, Imports, And Tests
Update the module directive and all ConfigKit self-imports as one coordinated
change across root wrappers, engine/document/YAML packages, tests, benchmarks,
and maintained examples. Preserve the existing package layout and public symbol
names. Do not mechanically replace every old-host URL: dependency identities,
upstream attribution, and historical references have different owners.
Review import-boundary predicates, reflection package-path assertions, and any
other literal module checks alongside imports. Retain their existing protection
with the new identity rather than removing assertions to make the migration pass.
Review module metadata/tidiness as appropriate after changing the module path,
without using that review to bundle unrelated upgrades or tool migrations.
### Release Configuration And Current Guidance
Align `releasekit.json`, its test, and the runbook's publication/recovery commands
with the retained SSH endpoint. Update the runbook's module identity, HTTPS source
links, and consumer verification examples accordingly. Reflect only necessary
endpoint wording changes in the adopted release policy if its current text
requires them; its release guarantees and gates remain the same.
Review README, declaration documentation, the consumer guide, and applicable
development/internal documentation for current identity references. Describe the
consumer import/type-identity migration in the existing consumer migration
guidance. New release documentation, when prepared through the normal procedure,
should explain the change and use version-matched Forge source/license links.
Preserve historical release records, accepted ADR decision content, revision-pinned
audit evidence, upstream provenance links, and third-party module paths where
they describe their original material. A historical old-host reference is not an
unmigrated self-import. This roadmap does not authorize rewriting that history or
changing licensing arrangements.
### Proportionate Verification
Use the existing [development checks](../development.md#baseline-validation)
and [testing policy](../policy/testing.md). Verify the migrated module builds,
the default suite and maintained examples pass, and public/dependency boundary
checks still enforce the root/internal separation under the new prefix. The
eventual release retains its existing validation, rather than acquiring a second
validation framework or an expanded platform matrix.
When the migrated commit is available remotely, a one-time disposable consumer
check should retrieve that exact revision through the vanity path, without a
local replacement or workspace override, and compile a small public-API use.
An exact revision before tagging can exercise the migration without creating a
test release. Report direct retrieval and default proxy/fallback results
separately where both are checked; do not infer a successful install from
`go list` or archive download alone.
This remote check is migration evidence when the revised commit is available,
not authorization to push and not a new publication-completion gate. Keep network
checks outside the deterministic default suite, avoid polling external services,
and preserve the policy that successful tag publication completes a release.
After publication, retrieval of the exact released version remains optional and
non-gating under the existing procedure. Record any unavailable verification
honestly instead of claiming that baseline retrieval validates migrated code.
## Completion Criteria
- The repository consistently declares and imports the vanity module path,
while preserving the package name, facade ownership, and configuration behavior.
- Current documentation, examples, path-sensitive tests, and Forge publication
references agree with the implemented identities.
- Applicable local checks pass, without unrelated dependency or contract changes.
- Migration evidence distinguishes the preliminary infrastructure checks from
any later successful consumer installation of the actual migrated revision.
- Historical releases remain unchanged, and downstream migration guidance
explains both import changes and distinct Go type identities.
Repository implementation completion, external retrieval evidence, downstream
application adoption, and release publication are separate outcomes. No push,
tag, or downstream change is authorized merely by completing this roadmap.
No significant open decision remains for this migration.
## Implementation And Verification
The module declaration and all ConfigKit self-imports now use
`go.maximumdirect.net/configkit`, including internal packages, external-package
tests, maintained examples, and benchmarks. The import-boundary predicates and
root-type reflection expectations use the new prefix. Review of every changed
Go hunk confirmed only the intended module-prefix replacements and publication
URL expectation; no loading behavior or public symbol names changed.
`releasekit.json`, its profile test, and the runbook now use the retained Forge
SSH publication endpoint, matching the existing effective `origin` push URL.
The runbook distinguishes that transport from Forge HTTPS browser links and
vanity-path consumer retrieval. README and the consumer guide identify the new
import path; migration guidance covers named-type identity and historical tags.
The Go minimum, dependency/tool versions, and `go.sum` are unchanged. Historical
release records, accepted ADRs, adopted policies, and upstream attribution were
left intact. Remaining old ConfigKit path references describe the migration's
baseline, consumer upgrade instructions, or historical releases.
Local validation passed on Linux with Go 1.26.10 and cached dependencies:
```sh
GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off ./scripts/check.sh
GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go mod tidy -diff
GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test -race ./...
```
The everyday check included maintained examples, documentation links, root type
ownership, dependency boundaries, and the release profile. Module/package
inspection reported the new main-module identity and no old ConfigKit imports
in the assembled test dependency graph. The module-selected ReleaseKit built
outside the checkout and reported v0.4.2. Module tidiness produced no diff.
### Remote Consumer Verification
With subsequent user authorization, migration commit
`587f407a32dec1e00d2aa17d2790c293d994721d` was pushed to authoritative `main`
through the retained Forge SSH endpoint. No release tag was created or published.
Two disposable consumer modules outside the repository retrieved that exact
revision through `go.maximumdirect.net/configkit`, using separate fresh module
caches and Go 1.26.10:
- Direct retrieval with `GOPROXY=direct` passed.
- Retrieval with `GOPROXY=https://proxy.golang.org,direct` passed. This verifies
the standard proxy/fallback configuration, not a proxy-only availability claim.
Checksum verification was enabled, `GOWORK=off` disabled workspace overrides, and
neither consumer had a `replace` directive. Go selected
`v0.10.3-0.20261101141937-587f407a32de`; the downloaded origin metadata identified
the exact migration commit in the Forge HTTPS repository. Both consumers received
the same module and module-file checksums.
After retrieval, each consumer passed `GOPROXY=off go test -mod=readonly -v ./...`.
The smoke test compiled and exercised the public API: explicit false/zero input
over typed defaults, effective provenance with `LayerMain`, source lookup by an
origin's source ID, and effective YAML rendering. Module inspection confirmed
that ConfigKit came from the corresponding fresh download cache, not the checkout.
This completes the one-time remote migration verification. The Go-generated
pseudo-version identifies an untagged commit; it is not an additional release.
No CI waiting or release gate was introduced.

View File

@@ -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.

View File

@@ -6,7 +6,7 @@ import (
"os"
"strings"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
func ExampleLoad() {
@@ -186,7 +186,8 @@ func ExampleOverride() {
},
Overrides: overrides,
})
fmt.Println(result.Config.Workers, result.Sources()[0].Label, err)
source, _ := result.Source("source-1")
fmt.Println(result.Config.Workers, source.Label, err)
// Output: 0 --workers <nil>
}

View File

@@ -9,7 +9,7 @@ import (
"strings"
"time"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
// Artifact is a Narratio-shaped generated output declaration.
@@ -167,7 +167,7 @@ func LoadNotariusPaths(path, invocationDir string, overrides ...configkit.Overri
resolveNotariusReference(configuration, resolution, invocationDir)
return nil
},
Secret: func(path configkit.Path) bool { return path.String() == "/sensitive" },
Sensitive: func(path configkit.Path) bool { return path.String() == "/sensitive" },
},
})
}
@@ -177,7 +177,7 @@ func resolveNotariusReference(configuration *NotariusPathConfig, resolution *con
if configuration.Reference == "" || filepath.IsAbs(configuration.Reference) {
return
}
info, found := resolution.InputInfo(path)
info, found := resolution.EntryInfo(path)
if !found {
resolution.Warning(path, "relative reference has no entry provenance")
return

View File

@@ -11,8 +11,8 @@ import (
"strings"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
configkit "go.maximumdirect.net/configkit"
"go.maximumdirect.net/configkit/examples/consumers"
)
func Example_narratio() {
@@ -82,7 +82,7 @@ func Example_weatherStartup() {
defer os.RemoveAll(directory)
_ = os.WriteFile(filepath.Join(directory, blockedName), []byte("unused-example-value\n"), 0o600)
_ = os.WriteFile(filepath.Join(directory, tokenName), []byte("disposable-example-credential\n"), 0o600)
report, bootstrapErr := configkit.LoadSecretsDir(directory)
report, bootstrapErr := configkit.LoadSecretsIntoEnvironment(directory)
var reportOutput bytes.Buffer
_, reportWriteErr := report.WriteTo(&reportOutput)

View File

@@ -6,7 +6,7 @@ import (
"io/fs"
"path"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
// CollectionBundleConfig is a representative application-owned collection
@@ -86,7 +86,7 @@ func resolveCollectionResources(configuration *CollectionBundleConfig, resolutio
continue
}
contentPath := configkit.Keys("collections", collectionID, "prompts", promptID, "content_file")
info, found := resolution.InputInfo(contentPath)
info, found := resolution.EntryInfo(contentPath)
if !found || len(info.Origin.Locations) != 1 {
return fmt.Errorf("prompt resource has no single declaring source")
}
@@ -104,9 +104,9 @@ func resolveCollectionResources(configuration *CollectionBundleConfig, resolutio
}
prompt.ResourceText = string(data)
collection.Prompts[promptID] = prompt
resolution.Record(
resolution.RecordNormalization(
configkit.Keys("collections", collectionID, "prompts", promptID, "resource_text"),
configkit.ChangeNormalization, "read source-relative prompt content", contentPath,
"read source-relative prompt content", contentPath,
)
}
configuration.Collections[collectionID] = collection

View File

@@ -5,8 +5,8 @@ import (
"fmt"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
configkit "go.maximumdirect.net/configkit"
"go.maximumdirect.net/configkit/examples/consumers"
)
func Example_inputDestination() {
@@ -60,7 +60,7 @@ func TestCollectionBundleAssemblesMountedPeersAndResources(t *testing.T) {
}
versionInfo, ok := result.Metadata().Lookup(configkit.Keys("version"))
if !ok || !versionInfo.Explicit || versionInfo.Origin.Layer != configkit.LayerBase || len(versionInfo.Origin.Locations) != 1 {
if !ok || !versionInfo.Explicit || versionInfo.Origin.Layer != configkit.LayerMain || len(versionInfo.Origin.Locations) != 1 {
t.Fatalf("main-owned version metadata = %#v, %v", versionInfo, ok)
}
collectionVersionPath := configkit.Keys("collections", "weatherreporter", "version")

View File

@@ -5,7 +5,7 @@ import (
"strconv"
"strings"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
// FileComparisonConfig is a small application-owned shape for comparing
@@ -101,8 +101,8 @@ func resolveRollout(configuration *RolloutConfig, resolution *configkit.Resoluti
url := fmt.Sprintf("https://%s:%d", endpoint.Host, endpoint.Port)
if endpoint.URL != url {
endpoint.URL = url
resolution.Record(configkit.Keys("service", "endpoint", "url"),
configkit.ChangeNormalization, "derived endpoint URL",
resolution.RecordNormalization(configkit.Keys("service", "endpoint", "url"),
"derived endpoint URL",
configkit.Keys("service", "endpoint", "host"),
configkit.Keys("service", "endpoint", "port"))
}

View File

@@ -7,8 +7,8 @@ import (
"strings"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
configkit "go.maximumdirect.net/configkit"
"go.maximumdirect.net/configkit/examples/consumers"
)
func ExampleCompareFiles() {

View File

@@ -5,7 +5,7 @@ import (
"os"
"path/filepath"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
type workingDirectoryPresetConfig struct {
@@ -40,6 +40,7 @@ func Example_workingDirectoryPreset() {
fmt.Println(err)
return
}
fmt.Println(result.Config.Name, filepath.Base(result.Sources()[0].Path))
source, _ := result.Source("source-1")
fmt.Println(result.Config.Name, filepath.Base(source.Path))
// Output: example service.yml
}

View File

@@ -1,6 +1,6 @@
package consumers
import configkit "gitea.maximumdirect.net/eric/ConfigKit"
import configkit "go.maximumdirect.net/configkit"
// PresetCatalog is ordinary application data; no inheritance fields or
// registration hooks are required in the typed configuration.

View File

@@ -3,8 +3,8 @@ package consumers_test
import (
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
configkit "go.maximumdirect.net/configkit"
"go.maximumdirect.net/configkit/examples/consumers"
)
func TestPresetCatalogOneLoadAndDerivation(t *testing.T) {

View File

@@ -6,7 +6,7 @@ import (
"io/fs"
"path"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
//go:embed testdata/native-source testdata/layered-bundle testdata/collection-bundle testdata/ordinary-imports testdata/source-version

View File

@@ -6,8 +6,8 @@ import (
"strings"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
configkit "go.maximumdirect.net/configkit"
"go.maximumdirect.net/configkit/examples/consumers"
)
func Example_nativeFilesystem() {
@@ -26,7 +26,7 @@ func Example_rootRelativeImports() {
fmt.Println(err)
return
}
source := result.Sources()[2]
source, _ := result.Source("source-3")
fmt.Println(result.Config.Name, result.Config.Version, result.Config.Prompts["brief"].Summary, source.Destination.String(), err)
// Output: Root import example 1.0 A root-loaded prompt. /prompts <nil>
}
@@ -68,7 +68,7 @@ func Example_sourceVersionAdoption() {
}
checks, redacted := 0, true
for _, source := range result.Sources() {
if check := source.VersionCheck; check != nil {
if check := source.VersionEvidence; check != nil {
checks++
redacted = redacted && check.Redacted && check.Version.Text == ""
}
@@ -95,7 +95,7 @@ func TestVersionedServiceModesAndMountedProtection(t *testing.T) {
t.Fatalf("versioned service sources = %#v", sources)
}
for _, source := range sources {
check := source.VersionCheck
check := source.VersionEvidence
if check == nil || check.Path.String() != "/version" || !check.Version.Present ||
!check.Redacted || check.Version.Text != "" {
t.Fatalf("safe source version evidence = %#v", sources)
@@ -119,7 +119,7 @@ func TestCatalogRetainsPresenceAndSourceContext(t *testing.T) {
}
for _, key := range []string{"enabled", "workers"} {
info, found := result.Metadata().Lookup(configkit.Keys(key))
if !found || !info.Explicit || info.Origin.Layer != configkit.LayerBase {
if !found || !info.Explicit || info.Origin.Layer != configkit.LayerMain {
t.Fatalf("%s presence = %#v, %v", key, info, found)
}
}

View File

@@ -3,7 +3,7 @@ package consumers
import (
"strings"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
// PromptCatalogConfig is an application-owned prompt collection shape.

View File

@@ -8,8 +8,8 @@ import (
"path/filepath"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
configkit "go.maximumdirect.net/configkit"
"go.maximumdirect.net/configkit/examples/consumers"
)
func Example_identifiedPromptCatalog() {
@@ -24,8 +24,8 @@ func Example_identifiedPromptCatalog() {
}
emptySearches := 0
for _, source := range result.Sources() {
for _, selection := range source.ImportSelections {
if selection.Selector == configkit.ImportSelectorDirectory && selection.Outcome == configkit.ImportSelectionNone {
for _, selection := range source.EmptyImportSelections {
if selection.Selector == configkit.ImportSelectorDirectory {
emptySearches++
}
}

View File

@@ -9,7 +9,7 @@ import (
"path/filepath"
"strings"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
// SequenceCatalogConfig is a source-backed prompt catalog with an application
@@ -43,7 +43,7 @@ func LoadSequenceCatalog(mainPath, overlayName, runLabel string) (*configkit.Res
filesystem := configkit.NewFilesystem("sequence catalog", os.DirFS(root))
filesystemContext := &configkit.FilesystemContext{BaseDir: ".", Root: ".", FS: &filesystem}
mainSearch := configkit.SearchConfig("", configkit.Searches(
configkit.Paths(absoluteMain),
configkit.SearchPaths(absoluteMain),
configkit.SearchDirectory(filepath.Join(root, "fallback"), configkit.DirectorySearchOptions{
Filenames: []configkit.FilenameGroup{configkit.CustomFilenames(filepath.Base(absoluteMain))},
}),
@@ -54,7 +54,7 @@ func LoadSequenceCatalog(mainPath, overlayName, runLabel string) (*configkit.Res
},
Resolve: func(configuration *SequenceCatalogConfig, resolution *configkit.Resolution) error {
configuration.ResolvedLabel = "for:" + configuration.RunLabel
resolution.Record(configkit.Keys("resolved_label"), configkit.ChangeNormalization,
resolution.RecordNormalization(configkit.Keys("resolved_label"),
"derive the resolved label from the selected run label", configkit.Keys("run_label"))
return nil
},

View File

@@ -8,8 +8,8 @@ import (
"strings"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
configkit "go.maximumdirect.net/configkit"
"go.maximumdirect.net/configkit/examples/consumers"
)
func Example_sequenceCatalog() {

View File

@@ -4,7 +4,7 @@ import (
"errors"
"io/fs"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
// VersionedServiceConfig demonstrates a mounted main input whose source-local
@@ -47,7 +47,7 @@ func LoadVersionedService(mode configkit.SourceVersionMode, overrides ...configk
return nil
},
},
Secret: func(path configkit.Path) bool {
Sensitive: func(path configkit.Path) bool {
return path.String() == "/service/version"
},
}

View File

@@ -11,7 +11,7 @@ import (
"strings"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
var (
@@ -23,7 +23,7 @@ var (
func TestFacadeValuesRemainRootOwned(t *testing.T) {
t.Parallel()
const packagePath = "gitea.maximumdirect.net/eric/ConfigKit"
const packagePath = "go.maximumdirect.net/configkit"
for _, candidate := range []reflect.Type{
reflect.TypeFor[configkit.Path](), reflect.TypeFor[configkit.Source](),
reflect.TypeFor[configkit.Secret](), reflect.TypeFor[configkit.Diagnostic](),
@@ -140,7 +140,7 @@ func TestFacadeSecretsBootstrapUsesFacadeRecords(t *testing.T) {
t.Fatal(err)
}
report, err := configkit.LoadSecretsDir(directory)
report, err := configkit.LoadSecretsIntoEnvironment(directory)
if err != nil {
t.Fatal(err)
}
@@ -164,7 +164,7 @@ func TestFacadeSecretsBootstrapUsesFacadeRecords(t *testing.T) {
t.Fatalf("report rendering = %q, %v", rendered.String(), err)
}
failedReport, err := configkit.LoadSecretsDir("")
failedReport, err := configkit.LoadSecretsIntoEnvironment("")
var secretsError *configkit.SecretsError
if failedReport == nil || !errors.As(err, &secretsError) || len(secretsError.Diagnostics()) != 1 || secretsError.Diagnostics()[0].Code != configkit.CodeSecretsInvalidPath {
t.Fatalf("bootstrap failure = %#v / %#v", failedReport, err)
@@ -203,7 +203,7 @@ func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
if !resolution.IsExplicit(configkit.Keys("input")) {
resolution.Error(configkit.Keys("input"), "input is required")
}
input, ok := resolution.InputInfo(configkit.Keys("input"))
input, ok := resolution.EntryInfo(configkit.Keys("input"))
if !ok || !input.Explicit {
resolution.Error(configkit.Keys("input"), "missing input provenance")
}
@@ -245,6 +245,8 @@ func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
}
for _, context := range []func(){
func() { retainedResolution.IsExplicit(configkit.Keys("input")) },
func() { retainedResolution.RecordDerivedDefault(configkit.Keys("input"), "too late") },
func() { retainedResolution.RecordNormalization(configkit.Keys("input"), "too late") },
func() { retainedValidation.Warning(configkit.Keys("input"), "too late") },
} {
func() {

View File

@@ -7,7 +7,7 @@ import (
"io"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
var (

View 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)
}
}
}

View File

@@ -3,7 +3,7 @@ package configkit_test
import (
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
func TestFacadeInactiveContextsPreserveClosedFailure(t *testing.T) {
@@ -24,10 +24,11 @@ func TestFacadeInactiveContextsPreserveClosedFailure(t *testing.T) {
call func()
}{
{"IsExplicit", func() { state.resolution.IsExplicit(path) }},
{"InputInfo", func() { state.resolution.InputInfo(path) }},
{"EntryInfo", func() { state.resolution.EntryInfo(path) }},
{"Source", func() { state.resolution.Source("source") }},
{"SourceContext", func() { state.resolution.SourceContext("source") }},
{"Record", func() { state.resolution.Record(path, configkit.ChangeNormalization, "reason") }},
{"RecordDerivedDefault", func() { state.resolution.RecordDerivedDefault(path, "reason") }},
{"RecordNormalization", func() { state.resolution.RecordNormalization(path, "reason") }},
{"Resolution.Error", func() { state.resolution.Error(path, "message") }},
{"Resolution.Warning", func() { state.resolution.Warning(path, "message") }},
{"Default", func() { configkit.Default(state.resolution, path, &target, 1, "reason") }},

View File

@@ -14,7 +14,7 @@ import (
"testing"
"testing/fstest"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
func TestFacadeInputDestinationsAcrossSourceKinds(t *testing.T) {
@@ -352,7 +352,7 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
Resolve: func(configuration *map[string]any, resolution *configkit.Resolution) error {
callbacks.resolve++
resourcePath := configkit.Keys("catalog", "entries", "item", "resource")
input, ok := resolution.InputInfo(resourcePath)
input, ok := resolution.EntryInfo(resourcePath)
if !ok || len(input.Origin.Locations) != 1 {
return fmt.Errorf("mounted resource input has no single source origin")
}
@@ -370,11 +370,11 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
}
entry := facadeDestinationMap(t, facadeDestinationMap(t, facadeDestinationMap(t, *configuration, "catalog"), "entries"), "item")
entry["resource_text"] = string(data)
resolution.Record(configkit.Keys("catalog", "entries", "item", "resource_text"), configkit.ChangeNormalization, "read source-relative summary", resourcePath)
resolution.RecordNormalization(configkit.Keys("catalog", "entries", "item", "resource_text"), "read source-relative summary", resourcePath)
return nil
},
Validate: func(*map[string]any, *configkit.Validation) { callbacks.validate++ },
Secret: func(candidate configkit.Path) bool { return candidate.String() == tokenPath.String() },
Validate: func(*map[string]any, *configkit.Validation) { callbacks.validate++ },
Sensitive: func(candidate configkit.Path) bool { return candidate.String() == tokenPath.String() },
},
})
if err != nil {
@@ -405,7 +405,7 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
if err != nil || string(data) != "source-relative summary" {
t.Fatalf("completed relative resource = %q, %v", data, err)
}
if info, ok := result.Metadata().Lookup(titlePath); !ok || info.Origin.Layer != configkit.LayerBase {
if info, ok := result.Metadata().Lookup(titlePath); !ok || info.Origin.Layer != configkit.LayerMain {
t.Fatalf("mounted title metadata = %#v, %v", info, ok)
}
if history := result.Metadata().History(titlePath); len(history) < 2 {

View 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)
}
}

View File

@@ -7,7 +7,7 @@ import (
"strings"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
type facadeDiffConfig struct {
@@ -29,10 +29,10 @@ func TestFacadeDiffKeepsPublicSequenceChangesWithHistoricalDescendants(t *testin
t.Helper()
result, err := configkit.Load(configkit.Bytes("historical-sequence.yml", []byte("items: [{secret: disposable-canary}, "+public+"]\n")),
configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
Secret: func(path configkit.Path) bool { return path.String() == "/items/0/secret" },
Sensitive: func(path configkit.Path) bool { return path.String() == "/items/0/secret" },
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
current.Items[0] = "public scalar"
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "normalize first item")
resolution.RecordNormalization(configkit.Keys("items"), "normalize first item")
return nil
},
}})
@@ -71,10 +71,10 @@ func TestFacadeDiffKeepsPublicScalarAcrossHistoricalAbsence(t *testing.T) {
Dynamic map[string]any `yaml:"dynamic"`
}
options := configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
Secret: func(path configkit.Path) bool { return path.String() == "/dynamic/entry/token" },
Sensitive: func(path configkit.Path) bool { return path.String() == "/dynamic/entry/token" },
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
delete(current.Dynamic, "entry")
resolution.Record(configkit.Keys("dynamic", "entry"), configkit.ChangeNormalization, "remove entry")
resolution.RecordNormalization(configkit.Keys("dynamic", "entry"), "remove entry")
return nil
},
}}
@@ -221,7 +221,7 @@ func TestFacadeDiffIgnoresSourceAndResolutionHistory(t *testing.T) {
resolve := func(value string) configkit.Options[config] {
return configkit.Options[config]{Definition: configkit.Definition[config]{Resolve: func(current *config, resolution *configkit.Resolution) error {
current.Count = len(value) + 8
resolution.Record(configkit.Keys("count"), configkit.ChangeNormalization, "set effective count")
resolution.RecordNormalization(configkit.Keys("count"), "set effective count")
return nil
}}}
}
@@ -281,7 +281,7 @@ func TestFacadeDiffUsesFrozenFilesAndRunsCallbacksOnlyDuringLoad(t *testing.T) {
func TestFacadeDiffCombinesSecretSensitivityAndLoadErrors(t *testing.T) {
t.Setenv("FACADE_DIFF_SECRET", "credential-one")
secretOptions := configkit.Options[facadeDiffSecretConfig]{Definition: configkit.Definition[facadeDiffSecretConfig]{Secret: func(path configkit.Path) bool {
secretOptions := configkit.Options[facadeDiffSecretConfig]{Definition: configkit.Definition[facadeDiffSecretConfig]{Sensitive: func(path configkit.Path) bool {
return path.String() == "/secret"
}}}
left, err := configkit.Load[facadeDiffSecretConfig](configkit.Bytes("left.yml", []byte("secret: FACADE_DIFF_SECRET\n")), secretOptions)

View File

@@ -93,14 +93,14 @@ func TestFacadeSearchOrderingRequirednessAndSnapshots(t *testing.T) {
names := []string{"config.yml"}
groups := []FilenameGroup{CustomFilenames(names...)}
directoryOptions := []DirectorySearchOptions{{Filenames: groups}}
search := Searches(Paths(paths...), SearchDirectory(root, directoryOptions...), Paths(laterPath))
search := Searches(SearchPaths(paths...), SearchDirectory(root, directoryOptions...), SearchPaths(laterPath))
source := SearchConfig("", search, SearchOptions{Required: true})
paths[0] = laterPath
names[0] = "later.yml"
groups[0] = CustomFilenames("later.yml")
directoryOptions[0].Filenames[0] = CustomFilenames("later.yml")
search = Paths(laterPath)
search = SearchPaths(laterPath)
result, err := Load[facadeDiscoveryConfig](source)
if err != nil || result.Config.Value != "selected" {
@@ -110,7 +110,7 @@ func TestFacadeSearchOrderingRequirednessAndSnapshots(t *testing.T) {
t.Fatalf("ordered search sources = %#v", sources)
}
_, err = Load[facadeDiscoveryConfig](SearchConfig("", Paths(filepath.Join(root, "missing.yml")), SearchOptions{Required: true}))
_, err = Load[facadeDiscoveryConfig](SearchConfig("", SearchPaths(filepath.Join(root, "missing.yml")), SearchOptions{Required: true}))
var loadErr *LoadError
if !errors.As(err, &loadErr) {
t.Fatalf("required exhausted search error = %v, want *LoadError", err)
@@ -131,7 +131,7 @@ func TestFacadeSearchWorksForLayeredMainAndUnderlay(t *testing.T) {
mainPath := writeFacadeImportFile(t, root, "main.yml", "settings: {main: value}\n")
underlayPath := writeFacadeImportFile(t, root, "underlay/base.yml", "settings: {underlay: value}\n")
result, err := LoadLayered(
Input{Source: SearchConfig("", Paths(mainPath))},
Input{Source: SearchConfig("", SearchPaths(mainPath))},
LayeredOptions[facadeDiscoveryConfig]{Underlays: []Underlay{{Inputs: []Input{{
Source: SearchConfig("", SearchDirectory(filepath.Dir(underlayPath), DirectorySearchOptions{
Filenames: []FilenameGroup{CustomFilenames("base.yml")},
@@ -142,7 +142,7 @@ func TestFacadeSearchWorksForLayeredMainAndUnderlay(t *testing.T) {
t.Fatalf("layered search = %#v, %v", result, err)
}
sources := result.Sources()
if len(sources) != 2 || sources[0].Path != canonicalFacadePath(t, mainPath) || sources[0].Layer != LayerBase ||
if len(sources) != 2 || sources[0].Path != canonicalFacadePath(t, mainPath) || sources[0].Layer != LayerMain ||
sources[1].Path != canonicalFacadePath(t, underlayPath) || sources[1].Layer != LayerUnderlay {
t.Fatalf("layered search sources = %#v", sources)
}
@@ -157,7 +157,7 @@ func TestFacadeSearchSelectedAndAbsentUseRootOnlyContext(t *testing.T) {
filesystem := &FilesystemContext{Root: root}
options := Options[facadeDiscoveryConfig]{Overlay: "testing", Filesystem: filesystem}
selected, err := Load(SearchConfig("", Paths(mainPath)), options)
selected, err := Load(SearchConfig("", SearchPaths(mainPath)), options)
if err != nil || selected.Config.Value != "overlay" {
t.Fatalf("selected search with root-only context = %#v, %v", selected, err)
}
@@ -171,7 +171,7 @@ func TestFacadeSearchSelectedAndAbsentUseRootOnlyContext(t *testing.T) {
t.Fatalf("selected file base context = %#v, %v", mainContext, ok)
}
missing := SearchConfig("", Paths(filepath.Join(root, "missing.yml")))
missing := SearchConfig("", SearchPaths(filepath.Join(root, "missing.yml")))
searchedAbsent, err := Load(missing, options)
if err != nil {
t.Fatalf("exhausted search with root-only overlay context: %v", err)
@@ -201,7 +201,7 @@ func TestFacadeSearchRejectsSelectedContextWithoutFallback(t *testing.T) {
inside := writeFacadeImportFile(t, root, "inside.yml", "value: fallback\n")
outsideRoot := t.TempDir()
outside := writeFacadeImportFile(t, outsideRoot, "outside.yml", "value: selected\n")
search := SearchConfig("", Paths(outside, inside))
search := SearchConfig("", SearchPaths(outside, inside))
_, err := Load(search, Options[facadeDiscoveryConfig]{Filesystem: &FilesystemContext{Root: root}})
var loadErr *LoadError
@@ -228,7 +228,7 @@ func TestFacadeSearchRunsCallbacksOnceAndKeepsInspectionWithSelectedFile(t *test
selectedPath := writeFacadeImportFile(t, root, "selected.yml", "value: selected\n")
writeFacadeImportFile(t, root, "later.yml", "value: later\n")
defaultsCalls, resolveCalls, validateCalls := 0, 0, 0
result, err := Load(SearchConfig("", Paths(selectedPath, filepath.Join(root, "later.yml"))), Options[facadeDiscoveryLifecycleConfig]{
result, err := Load(SearchConfig("", SearchPaths(selectedPath, filepath.Join(root, "later.yml"))), Options[facadeDiscoveryLifecycleConfig]{
Definition: Definition[facadeDiscoveryLifecycleConfig]{
Defaults: func() facadeDiscoveryLifecycleConfig {
defaultsCalls++
@@ -237,7 +237,7 @@ func TestFacadeSearchRunsCallbacksOnceAndKeepsInspectionWithSelectedFile(t *test
Resolve: func(configuration *facadeDiscoveryLifecycleConfig, resolution *Resolution) error {
resolveCalls++
configuration.Resolved = "from:" + configuration.Value
resolution.Record(Keys("resolved"), ChangeNormalization, "derive from selected value", Keys("value"))
resolution.RecordNormalization(Keys("resolved"), "derive from selected value", Keys("value"))
return nil
},
Validate: func(_ *facadeDiscoveryLifecycleConfig, _ *Validation) { validateCalls++ },
@@ -272,7 +272,7 @@ func TestFacadeSearchSequenceItemAndAbsentInputPlacement(t *testing.T) {
Destination: Keys("items"),
PlacementMode: PlacementModeSequenceItem,
}
selected, err := Load(SearchConfig("", Paths(selectedPath)), options)
selected, err := Load(SearchConfig("", SearchPaths(selectedPath)), options)
if err != nil || len(selected.Config.Items) != 1 || selected.Config.Items[0].Name != "selected" {
t.Fatalf("selected sequence item = %#v, %v", selected, err)
}
@@ -283,7 +283,7 @@ func TestFacadeSearchSequenceItemAndAbsentInputPlacement(t *testing.T) {
defaults := func() facadeDiscoveryPlacementConfig {
return facadeDiscoveryPlacementConfig{Items: []facadeDiscoveryPlacementItem{}}
}
absent, err := Load(SearchConfig("", Paths(filepath.Join(root, "missing.yml"))), Options[facadeDiscoveryPlacementConfig]{
absent, err := Load(SearchConfig("", SearchPaths(filepath.Join(root, "missing.yml"))), Options[facadeDiscoveryPlacementConfig]{
Definition: Definition[facadeDiscoveryPlacementConfig]{Defaults: defaults},
Destination: options.Destination,
PlacementMode: options.PlacementMode,

View 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")
}
}
}
}

View File

@@ -9,7 +9,7 @@ import (
"testing"
"testing/fstest"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
type equivalentConfig struct {
@@ -69,7 +69,7 @@ func TestEquivalentSourceFormsPreserveInputSemantics(t *testing.T) {
}
for _, field := range []string{"version", "name", "enabled", "count", "items", "optional"} {
info, ok := result.Metadata().Lookup(configkit.Keys(field))
if !ok || !info.Explicit || info.Origin.Layer != configkit.LayerBase || info.Null != (field == "optional") {
if !ok || !info.Explicit || info.Origin.Layer != configkit.LayerMain || info.Null != (field == "optional") {
t.Errorf("%s input info = %#v, %v", field, info, ok)
}
}
@@ -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]{
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{"secret": "old-secret-canary"})}}}},
Definition: configkit.Definition[config]{Secret: func(path configkit.Path) bool { return path.String() == "/secret" }},
Definition: configkit.Definition[config]{Sensitive: func(path configkit.Path) bool { return path.String() == "/secret" }},
})
if err != nil || result.Config.Secret != "new-secret-canary" {
t.Fatalf("secret load = %#v, %v", result, err)

View File

@@ -9,7 +9,7 @@ import (
"testing"
"testing/fstest"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
//go:embed testdata/filesystem

View File

@@ -5,7 +5,7 @@ import (
"strings"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit"
"go.maximumdirect.net/configkit"
)
func TestFinalSequenceIdentityTreatsLateExtendsFieldsAsPayload(t *testing.T) {
@@ -29,7 +29,7 @@ func TestFinalSequenceIdentityTreatsLateExtendsFieldsAsPayload(t *testing.T) {
if phase == "resolution" {
definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
(*value)["items"] = items
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "supply application payload")
resolution.RecordNormalization(configkit.Keys("items"), "supply application payload")
return nil
}
}
@@ -77,7 +77,7 @@ func TestFinalSequenceIdentityValidatesLateValues(t *testing.T) {
case "resolution":
options.Definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
(*value)["items"] = items
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "create effective records")
resolution.RecordNormalization(configkit.Keys("items"), "create effective records")
return nil
}
}
@@ -134,7 +134,7 @@ func TestFinalSequenceIdentityMatchesLateDynamicNamespaces(t *testing.T) {
case "resolution":
options.Definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
(*value)["collections"] = map[string]any{name: map[string]any{"items": items}}
resolution.Record(configkit.Keys("collections"), configkit.ChangeNormalization, "create namespaced records")
resolution.RecordNormalization(configkit.Keys("collections"), "create namespaced records")
return nil
}
}

View File

@@ -4,7 +4,7 @@ import (
"reflect"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
func TestPublicHistoricalPathsEnumerateContextualShapes(t *testing.T) {
@@ -40,12 +40,16 @@ func TestPublicHistoricalPathsEnumerateContextualShapes(t *testing.T) {
if len(metadata.History(paths[6])) == 0 {
t.Fatal("enumerated numeric path lost history")
}
assertPublicShapeHistory(t, metadata, configkit.LayerCLI)
if _, ok := metadata.Lookup(configkit.Keys("shape", "0")); ok {
t.Fatal("mapping-key path resolved against the effective sequence")
}
if len((configkit.Metadata{}).HistoricalPaths()) != 0 {
t.Fatal("zero metadata contains historical paths")
}
if (configkit.Metadata{}).History(configkit.Keys("shape")) != nil {
t.Fatal("zero metadata contains history")
}
paths[1] = configkit.Keys("mutated")
if again := metadata.HistoricalPaths(); !reflect.DeepEqual(pathStrings(again), want) {
@@ -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 {
result := make([]string, len(paths))
for index, path := range paths {

View File

@@ -8,7 +8,7 @@ import (
"path/filepath"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
func TestFacadeNativeDiscoveryPreservesAliasIdentity(t *testing.T) {

View File

@@ -93,7 +93,8 @@ main: main
func TestImportDefaultsPreserveEmptyPatternAndRequiredness(t *testing.T) {
type config struct {
Value string `yaml:"value"`
Value string `yaml:"value"`
Hidden string `yaml:"hidden"`
}
directory := t.TempDir()
mainPath := writeFacadeImportFile(t, directory, "main.yml", `configkit:
@@ -101,17 +102,23 @@ func TestImportDefaultsPreserveEmptyPatternAndRequiredness(t *testing.T) {
- dir: snippets
`)
writeFacadeImportFile(t, directory, "snippets/value.cfg", "value: imported\n")
writeFacadeImportFile(t, directory, "snippets/.hidden.cfg", "hidden: imported\n")
unrestricted, err := Load(File(mainPath), Options[config]{Imports: true})
if err != nil || unrestricted.Config.Value != "imported" {
t.Fatalf("unspecified pattern = %#v, %v", unrestricted, err)
}
includeHidden := false
visibleOnly, err := Load(File(mainPath), Options[config]{Imports: true, ImportDefaults: ImportDefaults{IncludeHidden: &includeHidden}})
if err != nil || visibleOnly.Config.Value != "imported" || visibleOnly.Config.Hidden != "" {
t.Fatalf("hidden-file default = %#v, %v", visibleOnly, err)
}
empty, err := Load(File(mainPath), Options[config]{Imports: true, ImportDefaults: ImportDefaults{Pattern: []string{}}})
if err != nil || empty.Config.Value != "" {
t.Fatalf("empty pattern = %#v, %v", empty, err)
}
emptySources := empty.Sources()
if len(emptySources) != 1 || len(emptySources[0].ImportSelections) != 1 || emptySources[0].ImportSelections[0].Path.String() != "/configkit/imports/0" || emptySources[0].ImportSelections[0].Selector != ImportSelectorDirectory || emptySources[0].ImportSelections[0].Outcome != ImportSelectionNone {
if len(emptySources) != 1 || len(emptySources[0].EmptyImportSelections) != 1 || emptySources[0].EmptyImportSelections[0].Path.String() != "/configkit/imports/0" || emptySources[0].EmptyImportSelections[0].Selector != ImportSelectorDirectory {
t.Fatalf("empty-pattern evidence = %#v", emptySources)
}
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 {
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)
}
}
@@ -230,15 +237,15 @@ value: loaded
callbackPath := ""
loaded, err := Load(File(mainPath), Options[config]{Imports: true, Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
first, ok := resolution.Source("source-1")
if !ok || len(first.ImportSelections) != 1 {
if !ok || len(first.EmptyImportSelections) != 1 {
t.Fatalf("resolution source = %#v, %v", first, ok)
}
first.ImportSelections[0].Path = Keys("callback mutation")
first.EmptyImportSelections[0].Path = Keys("callback mutation")
second, ok := resolution.Source("source-1")
if !ok || second.ImportSelections[0].Path.String() != "/configkit/imports/0" {
if !ok || second.EmptyImportSelections[0].Path.String() != "/configkit/imports/0" {
t.Fatalf("resolution snapshot changed after mutation: %#v", second)
}
callbackPath = second.ImportSelections[0].Path.String()
callbackPath = second.EmptyImportSelections[0].Path.String()
return nil
}}})
if err != nil {
@@ -248,11 +255,11 @@ value: loaded
t.Fatalf("callback path = %q", callbackPath)
}
sources := loaded.Sources()
if len(sources) != 1 || len(sources[0].ImportSelections) != 1 || sources[0].ImportSelections[0].Location.SourceID != sources[0].ID || sources[0].ImportSelections[0].Selector != ImportSelectorDirectory {
if len(sources) != 1 || len(sources[0].EmptyImportSelections) != 1 || sources[0].EmptyImportSelections[0].Location.SourceID != sources[0].ID || sources[0].EmptyImportSelections[0].Selector != ImportSelectorDirectory {
t.Fatalf("result source evidence = %#v", sources)
}
sources[0].ImportSelections[0].Path = Keys("result mutation")
if got := loaded.Sources()[0].ImportSelections[0].Path.String(); got != "/configkit/imports/0" {
sources[0].EmptyImportSelections[0].Path = Keys("result mutation")
if got := loaded.Sources()[0].EmptyImportSelections[0].Path.String(); got != "/configkit/imports/0" {
t.Fatalf("result snapshot changed after mutation: %q", got)
}
@@ -268,11 +275,11 @@ value: loaded
t.Fatalf("failed load error = %T %v", err, err)
}
errorSources := loadError.Sources()
if len(errorSources) != 1 || len(errorSources[0].ImportSelections) != 1 || errorSources[0].ImportSelections[0].Path.String() != "/configkit/imports/0" {
if len(errorSources) != 1 || len(errorSources[0].EmptyImportSelections) != 1 || errorSources[0].EmptyImportSelections[0].Path.String() != "/configkit/imports/0" {
t.Fatalf("error source evidence = %#v", errorSources)
}
errorSources[0].ImportSelections[0].Path = Keys("error mutation")
if got := loadError.Sources()[0].ImportSelections[0].Path.String(); got != "/configkit/imports/0" {
errorSources[0].EmptyImportSelections[0].Path = Keys("error mutation")
if got := loadError.Sources()[0].EmptyImportSelections[0].Path.String(); got != "/configkit/imports/0" {
t.Fatalf("error snapshot changed after mutation: %q", got)
}
}

View File

@@ -42,7 +42,7 @@ entries:
Definition: Definition[map[string]any]{
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
SequenceIdentities: []SequenceIdentity{SequenceIdentityAt(Keys("services", "records"), "id")},
Secret: func(candidate Path) bool {
Sensitive: func(candidate Path) bool {
return candidate.String() == "/services/records/0/version"
},
},
@@ -67,6 +67,14 @@ entries:
}
sources := result.Sources()
if len(sources) < 3 || sources[0].Layer != LayerMain || string(sources[0].Layer) != "main" {
t.Fatalf("main source attribution = %#v", sources)
}
for _, source := range sources[1:] {
if source.ImportedFrom != nil && (source.Layer != LayerMain || string(source.Layer) != "main") {
t.Errorf("imported main source attribution = %#v", source)
}
}
filesByID := make(map[string]string, len(sources))
for _, source := range sources {
filesByID[source.ID] = source.Path
@@ -77,8 +85,8 @@ entries:
itemSource = source
}
}
if itemSource.VersionCheck == nil || !itemSource.VersionCheck.Version.Present || !itemSource.VersionCheck.Redacted || itemSource.VersionCheck.Version.Text != "" {
t.Fatalf("consumed header inspection = %#v", itemSource.VersionCheck)
if itemSource.VersionEvidence == nil || !itemSource.VersionEvidence.Version.Present || !itemSource.VersionEvidence.Redacted || itemSource.VersionEvidence.Version.Text != "" {
t.Fatalf("consumed header inspection = %#v", itemSource.VersionEvidence)
}
idInfo, ok := result.Metadata().Lookup(Keys("services", "records").Index(0).Key("id"))
if !ok || len(idInfo.Origin.Locations) != 1 || filesByID[idInfo.Origin.Locations[0].SourceID] != "bundle/config/records/helper.yml" {
@@ -107,7 +115,7 @@ entries:
Definition: Definition[map[string]any]{
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
SequenceIdentities: []SequenceIdentity{SequenceIdentityAt(Keys("services", "records"), "version")},
Secret: func(candidate Path) bool {
Sensitive: func(candidate Path) bool {
return candidate.String() == "/services/records/0/version"
},
},
@@ -129,7 +137,7 @@ entries:
break
}
}
if !foundFailedItem || failedItemSource.VersionCheck == nil || failedItemSource.VersionCheck.Version.Text != "" || !failedItemSource.VersionCheck.Redacted {
if !foundFailedItem || failedItemSource.VersionEvidence == nil || failedItemSource.VersionEvidence.Version.Text != "" || !failedItemSource.VersionEvidence.Redacted {
t.Fatalf("failed-load source inspection exposed or omitted header evidence: %#v", failedItemSource)
}
}

View File

@@ -7,7 +7,7 @@ import (
"path/filepath"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
func TestFacadeDirectoryRootBelowContainedAbsoluteSymlink(t *testing.T) {

View File

@@ -8,7 +8,7 @@ import (
"testing"
"testing/fstest"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
type inheritedEntry struct {
@@ -42,7 +42,7 @@ func TestFacadeInheritancePreservesPresenceOriginsAndHistory(t *testing.T) {
if !resolution.IsExplicit(configkit.Keys("entries", "child", "enabled")) {
t.Error("inherited false is not explicit")
}
info, found := resolution.InputInfo(configkit.Keys("entries", "child", "enabled"))
info, found := resolution.EntryInfo(configkit.Keys("entries", "child", "enabled"))
if !found || len(info.Origin.Inheritance) != 1 {
t.Errorf("resolver inherited origin = %#v, %v", info, found)
}
@@ -139,7 +139,7 @@ func TestFacadeIdentifiedInheritanceResolvesChainsAndNestedCollections(t *testin
result, err := configkit.Load(filesystem.File("config/identified.yml"), configkit.Options[configuration]{
Definition: configkit.Definition[configuration]{
SequenceIdentities: registrations,
Secret: func(path configkit.Path) bool {
Sensitive: func(path configkit.Path) bool {
return path.String() == "/jobs/0/credential" || path.String() == "/jobs/1/credential" || path.String() == "/jobs/2/credential"
},
},
@@ -221,7 +221,7 @@ func TestFacadeIdentifiedInheritanceFailureDoesNotRevealSecretCanary(t *testing.
_, err := configkit.Load(configkit.Bytes("identified-failure.yml", input), configkit.Options[configuration]{
Definition: configkit.Definition[configuration]{
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("jobs"), "id")},
Secret: func(path configkit.Path) bool { return path.String() == "/jobs/0/credential" },
Sensitive: func(path configkit.Path) bool { return path.String() == "/jobs/0/credential" },
},
})
if err == nil || strings.Contains(err.Error(), canary) {
@@ -468,7 +468,7 @@ func TestFacadeInheritedSecretsStayRedactedAfterLookup(t *testing.T) {
Entries map[string]entry `yaml:"entries"`
}
result, err := configkit.Load[configuration](configkit.Bytes("secret.yml", []byte("entries: {parent: {credential: CONFIGKIT_INHERITED_TEST_SECRET, plain: visible}, child: {extends: parent}}\n")), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
Secret: func(path configkit.Path) bool { return path.String() == "/entries/parent/credential" },
Sensitive: func(path configkit.Path) bool { return path.String() == "/entries/parent/credential" },
}})
if err != nil || result.Config.Entries["child"].Credential.Value() != canary {
t.Fatalf("inherited credential = %#v, %v", result, err)

View File

@@ -5,7 +5,7 @@ import (
"testing"
"testing/fstest"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
func TestFacadeItemScopesDoNotReinterpretGlobalRegistrations(t *testing.T) {

View File

@@ -4,7 +4,7 @@ import (
"testing"
"testing/fstest"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
func TestFacadeItemDomainVersionsKeepEffectiveScope(t *testing.T) {

View File

@@ -10,7 +10,7 @@ import (
"testing"
"testing/fstest"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
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"))
return nil
},
Validate: func(config *layeredFacadeConfig, validation *configkit.Validation) { checks.validation++ },
Secret: func(path configkit.Path) bool { return path.String() == "/settings/secret" },
Validate: func(config *layeredFacadeConfig, validation *configkit.Validation) { checks.validation++ },
Sensitive: func(path configkit.Path) bool { return path.String() == "/settings/secret" },
},
})
if err != nil {
@@ -80,7 +80,7 @@ func TestFacadeLayeredMixedSourcesAndInspection(t *testing.T) {
if len(sources) != 8 {
t.Fatalf("sources = %#v", sources)
}
wantLayers := []configkit.Layer{configkit.LayerBase, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerOverlay, configkit.LayerCLI}
wantLayers := []configkit.Layer{configkit.LayerMain, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerOverlay, configkit.LayerCLI}
wantIndexes := []int{0, 1, 1, 1, 1, 2, 0, 0}
wantKinds := []configkit.SourceKind{configkit.SourceKindFile, configkit.SourceKindFilesystem, configkit.SourceKindFilesystem, configkit.SourceKindMapping, configkit.SourceKindFilesystem, configkit.SourceKindMapping, configkit.SourceKindFilesystem, configkit.SourceKindCLI}
for index, source := range sources {

View File

@@ -7,7 +7,7 @@ import (
"testing"
"testing/fstest"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
type mappingFacadeConfig struct {
@@ -36,7 +36,7 @@ func TestFacadeMappingSnapshotsAndMatchesYAML(t *testing.T) {
if !reflect.DeepEqual(result.Config, want) {
t.Fatalf("mapping config = %#v, want %#v", result.Config, want)
}
if sources := result.Sources(); len(sources) != 1 || sources[0].ID != "source-1" || sources[0].Kind != configkit.SourceKindMapping || sources[0].Layer != configkit.LayerBase || sources[0].Path != "" {
if sources := result.Sources(); len(sources) != 1 || sources[0].ID != "source-1" || sources[0].Kind != configkit.SourceKindMapping || sources[0].Layer != configkit.LayerMain || sources[0].Path != "" {
t.Fatalf("mapping sources = %#v", sources)
}
info, ok := result.Metadata().Lookup(configkit.Keys("name"))
@@ -89,7 +89,7 @@ func TestFacadeMappingImportsUseExplicitContext(t *testing.T) {
if err != nil || result.Config.Name != "imported" || !result.Config.Enabled {
t.Fatalf("mapping import = %#v, %v", result, err)
}
if sources := result.Sources(); len(sources) != 2 || sources[0].Kind != configkit.SourceKindMapping || sources[1].Kind != configkit.SourceKindFilesystem || sources[1].Layer != configkit.LayerBase {
if sources := result.Sources(); len(sources) != 2 || sources[0].Kind != configkit.SourceKindMapping || sources[1].Kind != configkit.SourceKindFilesystem || sources[1].Layer != configkit.LayerMain {
t.Fatalf("mapping import sources = %#v", sources)
}
checkMappingCode(t, source, configkit.Options[mappingFacadeConfig]{Imports: true}, configkit.CodeOptionsInvalid)

View 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)
}
})
}
}

View 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")
}
}

View File

@@ -12,7 +12,7 @@ import (
"testing"
"testing/fstest"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
type observedResourceFS struct {

View File

@@ -5,7 +5,7 @@ import (
"strings"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
func TestFacadeSequenceIdentitySnapshotsSelectorsAndFields(t *testing.T) {

View File

@@ -9,7 +9,7 @@ import (
"testing"
"testing/fstest"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
func TestFacadeNoSourcePreservesAbsenceWithSequencePolicy(t *testing.T) {
@@ -125,7 +125,7 @@ func TestFacadeRegisteredReplacementExplainsRemovedDescendants(t *testing.T) {
t.Fatalf("removal not explained: %#v", explanation)
}
last := explanation.History[len(explanation.History)-1]
if !last.Before.Present || last.After.Present || last.Origin.Layer != configkit.LayerBase || len(last.Origin.Locations) != 1 || last.Origin.Locations[0].SourceID != result.Sources()[0].ID {
if !last.Before.Present || last.After.Present || last.Origin.Layer != configkit.LayerMain || len(last.Origin.Locations) != 1 || last.Origin.Locations[0].SourceID != result.Sources()[0].ID {
t.Fatalf("wrong removal transition/origin: %#v", last)
}
})
@@ -211,7 +211,7 @@ func TestFacadeSequencePlacementControlsAcrossLoadEntryPoints(t *testing.T) {
if got := ordinary.Config["entries"].([]any); len(got) != 1 || got[0].(map[string]any)["id"] != "one" {
t.Fatalf("ordinary item placement = %#v", ordinary.Config)
}
if sources := ordinary.Sources(); len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeSequenceItem || sources[0].Destination.String() != "/entries" || sources[0].DestinationUnresolved {
if sources := ordinary.Sources(); len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeSequenceItem || sources[0].Destination.String() != "/entries" || sources[0].DestinationIsFallback {
t.Fatalf("ordinary item source = %#v", sources)
}
@@ -230,7 +230,7 @@ func TestFacadeSequencePlacementControlsAcrossLoadEntryPoints(t *testing.T) {
t.Fatalf("layered item placement = %#v", layered.Config)
}
for _, source := range layered.Sources() {
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationUnresolved {
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationIsFallback {
t.Errorf("layered item source = %#v", source)
}
}
@@ -334,7 +334,7 @@ items:
t.Fatalf("callback snapshots = %#v, contexts %#v", callbackSources, callbackContexts)
}
for index, source := range callbackSources {
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/items" || source.DestinationUnresolved {
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/items" || source.DestinationIsFallback {
t.Errorf("callback source = %#v", source)
}
if callbackContexts[index].FS == nil || callbackContexts[index].BaseDir != "records" {
@@ -349,7 +349,7 @@ items:
t.Errorf("shifted item origin at %s = %#v, %v", path.String(), info, ok)
}
}
if got := result.Sources(); len(got) != 3 || got[1].Destination.String() != "/items" || got[1].DestinationUnresolved {
if got := result.Sources(); len(got) != 3 || got[1].Destination.String() != "/items" || got[1].DestinationIsFallback {
t.Fatalf("final item sources = %#v", got)
}
}
@@ -372,9 +372,9 @@ func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
t.Fatalf("early failure = %v", err)
}
sources := loadError.Sources()
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationUnresolved ||
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationIsFallback ||
sources[0].PlacementMode != configkit.PlacementModeSequenceItem ||
sources[1].Destination.String() != "/items" || !sources[1].DestinationUnresolved ||
sources[1].Destination.String() != "/items" || !sources[1].DestinationIsFallback ||
sources[1].PlacementMode != configkit.PlacementModeMapping {
t.Fatalf("early source snapshots = %#v", sources)
}
@@ -399,13 +399,13 @@ func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
t.Fatalf("post-placement failure = %v", err)
}
sources = loadError.Sources()
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationUnresolved ||
sources[1].Destination.String() != "/items/0/metadata" || sources[1].DestinationUnresolved {
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationIsFallback ||
sources[1].Destination.String() != "/items/0/metadata" || sources[1].DestinationIsFallback {
t.Fatalf("post-placement source snapshots = %#v", sources)
}
firstSnapshot := sources[1]
sources[1].Destination = configkit.Keys("mutated")
if again := loadError.Sources(); len(again) != 2 || again[1].Destination.String() != firstSnapshot.Destination.String() || again[1].DestinationUnresolved {
if again := loadError.Sources(); len(again) != 2 || again[1].Destination.String() != firstSnapshot.Destination.String() || again[1].DestinationIsFallback {
t.Fatalf("failure source snapshots were aliased: %#v", again)
}
@@ -420,7 +420,7 @@ func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
if err != nil {
t.Fatalf("successful item load: %v", err)
}
if callback.Destination.String() != "/items/0/metadata" || callback.DestinationUnresolved || callback.PlacementMode != configkit.PlacementModeMapping {
if callback.Destination.String() != "/items/0/metadata" || callback.DestinationIsFallback || callback.PlacementMode != configkit.PlacementModeMapping {
t.Fatalf("callback source snapshot = %#v", callback)
}
}
@@ -450,13 +450,13 @@ func TestFacadeNestedItemFallbackUsesNearestResolvedContainer(t *testing.T) {
if len(sources) != 3 {
t.Fatalf("nested source snapshots = %#v", sources)
}
if got := sources[0]; got.Destination.String() != "/groups" || got.DestinationUnresolved || got.PlacementMode != configkit.PlacementModeSequenceItem {
if got := sources[0]; got.Destination.String() != "/groups" || got.DestinationIsFallback || got.PlacementMode != configkit.PlacementModeSequenceItem {
t.Errorf("outer item snapshot = %#v", got)
}
if got := sources[1]; got.Destination.String() != "/groups" || !got.DestinationUnresolved || got.PlacementMode != configkit.PlacementModeSequenceItem {
if got := sources[1]; got.Destination.String() != "/groups" || !got.DestinationIsFallback || got.PlacementMode != configkit.PlacementModeSequenceItem {
t.Errorf("nested item fallback = %#v", got)
}
if got := sources[2]; got.Destination.String() != "/groups" || !got.DestinationUnresolved || got.PlacementMode != configkit.PlacementModeMapping {
if got := sources[2]; got.Destination.String() != "/groups" || !got.DestinationIsFallback || got.PlacementMode != configkit.PlacementModeMapping {
t.Errorf("nested helper fallback = %#v", got)
}
}
@@ -496,7 +496,7 @@ func TestFacadeSequenceIdentityRegistrationIsDetached(t *testing.T) {
func TestFacadeSourceInfoPlacementCopiesAndZeroValues(t *testing.T) {
zero := configkit.SourceInfo{}
if zero.PlacementMode != configkit.PlacementModeMapping || zero.DestinationUnresolved {
if zero.PlacementMode != configkit.PlacementModeMapping || zero.DestinationIsFallback {
t.Fatalf("zero source info = %#v", zero)
}
loaded, err := configkit.Load[map[string]any](configkit.Mapping("mapping", map[string]any{"value": true}))
@@ -504,12 +504,12 @@ func TestFacadeSourceInfoPlacementCopiesAndZeroValues(t *testing.T) {
t.Fatal(err)
}
sources := loaded.Sources()
if len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeMapping || sources[0].DestinationUnresolved {
if len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeMapping || sources[0].DestinationIsFallback {
t.Fatalf("mapping source evidence = %#v", sources)
}
first := sources[0]
first.Destination = configkit.Keys("caller")
if again := loaded.Sources(); len(again) != 1 || again[0].Destination.String() != "" || again[0].DestinationUnresolved {
if again := loaded.Sources(); len(again) != 1 || again[0].Destination.String() != "" || again[0].DestinationIsFallback {
t.Fatalf("source evidence changed after caller mutation: %#v", again)
}
}
@@ -598,7 +598,7 @@ entries:
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.File(underlayPath), Imports: true}}}},
Definition: configkit.Definition[configuration]{
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("entries"), "id")},
Secret: func(path configkit.Path) bool {
Sensitive: func(path configkit.Path) bool {
return path.String() == "/entries/0/token" || path.String() == "/entries/1/token"
},
Resolve: func(value *configuration, resolution *configkit.Resolution) error {
@@ -611,8 +611,8 @@ entries:
if resolution.IsExplicit(configkit.Keys("entries").Index(0).Key("omitted")) {
t.Error("resolver treated an omitted field as explicit")
}
info, found := resolution.InputInfo(configkit.Keys("entries").Index(0).Key("count"))
if !found || info.Origin.Layer != configkit.LayerBase {
info, found := resolution.EntryInfo(configkit.Keys("entries").Index(0).Key("count"))
if !found || info.Origin.Layer != configkit.LayerMain {
t.Errorf("resolver current-record input info = %#v, %v", info, found)
}
for _, id := range []string{"source-3", "source-4"} {
@@ -646,7 +646,7 @@ entries:
t.Fatalf("callback sources = %#v; contexts = %#v", callbackItems, callbackContexts)
}
for _, source := range callbackItems {
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationUnresolved {
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationIsFallback {
t.Errorf("callback item source = %#v", source)
}
}

View File

@@ -5,7 +5,7 @@ import (
"testing"
"testing/fstest"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
type movedVersionItem struct {
@@ -64,7 +64,7 @@ func TestMainVersionDecodingFollowsSequenceMovement(t *testing.T) {
t.Fatal("unrelated underlay payload was treated as the main declaration")
}
for _, source := range loaded.Sources() {
if source.Layer == configkit.LayerBase && source.VersionCheck.Path.String() != path.String() {
if source.Layer == configkit.LayerMain && source.VersionEvidence.Path.String() != path.String() {
t.Fatal("effective movement changed source-local version evidence")
}
}

View File

@@ -10,7 +10,7 @@ import (
"testing"
"testing/fstest"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
func TestFacadeSourceVersionEvidenceFollowsAssembledSequenceRanges(t *testing.T) {
@@ -25,7 +25,7 @@ func TestFacadeSourceVersionEvidenceFollowsAssembledSequenceRanges(t *testing.T)
definition := configkit.Definition[map[string]any]{
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("records"), "id")},
Version: &configkit.VersionRule{Path: configkit.Keys("records").Index(0).Key("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }},
Secret: func(path configkit.Path) bool { return sensitivePath != "" && path.String() == sensitivePath },
Sensitive: func(path configkit.Path) bool { return sensitivePath != "" && path.String() == sensitivePath },
}
first := "records: [{id: first, version: first-version}]\n"
child := "records: [{id: child, version: PRIVATE-VERSION-CANARY}]\n"
@@ -61,7 +61,7 @@ func TestFacadeSourceVersionEvidenceFollowsAssembledSequenceRanges(t *testing.T)
}
}
sources := result.Sources()
check := sources[len(sources)-1].VersionCheck
check := sources[len(sources)-1].VersionEvidence
if check == nil || check.Path.String() != "/records/0/version" || check.Location == nil || check.Location.SourceID != sources[len(sources)-1].ID {
t.Fatalf("source-local evidence changed: %#v", check)
}
@@ -209,7 +209,7 @@ func TestFacadeSourceVersionEvidenceRedactsCollidingShapes(t *testing.T) {
continue
}
foundMapping = true
if check := source.VersionCheck; check == nil || !check.Version.Present || !check.Redacted || check.Version.Text != "" {
if check := source.VersionEvidence; check == nil || !check.Version.Present || !check.Redacted || check.Version.Text != "" {
t.Errorf("reverse=%v: schema-tagged header was not redacted", reverse)
}
}
@@ -265,7 +265,7 @@ func TestFacadeSourceVersionAbsenceKeepsCompleteEvidencePath(t *testing.T) {
{"unavailable index", "meta: [null, null, null, {version: main}]\n", "meta: []\n", configkit.Keys("meta").Index(3).Key("version")},
} {
for _, mode := range []configkit.SourceVersionMode{configkit.SourceVersionModeIfPresent, configkit.SourceVersionModeRequired} {
var callbackEvidence *configkit.SourceVersionCheck
var callbackEvidence *configkit.SourceVersionEvidence
loaded, err := configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte(test.main))}, configkit.LayeredOptions[map[string]any]{
SourceVersionMode: mode,
Definition: configkit.Definition[map[string]any]{
@@ -275,7 +275,7 @@ func TestFacadeSourceVersionAbsenceKeepsCompleteEvidencePath(t *testing.T) {
if !found {
t.Fatal("missing callback source")
}
callbackEvidence = source.VersionCheck
callbackEvidence = source.VersionEvidence
return nil
},
},
@@ -283,7 +283,7 @@ func TestFacadeSourceVersionAbsenceKeepsCompleteEvidencePath(t *testing.T) {
Source: configkit.Bytes("underlay", []byte(test.underlay)), Destination: configkit.Keys("under"),
}}}},
})
var evidence *configkit.SourceVersionCheck
var evidence *configkit.SourceVersionEvidence
if mode == configkit.SourceVersionModeRequired {
var failure *configkit.LoadError
if loaded != nil || !errors.As(err, &failure) {
@@ -293,12 +293,12 @@ func TestFacadeSourceVersionAbsenceKeepsCompleteEvidencePath(t *testing.T) {
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeVersionError || diagnostics[0].Path.String() != test.path.String() {
t.Fatalf("%s: required diagnostics=%#v", test.name, diagnostics)
}
evidence = failure.Sources()[1].VersionCheck
evidence = failure.Sources()[1].VersionEvidence
} else {
if err != nil {
t.Fatalf("%s: %v", test.name, err)
}
evidence = loaded.Sources()[1].VersionCheck
evidence = loaded.Sources()[1].VersionEvidence
if callbackEvidence == nil || callbackEvidence.Path.String() != test.path.String() {
t.Errorf("%s: callback path is incomplete", test.name)
}
@@ -365,7 +365,7 @@ func TestFacadeSourceVersionMissingHeaderDoesNotOverlapControl(t *testing.T) {
if child["value"] != "good" {
t.Fatal("valid sibling inheritance was not applied")
}
check := loaded.Sources()[1].VersionCheck
check := loaded.Sources()[1].VersionEvidence
if check == nil || check.Version.Present || check.Path.String() != path.String() {
t.Fatal("optional omission evidence was not retained")
}
@@ -544,16 +544,16 @@ func TestFacadeSourceVersionEvidenceIsSanitizedAndDetached(t *testing.T) {
var callbackSource configkit.SourceInfo
var callbackContext configkit.ResourceContext
definition := configkit.Definition[map[string]any]{
Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }},
Secret: func(candidate configkit.Path) bool { return candidate.String() == "/service/version" },
Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }},
Sensitive: func(candidate configkit.Path) bool { return candidate.String() == "/service/version" },
Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
var found bool
callbackSource, found = resolution.Source("source-1")
if !found {
t.Fatal("resolution callback could not query the acquired source")
}
if callbackSource.VersionCheck == nil || !callbackSource.VersionCheck.Redacted || callbackSource.VersionCheck.Version.Text != "" {
t.Fatalf("callback source did not conservatively redact evidence: %#v", callbackSource.VersionCheck)
if callbackSource.VersionEvidence == nil || !callbackSource.VersionEvidence.Redacted || callbackSource.VersionEvidence.Version.Text != "" {
t.Fatalf("callback source did not conservatively redact evidence: %#v", callbackSource.VersionEvidence)
}
callbackContext, found = resolution.SourceContext("source-1")
if !found || callbackContext.FS == nil || callbackContext.BaseDir != "." {
@@ -568,17 +568,17 @@ func TestFacadeSourceVersionEvidenceIsSanitizedAndDetached(t *testing.T) {
if err != nil || loaded == nil {
t.Fatalf("LoadLayered = %#v, %v", loaded, err)
}
if callbackSource.VersionCheck == nil || !callbackSource.VersionCheck.Redacted || callbackSource.VersionCheck.Version.Text != "" {
t.Fatalf("callback evidence lost its conservative snapshot: %#v", callbackSource.VersionCheck)
if callbackSource.VersionEvidence == nil || !callbackSource.VersionEvidence.Redacted || callbackSource.VersionEvidence.Version.Text != "" {
t.Fatalf("callback evidence lost its conservative snapshot: %#v", callbackSource.VersionEvidence)
}
callbackSource.VersionCheck.Path = configkit.Keys("mutated-callback")
callbackSource.VersionCheck.Version.Text = canary
callbackSource.VersionCheck.Location.Line = 98
callbackSource.VersionEvidence.Path = configkit.Keys("mutated-callback")
callbackSource.VersionEvidence.Version.Text = canary
callbackSource.VersionEvidence.Location.Line = 98
finalSources := loaded.Sources()
if len(finalSources) != 1 || finalSources[0].Path != path || finalSources[0].VersionCheck == nil {
if len(finalSources) != 1 || finalSources[0].Path != path || finalSources[0].VersionEvidence == nil {
t.Fatalf("final sources = %#v", finalSources)
}
check := finalSources[0].VersionCheck
check := finalSources[0].VersionEvidence
if check.Path.String() != "/version" || check.Version != (configkit.Version{Present: true}) || !check.Redacted ||
check.Location == nil || check.Location.SourceID != finalSources[0].ID || check.Location.Line != 1 {
t.Fatalf("final classified evidence = %#v", check)
@@ -594,9 +594,9 @@ func TestFacadeSourceVersionEvidenceIsSanitizedAndDetached(t *testing.T) {
check.Redacted = false
check.Version.Text = "mutated"
repeated := loaded.Sources()
if len(repeated) != 1 || repeated[0].VersionCheck.Path.String() != "/version" ||
repeated[0].VersionCheck.Location.Line != 1 || !repeated[0].VersionCheck.Redacted ||
repeated[0].VersionCheck.Version.Text != "" {
if len(repeated) != 1 || repeated[0].VersionEvidence.Path.String() != "/version" ||
repeated[0].VersionEvidence.Location.Line != 1 || !repeated[0].VersionEvidence.Redacted ||
repeated[0].VersionEvidence.Version.Text != "" {
t.Fatalf("source evidence was aliased or re-read after source removal: %#v", repeated)
}
@@ -612,14 +612,14 @@ func TestFacadeSourceVersionEvidenceIsSanitizedAndDetached(t *testing.T) {
},
},
})
if err != nil || public == nil || len(public.Sources()) != 1 || public.Sources()[0].VersionCheck == nil {
if err != nil || public == nil || len(public.Sources()) != 1 || public.Sources()[0].VersionEvidence == nil {
t.Fatalf("unclassified source evidence = %#v, %v", public, err)
}
if got := public.Sources()[0].VersionCheck; got.Redacted || got.Version.Text != publicText || !got.Version.Present {
if got := public.Sources()[0].VersionEvidence; got.Redacted || got.Version.Text != publicText || !got.Version.Present {
t.Fatalf("unclassified evidence = %#v", got)
}
if publicCallback.VersionCheck == nil || !publicCallback.VersionCheck.Redacted || publicCallback.VersionCheck.Version.Text != "" {
t.Fatalf("unclassified callback evidence was not initially conservative: %#v", publicCallback.VersionCheck)
if publicCallback.VersionEvidence == nil || !publicCallback.VersionEvidence.Redacted || publicCallback.VersionEvidence.Version.Text != "" {
t.Fatalf("unclassified callback evidence was not initially conservative: %#v", publicCallback.VersionEvidence)
}
}
@@ -650,7 +650,7 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
if checked != 1 {
t.Fatalf("main declaration checker calls = %d; error %v", checked, err)
}
check := failure.Sources()[0].VersionCheck
check := failure.Sources()[0].VersionEvidence
if check == nil || !check.Redacted || !check.Version.Present || check.Version.Text != "" {
t.Fatalf("early acquisition evidence was not conservatively redacted: %#v; sources %#v", check, failure.Sources())
}
@@ -666,14 +666,14 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
t.Fatalf("malformed version error = %v", err)
}
sources := loadError.Sources()
if len(sources) != 1 || sources[0].VersionCheck == nil || !sources[0].VersionCheck.Redacted ||
!sources[0].VersionCheck.Version.Present || sources[0].VersionCheck.Version.Text != "" || sources[0].VersionCheck.Path.String() != "/version" {
if len(sources) != 1 || sources[0].VersionEvidence == nil || !sources[0].VersionEvidence.Redacted ||
!sources[0].VersionEvidence.Version.Present || sources[0].VersionEvidence.Version.Text != "" || sources[0].VersionEvidence.Path.String() != "/version" {
t.Fatalf("failed-load source evidence = %#v", sources)
}
sources[0].VersionCheck.Path = configkit.Keys("mutated-error")
sources[0].VersionCheck.Version.Text = "mutated"
sources[0].VersionCheck.Location.Line = 99
if again := loadError.Sources()[0].VersionCheck; again.Path.String() != "/version" || again.Version.Text != "" || again.Location.Line == 99 {
sources[0].VersionEvidence.Path = configkit.Keys("mutated-error")
sources[0].VersionEvidence.Version.Text = "mutated"
sources[0].VersionEvidence.Location.Line = 99
if again := loadError.Sources()[0].VersionEvidence; again.Path.String() != "/version" || again.Version.Text != "" || again.Location.Line == 99 {
t.Fatalf("LoadError source evidence was aliased: %#v", again)
}
for _, test := range []struct {
@@ -707,7 +707,7 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
if strings.Contains(err.Error(), test.canary) {
t.Fatalf("late failure disclosed source-version canary: %v", err)
}
check := failure.Sources()[0].VersionCheck
check := failure.Sources()[0].VersionEvidence
if check == nil || !check.Redacted || !check.Version.Present || check.Version.Text != "" {
t.Fatalf("late-failure evidence was not conservatively redacted: %#v", check)
}
@@ -715,7 +715,7 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
}
defaultResult, err := configkit.Load(configkit.Bytes("default", []byte("version: 7\n")), configkit.Options[map[string]any]{})
if err != nil || defaultResult == nil || len(defaultResult.Sources()) != 1 || defaultResult.Sources()[0].VersionCheck != nil {
if err != nil || defaultResult == nil || len(defaultResult.Sources()) != 1 || defaultResult.Sources()[0].VersionEvidence != nil {
t.Fatalf("default source version check = %#v, %v", defaultResult, err)
}
@@ -728,7 +728,7 @@ func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.
}
sawCLI := false
for _, source := range withCLI.Sources() {
if source.Kind == configkit.SourceKindCLI && source.VersionCheck != nil {
if source.Kind == configkit.SourceKindCLI && source.VersionEvidence != nil {
t.Fatalf("synthetic CLI source acquired declaration evidence: %#v", source)
}
sawCLI = sawCLI || source.Kind == configkit.SourceKindCLI
@@ -764,14 +764,14 @@ func TestFacadeSourceVersionEvidencePreservesAbsenceAndNull(t *testing.T) {
t.Fatalf("checker calls = %#v; absent contribution must skip while null is checked", checked)
}
sources := loaded.Sources()
if len(sources) != 3 || sources[1].VersionCheck == nil || sources[2].VersionCheck == nil {
if len(sources) != 3 || sources[1].VersionEvidence == nil || sources[2].VersionEvidence == nil {
t.Fatalf("source version evidence = %#v", sources)
}
absent := sources[1].VersionCheck
absent := sources[1].VersionEvidence
if absent.Version.Present || absent.Path.String() != "/version" || absent.Location == nil || absent.Redacted {
t.Fatalf("optional omission evidence = %#v", absent)
}
null := sources[2].VersionCheck
null := sources[2].VersionEvidence
if !null.Version.Present || !null.Version.Null || null.Version.Text != "" || null.Path.String() != "/version" {
t.Fatalf("null declaration evidence = %#v", null)
}
@@ -807,7 +807,7 @@ func TestFacadeSourceVersionLifecycleKeepsConsumedHeadersOutOfPayloadHistory(t *
},
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
current.Service.Resolved = "resolved once"
resolution.Record(configkit.Keys("service", "resolved"), configkit.ChangeNormalization, "derive resolved value")
resolution.RecordNormalization(configkit.Keys("service", "resolved"), "derive resolved value")
return nil
},
Validate: func(current *configuration, validation *configkit.Validation) {
@@ -865,8 +865,8 @@ func TestFacadeSourceVersionLifecycleKeepsConsumedHeadersOutOfPayloadHistory(t *
t.Fatalf("consumed declarations affected effective rendering:\n%s\n%s", firstEffective.String(), secondEffective.String())
}
sourcesA, sourcesB := first.Sources(), second.Sources()
if len(sourcesA) != 2 || len(sourcesB) != 2 || sourcesA[1].VersionCheck == nil || sourcesB[1].VersionCheck == nil ||
sourcesA[1].VersionCheck.Version.Text != "underlay-release-a" || sourcesB[1].VersionCheck.Version.Text != "underlay-release-b" {
if len(sourcesA) != 2 || len(sourcesB) != 2 || sourcesA[1].VersionEvidence == nil || sourcesB[1].VersionEvidence == nil ||
sourcesA[1].VersionEvidence.Version.Text != "underlay-release-a" || sourcesB[1].VersionEvidence.Version.Text != "underlay-release-b" {
t.Fatalf("sanitized evidence did not retain distinct accepted declarations: %#v / %#v", sourcesA, sourcesB)
}
versionExplanation, found := first.Explain(configkit.Keys("service", "version"))

View 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)
}
}
})
}
}

View File

@@ -5,7 +5,7 @@ import (
"testing"
"testing/fstest"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
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 {
for _, pointer := range paths {
path, _ := configkit.ParsePath(pointer)
info, ok := resolution.InputInfo(path)
info, ok := resolution.EntryInfo(path)
if !ok {
t.Errorf("missing resolver input %s", pointer)
}

View File

@@ -5,7 +5,7 @@ import (
"strings"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
configkit "go.maximumdirect.net/configkit"
)
type versionCountingReader struct {

2
go.mod
View File

@@ -1,4 +1,4 @@
module gitea.maximumdirect.net/eric/ConfigKit
module go.maximumdirect.net/configkit
go 1.26.0

View File

@@ -44,10 +44,10 @@ func TestDependencyImportBoundary(t *testing.T) {
production := !strings.HasSuffix(path, "_test.go")
rootProduction := production && filepath.Dir(path) == "."
internalProduction := production && strings.HasPrefix(filepath.ToSlash(path), "internal/")
if rootProduction && strings.HasPrefix(importPath, "gitea.maximumdirect.net/eric/ConfigKit/internal/") && importPath != "gitea.maximumdirect.net/eric/ConfigKit/internal/engine" {
if rootProduction && strings.HasPrefix(importPath, "go.maximumdirect.net/configkit/internal/") && importPath != "go.maximumdirect.net/configkit/internal/engine" {
t.Errorf("%s bypasses the engine boundary through %s", path, importPath)
}
if internalProduction && importPath == "gitea.maximumdirect.net/eric/ConfigKit" {
if internalProduction && importPath == "go.maximumdirect.net/configkit" {
t.Errorf("%s imports the public root package", path)
}
}

View File

@@ -3,7 +3,7 @@ package configkit
import (
"io"
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
"go.maximumdirect.net/configkit/internal/engine"
)
// ValueSnapshot is a detached, deterministic YAML representation of one value.

View File

@@ -4,7 +4,7 @@ import (
"strconv"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
func BenchmarkComposeWideMappings(b *testing.B) {

View File

@@ -20,9 +20,9 @@ func TestCallbackAssignmentsAreDetachedAcrossLoads(t *testing.T) {
definition := Definition[cfg]{Resolve: func(c *cfg, r *Resolution) error {
Normalize(r, Keys("map"), &c.Map, shared, "populate")
c.Pointer = external
r.Record(Keys("pointer"), ChangeNormalization, "populate")
r.RecordNormalization(Keys("pointer"), "populate")
c.Secret = SecretEnv("CONFIGKIT_CALLBACK_SECRET")
r.Record(Keys("secret"), ChangeNormalization, "reference")
r.RecordNormalization(Keys("secret"), "reference")
return nil
}}
if validatorAssignment {

View File

@@ -5,7 +5,7 @@ import (
"strings"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
func TestOverlayBroadCollectionPoliciesAndRetainedDeclarations(t *testing.T) {

View File

@@ -3,7 +3,7 @@ package engine
import (
"sort"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
type collectionPolicyScope struct {

View File

@@ -4,7 +4,7 @@ import (
"reflect"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
func TestCollectionPolicyWithoutValueRetainsIdentityAndMode(t *testing.T) {

View File

@@ -1,6 +1,6 @@
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;
// false records ordinary ancestor replacement without selecting a collection

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
func TestSourceReplacementRetiresIdentityForEveryValueKind(t *testing.T) {

View File

@@ -3,7 +3,7 @@ package engine
import (
"maps"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
// composedDocument is the private handoff between source assembly and later
@@ -223,7 +223,7 @@ func composeDocumentsWithCollectionPolicy(
rangesBySource[assigned.sourceIndex] = append(rangesBySource[assigned.sourceIndex], assigned)
}
for sourceIndex, input := range inputs {
application := historyApplication{layer: LayerBase, value: input}
application := historyApplication{layer: LayerMain, value: input}
for _, assigned := range rangesBySource[sourceIndex] {
pointer := assigned.path.String()
mode := sequenceHistoryAppend
@@ -255,7 +255,7 @@ func composeDocumentsWithCollectionPolicy(
func completeCompositionOrigins(node *document.Node, path Path, origins map[string]Origin) {
pointer := path.String()
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 {
case document.Mapping:
@@ -513,7 +513,7 @@ func projectedCompositionOrigins(inputs []*document.Node, ranges []sequenceRange
walkHistoryApplication(application, func(_, effective Path, node *document.Node) {
pointer := effective.String()
origin := origins[pointer]
origin.Layer = LayerBase
origin.Layer = LayerMain
origin.Locations = append(origin.Locations, publicLocation(node.Location))
origins[pointer] = origin
})

View File

@@ -6,7 +6,7 @@ import (
"strings"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
func TestComposeWideRegisteredCollectionsPreservesEvidenceAndReplay(t *testing.T) {
@@ -92,7 +92,7 @@ values: [one, two]
{SourceID: "source-1", 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)
}
wantClaimPaths := []string{"", "/services", "/services/api", "/services/api/enabled", "/empty", "", "/services", "/services/worker", "/services/worker/enabled", "/values"}

View File

@@ -5,8 +5,8 @@ import (
"reflect"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml"
"go.maximumdirect.net/configkit/internal/document"
yamladapter "go.maximumdirect.net/configkit/internal/yaml"
)
func FuzzCompositionAndOverlay(f *testing.F) {

View File

@@ -8,7 +8,7 @@ import (
"strconv"
"time"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
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) {
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 {
origins[path.String()] = Origin{Layer: LayerDefault}
}

View File

@@ -8,7 +8,7 @@ import (
"testing"
"time"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
type namedSmall int8
@@ -141,7 +141,7 @@ custom: accepted
if _, ok := decoded.presence[Keys("nested", "a").String()]; ok {
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)
}
if decoded.origins[Keys("nested", "b").String()].Locations[0].Line == 0 {

View File

@@ -3,7 +3,7 @@ package engine
import (
"errors"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
var (

View File

@@ -6,7 +6,7 @@ import (
"testing"
"testing/fstest"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
func TestDestinationPlacementCountsAgainstSharedNodeBudget(t *testing.T) {

View File

@@ -70,7 +70,7 @@ func TestRelativeImportsFeedFinalInheritanceAndInspectionLifecycle(t *testing.T)
child := config.Catalog.Entries["child"]
child.Derived = child.Label + ":" + child.Dynamic + ":" + fmt.Sprint(child.Height)
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
},
Validate: func(config *configuration, validation *Validation) {

View File

@@ -5,7 +5,7 @@ import (
"testing"
"testing/fstest"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
type importDestinationConfig struct {

View File

@@ -287,7 +287,7 @@ func TestLayeredDestinationLifecycleAndSensitiveInspectionUseFinalShape(t *testi
Resolve: func(configuration *destinationLifecycleConfig, resolution *Resolution) error {
observed.resolve++
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
},
Validate: func(configuration *destinationLifecycleConfig, _ *Validation) {
@@ -425,7 +425,7 @@ func TestLayeredDestinationDiffUsesBilateralSensitivity(t *testing.T) {
definition: Definition[config]{
Resolve: func(configuration *config, resolution *Resolution) error {
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
},
Secret: func(path Path) bool { return classify && path.String() == "/tenant/dynamic" },

View File

@@ -5,7 +5,7 @@ import (
"reflect"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
func TestNormalizeDestinationUsesLiteralMappingKeys(t *testing.T) {

View File

@@ -50,6 +50,16 @@ func (e *LoadError) Sources() []SourceInfo {
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
// control characters and propagates writer errors, including short writes.
func WriteDiagnostics(output io.Writer, diagnostics []Diagnostic) error {
@@ -88,7 +98,9 @@ func (a *diagnosticAccumulator) loadError() *LoadError {
func canonicalDiagnostics(diagnostics []Diagnostic) []Diagnostic {
canonical := cloneDiagnostics(diagnostics)
for i := range canonical {
slices.SortFunc(canonical[i].Related, compareLocation)
if canonical[i].Code != CodeComposeConflict {
slices.SortFunc(canonical[i].Related, compareLocation)
}
}
type orderedDiagnostic struct {
diagnostic Diagnostic

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"io"
"slices"
"strings"
"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) {
t.Parallel()

View File

@@ -5,8 +5,8 @@ import (
"errors"
"io"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml"
"go.maximumdirect.net/configkit/internal/document"
yamladapter "go.maximumdirect.net/configkit/internal/yaml"
)
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)
regions := make([]diffRegion, 0, len(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 {
leftNode = nil
}
rightNode, rightPresent := documentAtPathWithLookup(right.final, delta.path, right.immutableLookup)
rightNode, rightPresent := documentAtPathWithLookup(right.final, delta.path, right.documentLookup())
if !rightPresent {
rightNode = nil
}

View File

@@ -3,7 +3,7 @@ package engine
import (
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
func TestPreparePairedDiffSnapshotReportsWhollyMaskedChanges(t *testing.T) {

View File

@@ -6,7 +6,7 @@ import (
"strings"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
type diffIntegrationConfig struct {
@@ -29,7 +29,7 @@ func TestDiffUsesFinalValuesAndDependencyDerivedSensitivity(t *testing.T) {
Resolve: func(configuration *diffIntegrationConfig, resolution *Resolution) error {
resolveCalls++
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
},
}}
@@ -53,7 +53,7 @@ func TestDiffUsesFinalValuesAndDependencyDerivedSensitivity(t *testing.T) {
!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)
}
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)
}
@@ -138,10 +138,10 @@ func TestDiffUsesReferenceNamesAndFrozenSecretState(t *testing.T) {
if err != nil || len(differences) != 0 {
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)
}
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)
}
@@ -164,7 +164,7 @@ func TestDiffUsesReferenceNamesAndFrozenSecretState(t *testing.T) {
if absent.Config.Optional != nil {
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)
}
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) {
left := load("dynamic: {token: removed-canary}\n", func(configuration *configuration, resolution *Resolution) error {
delete(configuration.Dynamic, "token")
resolution.Record(secretPath, ChangeNormalization, "remove token")
resolution.RecordNormalization(secretPath, "remove token")
return nil
})
right := load("dynamic: {token: current-canary}\n", nil)
if !left.Inspection.secrets[secretPath.String()] {
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")
}
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) {
left := load("dynamic: {token: original-canary}\n", func(configuration *configuration, resolution *Resolution) error {
configuration.Dynamic["token"] = "replacement-canary"
resolution.Record(secretPath, ChangeNormalization, "replace token")
resolution.RecordNormalization(secretPath, "replace token")
return nil
})
right := load("dynamic: {token: other-canary}\n", nil)

View File

@@ -3,7 +3,7 @@ package engine
import (
"sort"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
type pairedDiffSnapshot struct {

View File

@@ -3,7 +3,7 @@ package engine
import (
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
func TestPreparePairedDiffSnapshotRedactsAlignedMappingFromEitherSide(t *testing.T) {

View File

@@ -3,7 +3,7 @@ package engine
import (
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
func TestPrepareDiffRegionsUsesCanonicalConfigurationComparison(t *testing.T) {
@@ -127,7 +127,10 @@ type expectedDiffRegion struct {
}
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) {

View File

@@ -4,7 +4,7 @@ import (
"path"
"strings"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
)
const directiveKey = "configkit"

View File

@@ -481,7 +481,7 @@ func resolveSearchInput(source Source, sourceID string, helpers discoveryDirecto
if len(diagnostics) == 0 || selected.kind != sourceFile {
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 {
diagnostics[index].Primary = &Location{SourceID: sourceID}
}

View File

@@ -77,7 +77,7 @@ settings: {underlay: value}
Keys("settings", "main"), Keys("settings", "underlay"), Keys("settings", "imported"))
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)
}
}

View File

@@ -2,7 +2,7 @@ package engine
import (
"cmp"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
"go.maximumdirect.net/configkit/internal/document"
"slices"
)

View File

@@ -1,6 +1,6 @@
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.
type documentLookup struct {

View File

@@ -1,6 +1,6 @@
package engine
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
import "go.maximumdirect.net/configkit/internal/document"
// walkDocument visits value nodes in preorder, preserving stored mapping order
// and sequence index order. Mapping keys are not separate nodes in this walk.

Some files were not shown because too many files have changed in this diff Show More