Separate engine loading from inspection state
This commit is contained in:
@@ -345,7 +345,7 @@ generic result before lifecycle delegation begins.
|
||||
|
||||
## Stage 5 — Separate engine publication and inspection
|
||||
|
||||
Status: not started.
|
||||
Status: complete (2026-09-12).
|
||||
|
||||
1. Split generic engine Result into the short-lived loaded configuration carrier
|
||||
and non-generic inspection state specified above. Move inspection methods to
|
||||
@@ -363,6 +363,23 @@ configuration detachment but does not store Config in inspection. Run
|
||||
`go test ./internal/engine -run 'Load|Metadata|Provenance|Historical|Explain|Effective|Render|Independence'`,
|
||||
`go test -race ./internal/engine`, and the everyday check.
|
||||
|
||||
### Completion
|
||||
|
||||
Replaced the engine's generic result with `Loaded[T]`, a short-lived carrier of
|
||||
the detached configuration and a non-generic `Inspection`. `Inspection` now
|
||||
owns the frozen metadata, sources, diagnostics, document snapshots, history,
|
||||
resolution snapshots, and sensitivity state; its query, explanation, and
|
||||
rendering methods retain their previous behavior. The published configuration
|
||||
is detached as before and is not retained by `Inspection`.
|
||||
|
||||
Engine tests use a test-only adapter while exercising the moved inspection
|
||||
receiver, and a direct `Loaded` test verifies that mutating `Config` cannot
|
||||
change its frozen effective view. Linux validation with Go 1.26.7 passed
|
||||
`go test ./internal/engine -run 'Load|Metadata|Provenance|Historical|Explain|Effective|Render|Independence'`,
|
||||
`go test -race ./internal/engine`, and `./scripts/check.sh`. There were no
|
||||
deviations or remaining risks. The next handoff wires the candidate facade's
|
||||
lifecycle and callback wrappers to the split engine result.
|
||||
|
||||
## Stage 6 — Wire candidate lifecycle and callback wrappers
|
||||
|
||||
Status: not started.
|
||||
|
||||
@@ -36,7 +36,7 @@ type Explanation struct {
|
||||
|
||||
// Explain returns safe effective and chronological values for a known final or
|
||||
// historical path. Unknown and malformed paths return false.
|
||||
func (r *Result[T]) Explain(path Path) (Explanation, bool) {
|
||||
func (r *Inspection) Explain(path Path) (Explanation, bool) {
|
||||
if r == nil || r.final == nil || r.baseline == nil || !r.metadata.hasHistoricalPath(path) {
|
||||
return Explanation{}, false
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ var (
|
||||
|
||||
// WriteExplanation writes a deterministic human-readable rendering of Explain.
|
||||
// Unknown paths produce no output. Writer failures may leave partial output.
|
||||
func (r *Result[T]) WriteExplanation(output io.Writer, path Path) error {
|
||||
func (r *Inspection) WriteExplanation(output io.Writer, path Path) error {
|
||||
if r == nil || output == nil {
|
||||
return errExplanationUnavailable
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestParallelLoadsShareRulesButNotMutableValues(t *testing.T) {
|
||||
}
|
||||
|
||||
const count = 32
|
||||
results := make([]*Result[independentConfig], count)
|
||||
results := make([]*testResult[independentConfig], count)
|
||||
errorsByIndex := make([]error, count)
|
||||
var group sync.WaitGroup
|
||||
for index := range count {
|
||||
@@ -73,6 +73,29 @@ func TestParallelLoadsShareRulesButNotMutableValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadedConfigurationIsSeparateFromInspection(t *testing.T) {
|
||||
type configuration struct {
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
|
||||
loaded, err := Load[configuration](Bytes("config.yml", []byte("value: original\n")), testSecretBinding())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if loaded.Inspection == nil {
|
||||
t.Fatal("inspection is nil")
|
||||
}
|
||||
|
||||
loaded.Config.Value = "mutated"
|
||||
var effective bytes.Buffer
|
||||
if err := loaded.Inspection.WriteEffective(&effective); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if effective.String() != "value: original\n" {
|
||||
t.Fatalf("inspection changed with configuration: %q", effective.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentLoadsOfCopiedReaderSourceHaveOneClaim(t *testing.T) {
|
||||
source := Reader("shared", strings.NewReader("value: one\n"))
|
||||
copied := source
|
||||
|
||||
@@ -66,10 +66,15 @@ type Version struct {
|
||||
Text string
|
||||
}
|
||||
|
||||
// Result is a successful independently owned configuration load.
|
||||
type Result[T any] struct {
|
||||
Config T
|
||||
// Loaded is a short-lived generic carrier for a successful configuration load.
|
||||
// Inspection is frozen independently of Config and does not retain application values.
|
||||
type Loaded[T any] struct {
|
||||
Config T
|
||||
Inspection *Inspection
|
||||
}
|
||||
|
||||
// Inspection owns frozen load snapshots and detached query state.
|
||||
type Inspection struct {
|
||||
metadata Metadata
|
||||
sources []SourceInfo
|
||||
diagnostics []Diagnostic
|
||||
@@ -84,7 +89,7 @@ type Result[T any] struct {
|
||||
}
|
||||
|
||||
// Metadata returns detached effective presence, origin, and history records.
|
||||
func (r *Result[T]) Metadata() Metadata {
|
||||
func (r *Inspection) Metadata() Metadata {
|
||||
if r == nil {
|
||||
return Metadata{}
|
||||
}
|
||||
@@ -92,7 +97,7 @@ func (r *Result[T]) Metadata() Metadata {
|
||||
}
|
||||
|
||||
// Sources returns a detached list of sources used by the load.
|
||||
func (r *Result[T]) Sources() []SourceInfo {
|
||||
func (r *Inspection) Sources() []SourceInfo {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -100,7 +105,7 @@ func (r *Result[T]) Sources() []SourceInfo {
|
||||
}
|
||||
|
||||
// Diagnostics returns detached nonfatal diagnostics from the load.
|
||||
func (r *Result[T]) Diagnostics() []Diagnostic {
|
||||
func (r *Inspection) Diagnostics() []Diagnostic {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -110,11 +115,11 @@ func (r *Result[T]) Diagnostics() []Diagnostic {
|
||||
// Load parses and strictly decodes zero or one selected source, explicitly enabled
|
||||
// imports, and one selected profile. It accepts at most one Options value and
|
||||
// returns no result when any error is reported.
|
||||
func Load[T any](source Source, secrets SecretBinding, supplied ...Options[T]) (*Result[T], error) {
|
||||
func Load[T any](source Source, secrets SecretBinding, supplied ...Options[T]) (*Loaded[T], error) {
|
||||
return loadWithSecretLookup(source, secrets, os.LookupEnv, supplied...)
|
||||
}
|
||||
|
||||
func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup secretLookup, supplied ...Options[T]) (*Result[T], error) {
|
||||
func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup secretLookup, supplied ...Options[T]) (*Loaded[T], error) {
|
||||
accumulator := diagnosticAccumulator{}
|
||||
options, diagnostics := selectOptions(source, supplied)
|
||||
accumulator.add(diagnostics...)
|
||||
|
||||
@@ -80,7 +80,7 @@ func detachLoadConfiguration[T any](schema *schemaDescriptor, configuration T, m
|
||||
return detached.Interface().(T), nil
|
||||
}
|
||||
|
||||
func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T], resolved resolvedLoad[T], references map[string]string, accumulator *diagnosticAccumulator) (*Result[T], error) {
|
||||
func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T], resolved resolvedLoad[T], references map[string]string, accumulator *diagnosticAccumulator) (*Loaded[T], error) {
|
||||
// A read-only validator can replace a value with an equal borrowed object.
|
||||
// Detach without re-reading the environment or discarding validated state.
|
||||
configuration, issue := detachLoadConfiguration(schema, resolved.configuration, cloneValidatedSecrets)
|
||||
@@ -92,8 +92,7 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T]
|
||||
final, applications, resolutionSnapshots := resolved.final, resolved.applications, resolved.snapshots
|
||||
finalizeMetadataPaths(&metadata, final, applications, decoded.baseline, decoded.final)
|
||||
secrets := classifySecretPaths(schema, definition.Secret, metadata, final, applications, decoded.baseline, decoded.final)
|
||||
return &Result[T]{
|
||||
Config: configuration,
|
||||
inspection := &Inspection{
|
||||
metadata: metadata,
|
||||
sources: cloneSourceInfos(accumulator.sources),
|
||||
diagnostics: canonicalDiagnostics(accumulator.diagnostics),
|
||||
@@ -105,7 +104,8 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T]
|
||||
applications: applications,
|
||||
secrets: secrets,
|
||||
secretReferences: cloneSecretReferences(references),
|
||||
}, nil
|
||||
}
|
||||
return &Loaded[T]{Config: configuration, Inspection: inspection}, nil
|
||||
}
|
||||
|
||||
func diagnosticFromSchemaIssue(issue *schemaIssue) Diagnostic {
|
||||
|
||||
@@ -166,7 +166,7 @@ func TestSelectedProfileLoadsRemainIndependentConcurrently(t *testing.T) {
|
||||
options := Options[layeredConfig]{Profile: "testing"}
|
||||
|
||||
const loads = 16
|
||||
results := make(chan *Result[layeredConfig], loads)
|
||||
results := make(chan *testResult[layeredConfig], loads)
|
||||
errors := make(chan error, loads)
|
||||
var group sync.WaitGroup
|
||||
for index := 0; index < loads; index++ {
|
||||
@@ -187,7 +187,7 @@ func TestSelectedProfileLoadsRemainIndependentConcurrently(t *testing.T) {
|
||||
for err := range errors {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var collected []*Result[layeredConfig]
|
||||
var collected []*testResult[layeredConfig]
|
||||
for result := range results {
|
||||
collected = append(collected, result)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ var errEffectiveUnavailable = errors.New("configkit: effective configuration is
|
||||
// WriteEffective writes deterministic redacted YAML from the frozen resolved
|
||||
// snapshot. The output is intended for inspection and is not promised reloadable.
|
||||
// Writer errors, including short writes, are returned and may leave partial output.
|
||||
func (r *Result[T]) WriteEffective(output io.Writer) error {
|
||||
func (r *Inspection) WriteEffective(output io.Writer) error {
|
||||
if r == nil || r.final == nil || output == nil {
|
||||
return errEffectiveUnavailable
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ sequence: [second, first]
|
||||
if err := result.WriteEffective(shortWriter{}); !errors.Is(err, io.ErrShortWrite) {
|
||||
t.Fatalf("short writer error = %v", err)
|
||||
}
|
||||
var nilResult *Result[renderConfig]
|
||||
var nilResult *testResult[renderConfig]
|
||||
if err := nilResult.WriteEffective(io.Discard); !errors.Is(err, errEffectiveUnavailable) {
|
||||
t.Fatalf("nil result error = %v", err)
|
||||
}
|
||||
|
||||
@@ -440,7 +440,7 @@ func TestValidationContextPanicsAfterVersionCallback(t *testing.T) {
|
||||
retained.Warning(Keys("version"), "too late")
|
||||
}
|
||||
|
||||
func assertLoadDiagnostic[T any](t *testing.T, result *Result[T], err error, code Code) *LoadError {
|
||||
func assertLoadDiagnostic[T any](t *testing.T, result *testResult[T], err error, code Code) *LoadError {
|
||||
t.Helper()
|
||||
if result != nil || err == nil {
|
||||
t.Fatalf("result, err = %#v, %v", result, err)
|
||||
|
||||
@@ -58,12 +58,62 @@ func testSecretBinding() SecretBinding {
|
||||
}
|
||||
}
|
||||
|
||||
func testLoad[T any](source Source, supplied ...Options[T]) (*Result[T], error) {
|
||||
return Load(source, testSecretBinding(), supplied...)
|
||||
func testLoad[T any](source Source, supplied ...Options[T]) (*testResult[T], error) {
|
||||
loaded, err := Load(source, testSecretBinding(), supplied...)
|
||||
return testLoaded(loaded), err
|
||||
}
|
||||
|
||||
func testLoadWithSecretLookup[T any](source Source, lookup secretLookup, supplied ...Options[T]) (*Result[T], error) {
|
||||
return loadWithSecretLookup(source, testSecretBinding(), lookup, supplied...)
|
||||
func testLoadWithSecretLookup[T any](source Source, lookup secretLookup, supplied ...Options[T]) (*testResult[T], error) {
|
||||
loaded, err := loadWithSecretLookup(source, testSecretBinding(), lookup, supplied...)
|
||||
return testLoaded(loaded), err
|
||||
}
|
||||
|
||||
type testResult[T any] struct {
|
||||
Config T
|
||||
*Inspection
|
||||
}
|
||||
|
||||
func testLoaded[T any](loaded *Loaded[T]) *testResult[T] {
|
||||
if loaded == nil {
|
||||
return nil
|
||||
}
|
||||
return &testResult[T]{Config: loaded.Config, Inspection: loaded.Inspection}
|
||||
}
|
||||
func (r *testResult[T]) Metadata() Metadata {
|
||||
if r == nil {
|
||||
return Metadata{}
|
||||
}
|
||||
return r.Inspection.Metadata()
|
||||
}
|
||||
func (r *testResult[T]) Sources() []SourceInfo {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return r.Inspection.Sources()
|
||||
}
|
||||
func (r *testResult[T]) Diagnostics() []Diagnostic {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return r.Inspection.Diagnostics()
|
||||
}
|
||||
func (r *testResult[T]) Explain(path Path) (Explanation, bool) {
|
||||
if r == nil {
|
||||
return Explanation{}, false
|
||||
}
|
||||
return r.Inspection.Explain(path)
|
||||
}
|
||||
func (r *testResult[T]) WriteEffective(output io.Writer) error {
|
||||
if r == nil {
|
||||
return errEffectiveUnavailable
|
||||
}
|
||||
return r.Inspection.WriteEffective(output)
|
||||
}
|
||||
func (r *testResult[T]) WriteExplanation(output io.Writer, path Path) error {
|
||||
if r == nil {
|
||||
return errExplanationUnknownPath
|
||||
}
|
||||
return r.Inspection.WriteExplanation(output, path)
|
||||
}
|
||||
|
||||
func testBuildSchema(root reflect.Type) (*schemaDescriptor, *schemaIssue) {
|
||||
|
||||
Reference in New Issue
Block a user