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
|
## 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
|
1. Split generic engine Result into the short-lived loaded configuration carrier
|
||||||
and non-generic inspection state specified above. Move inspection methods to
|
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 ./internal/engine -run 'Load|Metadata|Provenance|Historical|Explain|Effective|Render|Independence'`,
|
||||||
`go test -race ./internal/engine`, and the everyday check.
|
`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
|
## Stage 6 — Wire candidate lifecycle and callback wrappers
|
||||||
|
|
||||||
Status: not started.
|
Status: not started.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ type Explanation struct {
|
|||||||
|
|
||||||
// Explain returns safe effective and chronological values for a known final or
|
// Explain returns safe effective and chronological values for a known final or
|
||||||
// historical path. Unknown and malformed paths return false.
|
// 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) {
|
if r == nil || r.final == nil || r.baseline == nil || !r.metadata.hasHistoricalPath(path) {
|
||||||
return Explanation{}, false
|
return Explanation{}, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ var (
|
|||||||
|
|
||||||
// WriteExplanation writes a deterministic human-readable rendering of Explain.
|
// WriteExplanation writes a deterministic human-readable rendering of Explain.
|
||||||
// Unknown paths produce no output. Writer failures may leave partial output.
|
// Unknown paths 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 {
|
if r == nil || output == nil {
|
||||||
return errExplanationUnavailable
|
return errExplanationUnavailable
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func TestParallelLoadsShareRulesButNotMutableValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const count = 32
|
const count = 32
|
||||||
results := make([]*Result[independentConfig], count)
|
results := make([]*testResult[independentConfig], count)
|
||||||
errorsByIndex := make([]error, count)
|
errorsByIndex := make([]error, count)
|
||||||
var group sync.WaitGroup
|
var group sync.WaitGroup
|
||||||
for index := range count {
|
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) {
|
func TestConcurrentLoadsOfCopiedReaderSourceHaveOneClaim(t *testing.T) {
|
||||||
source := Reader("shared", strings.NewReader("value: one\n"))
|
source := Reader("shared", strings.NewReader("value: one\n"))
|
||||||
copied := source
|
copied := source
|
||||||
|
|||||||
@@ -66,10 +66,15 @@ type Version struct {
|
|||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Result is a successful independently owned configuration load.
|
// Loaded is a short-lived generic carrier for a successful configuration load.
|
||||||
type Result[T any] struct {
|
// Inspection is frozen independently of Config and does not retain application values.
|
||||||
Config T
|
type Loaded[T any] struct {
|
||||||
|
Config T
|
||||||
|
Inspection *Inspection
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inspection owns frozen load snapshots and detached query state.
|
||||||
|
type Inspection struct {
|
||||||
metadata Metadata
|
metadata Metadata
|
||||||
sources []SourceInfo
|
sources []SourceInfo
|
||||||
diagnostics []Diagnostic
|
diagnostics []Diagnostic
|
||||||
@@ -84,7 +89,7 @@ type Result[T any] struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Metadata returns detached effective presence, origin, and history records.
|
// Metadata returns detached effective presence, origin, and history records.
|
||||||
func (r *Result[T]) Metadata() Metadata {
|
func (r *Inspection) Metadata() Metadata {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return Metadata{}
|
return Metadata{}
|
||||||
}
|
}
|
||||||
@@ -92,7 +97,7 @@ func (r *Result[T]) Metadata() Metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sources returns a detached list of sources used by the load.
|
// 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 {
|
if r == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -100,7 +105,7 @@ func (r *Result[T]) Sources() []SourceInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Diagnostics returns detached nonfatal diagnostics from the load.
|
// Diagnostics returns detached nonfatal diagnostics from the load.
|
||||||
func (r *Result[T]) Diagnostics() []Diagnostic {
|
func (r *Inspection) Diagnostics() []Diagnostic {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return 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
|
// 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
|
// imports, and one selected profile. It accepts at most one Options value and
|
||||||
// returns no result when any error is reported.
|
// 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...)
|
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{}
|
accumulator := diagnosticAccumulator{}
|
||||||
options, diagnostics := selectOptions(source, supplied)
|
options, diagnostics := selectOptions(source, supplied)
|
||||||
accumulator.add(diagnostics...)
|
accumulator.add(diagnostics...)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func detachLoadConfiguration[T any](schema *schemaDescriptor, configuration T, m
|
|||||||
return detached.Interface().(T), nil
|
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.
|
// A read-only validator can replace a value with an equal borrowed object.
|
||||||
// Detach without re-reading the environment or discarding validated state.
|
// Detach without re-reading the environment or discarding validated state.
|
||||||
configuration, issue := detachLoadConfiguration(schema, resolved.configuration, cloneValidatedSecrets)
|
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
|
final, applications, resolutionSnapshots := resolved.final, resolved.applications, resolved.snapshots
|
||||||
finalizeMetadataPaths(&metadata, final, applications, decoded.baseline, decoded.final)
|
finalizeMetadataPaths(&metadata, final, applications, decoded.baseline, decoded.final)
|
||||||
secrets := classifySecretPaths(schema, definition.Secret, metadata, final, applications, decoded.baseline, decoded.final)
|
secrets := classifySecretPaths(schema, definition.Secret, metadata, final, applications, decoded.baseline, decoded.final)
|
||||||
return &Result[T]{
|
inspection := &Inspection{
|
||||||
Config: configuration,
|
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
sources: cloneSourceInfos(accumulator.sources),
|
sources: cloneSourceInfos(accumulator.sources),
|
||||||
diagnostics: canonicalDiagnostics(accumulator.diagnostics),
|
diagnostics: canonicalDiagnostics(accumulator.diagnostics),
|
||||||
@@ -105,7 +104,8 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T]
|
|||||||
applications: applications,
|
applications: applications,
|
||||||
secrets: secrets,
|
secrets: secrets,
|
||||||
secretReferences: cloneSecretReferences(references),
|
secretReferences: cloneSecretReferences(references),
|
||||||
}, nil
|
}
|
||||||
|
return &Loaded[T]{Config: configuration, Inspection: inspection}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func diagnosticFromSchemaIssue(issue *schemaIssue) Diagnostic {
|
func diagnosticFromSchemaIssue(issue *schemaIssue) Diagnostic {
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ func TestSelectedProfileLoadsRemainIndependentConcurrently(t *testing.T) {
|
|||||||
options := Options[layeredConfig]{Profile: "testing"}
|
options := Options[layeredConfig]{Profile: "testing"}
|
||||||
|
|
||||||
const loads = 16
|
const loads = 16
|
||||||
results := make(chan *Result[layeredConfig], loads)
|
results := make(chan *testResult[layeredConfig], loads)
|
||||||
errors := make(chan error, loads)
|
errors := make(chan error, loads)
|
||||||
var group sync.WaitGroup
|
var group sync.WaitGroup
|
||||||
for index := 0; index < loads; index++ {
|
for index := 0; index < loads; index++ {
|
||||||
@@ -187,7 +187,7 @@ func TestSelectedProfileLoadsRemainIndependentConcurrently(t *testing.T) {
|
|||||||
for err := range errors {
|
for err := range errors {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var collected []*Result[layeredConfig]
|
var collected []*testResult[layeredConfig]
|
||||||
for result := range results {
|
for result := range results {
|
||||||
collected = append(collected, result)
|
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
|
// WriteEffective writes deterministic redacted YAML from the frozen resolved
|
||||||
// snapshot. The output is intended for inspection and is not promised reloadable.
|
// snapshot. The output is intended for inspection and is not promised reloadable.
|
||||||
// Writer errors, including short writes, are returned and may leave partial output.
|
// Writer errors, including short writes, are returned and may leave partial output.
|
||||||
func (r *Result[T]) WriteEffective(output io.Writer) error {
|
func (r *Inspection) WriteEffective(output io.Writer) error {
|
||||||
if r == nil || r.final == nil || output == nil {
|
if r == nil || r.final == nil || output == nil {
|
||||||
return errEffectiveUnavailable
|
return errEffectiveUnavailable
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ sequence: [second, first]
|
|||||||
if err := result.WriteEffective(shortWriter{}); !errors.Is(err, io.ErrShortWrite) {
|
if err := result.WriteEffective(shortWriter{}); !errors.Is(err, io.ErrShortWrite) {
|
||||||
t.Fatalf("short writer error = %v", err)
|
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) {
|
if err := nilResult.WriteEffective(io.Discard); !errors.Is(err, errEffectiveUnavailable) {
|
||||||
t.Fatalf("nil result error = %v", err)
|
t.Fatalf("nil result error = %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -440,7 +440,7 @@ func TestValidationContextPanicsAfterVersionCallback(t *testing.T) {
|
|||||||
retained.Warning(Keys("version"), "too late")
|
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()
|
t.Helper()
|
||||||
if result != nil || err == nil {
|
if result != nil || err == nil {
|
||||||
t.Fatalf("result, err = %#v, %v", result, err)
|
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) {
|
func testLoad[T any](source Source, supplied ...Options[T]) (*testResult[T], error) {
|
||||||
return Load(source, testSecretBinding(), supplied...)
|
loaded, err := Load(source, testSecretBinding(), supplied...)
|
||||||
|
return testLoaded(loaded), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLoadWithSecretLookup[T any](source Source, lookup secretLookup, supplied ...Options[T]) (*Result[T], error) {
|
func testLoadWithSecretLookup[T any](source Source, lookup secretLookup, supplied ...Options[T]) (*testResult[T], error) {
|
||||||
return loadWithSecretLookup(source, testSecretBinding(), lookup, supplied...)
|
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) {
|
func testBuildSchema(root reflect.Type) (*schemaDescriptor, *schemaIssue) {
|
||||||
|
|||||||
Reference in New Issue
Block a user