Files
ConfigKit/internal/engine/explain.go

166 lines
5.7 KiB
Go

package engine
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
// ValueSnapshot is a detached, deterministic YAML representation of one value.
// Present distinguishes absence or deletion from an explicit null value.
type ValueSnapshot struct {
Present bool
Redacted bool
YAML string
}
// ValueChange describes one explicit definition or audited domain operation.
type ValueChange struct {
Before ValueSnapshot
After ValueSnapshot
Origin Origin
Reasons []string
Dependencies []Path
}
// SecretReferenceInfo describes a successfully resolved effective reference.
// Name is the environment-variable name, never its value.
type SecretReferenceInfo struct {
Name string
}
// Explanation is a detached safe account of one effective or historical path.
type Explanation struct {
Path Path
Effective ValueSnapshot
Origin Origin
History []ValueChange
SecretReference *SecretReferenceInfo
}
// Explain returns safe effective and chronological values for a known final or
// historical path. Unknown and malformed paths return false.
func (r *Inspection) Explain(path Path) (Explanation, bool) {
if r == nil || r.final == nil || r.baseline == nil || !r.metadata.hasHistoricalPath(path) {
return Explanation{}, false
}
explanation := Explanation{Path: path.clone(), Effective: valueSnapshotWithLookup(r.final, path, r.secrets, r.immutableLookup)}
if info, ok := r.metadata.Lookup(path); ok {
explanation.Origin = cloneOrigin(info.Origin)
}
working := document.Clone(r.baseline)
if _, present := documentAtPath(working, path); present {
explanation.History = append(explanation.History, ValueChange{
After: valueSnapshot(working, path, r.secrets), Origin: Origin{Layer: LayerDefault},
})
}
replayIndex := new(mappingIndex)
for _, application := range r.applications {
operationPath, relevant := applicationOperationPath(application, path, r.immutableLookup)
var before ValueSnapshot
if relevant {
before = valueSnapshot(working, path, r.secrets)
}
applyHistoryApplication(&working, application, replayIndex)
if !relevant {
continue
}
after := valueSnapshot(working, path, r.secrets)
if !before.Present && !after.Present {
continue
}
explanation.History = append(explanation.History, ValueChange{
Before: before,
After: after,
Origin: historyApplicationOrigin(application, operationPath, r.immutableLookup),
})
}
appendInheritanceExplanation(&explanation, &working, path, r.inheritance, r.secrets)
// Intermediate input replacements do not permanently suppress defaults:
// decoding applies them to the final input tree. Show that reconciliation
// explicitly, including canonical scalar conversions, before domain changes.
decodedInput := r.input
decodedOrigins := r.inputOrigins
if r.preCLI != nil {
decodedInput = r.preCLI
decodedOrigins = r.preCLIOrigins
}
replayed, _ := documentAtPath(working, path)
decoded, _ := documentAtPathWithLookup(decodedInput, path, r.immutableLookup)
if !configurationValuesEqual(replayed, decoded) {
origin := cloneOrigin(decodedOrigins[path.String()])
origin.Operation = ChangeDecoding
explanation.History = append(explanation.History, ValueChange{
Before: valueSnapshot(working, path, r.secrets),
After: valueSnapshotWithLookup(decodedInput, path, r.secrets, r.immutableLookup),
Origin: origin,
Reasons: []string{"applied retained defaults and canonical typed decoding"},
})
}
working = document.Clone(decodedInput)
if r.preCLI != nil {
for _, application := range r.postDecodeApps {
operationPath, relevant := applicationOperationPath(application, path, r.immutableLookup)
var before ValueSnapshot
if relevant {
before = valueSnapshot(working, path, r.secrets)
}
applyHistoryApplication(&working, application, replayIndex)
if !relevant {
continue
}
after := valueSnapshot(working, path, r.secrets)
if !before.Present && !after.Present {
continue
}
explanation.History = append(explanation.History, ValueChange{
Before: before,
After: after,
Origin: historyApplicationOrigin(application, operationPath, r.immutableLookup),
})
}
replayed, _ = documentAtPath(working, path)
effectiveInput, _ := documentAtPathWithLookup(r.input, path, r.immutableLookup)
if !configurationValuesEqual(replayed, effectiveInput) {
origin := cloneOrigin(r.inputOrigins[path.String()])
origin.Operation = ChangeDecoding
explanation.History = append(explanation.History, ValueChange{
Before: valueSnapshot(working, path, r.secrets),
After: valueSnapshotWithLookup(r.input, path, r.secrets, r.immutableLookup),
Origin: origin,
Reasons: []string{"applied the typed CLI assignment"},
})
}
working = document.Clone(r.input)
}
for _, snapshot := range r.resolutionReplay {
relevant := pathsIntersect(snapshot.change.Path, path)
var before ValueSnapshot
if relevant {
before = valueSnapshot(working, path, r.secrets)
}
replaceHistoryPath(&working, snapshot.change.Path, snapshot.after)
if !relevant {
continue
}
after := valueSnapshot(working, path, r.secrets)
if !before.Present && !after.Present {
continue
}
explanation.History = append(explanation.History, ValueChange{
Before: before,
After: after,
Origin: cloneOrigin(snapshot.change.Origin),
Reasons: append([]string(nil), snapshot.change.Reasons...),
Dependencies: clonePaths(snapshot.change.Dependencies),
})
}
if !explanation.Effective.Present && len(explanation.History) == 0 {
return Explanation{}, false
}
if name, ok := r.secretReferences[path.String()]; ok && !pathIsSecret(r.secrets, path) {
explanation.SecretReference = &SecretReferenceInfo{Name: name}
}
return explanation, true
}