Separate engine loading from inspection state

This commit is contained in:
2026-09-12 16:12:01 +00:00
parent 05812a6ad3
commit b8935e77bd
11 changed files with 120 additions and 25 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {