Files
configkit/internal/engine/explain.go

246 lines
8.7 KiB
Go

package engine
import "go.maximumdirect.net/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 || !path.valid() {
return Explanation{}, false
}
model := r.history
if model == nil {
return Explanation{}, false
}
selected := model.selectHistory(path)
if len(selected) == 0 && !model.hasObservedShape(path) {
return Explanation{}, false
}
selections := model.indexReplaySelections(selected)
explanation := Explanation{Path: path.clone(), Effective: valueSnapshotWithLookup(r.final, path, r.secrets, model.documentLookup)}
if info, ok := r.metadata.Lookup(path); ok {
explanation.Origin = cloneOrigin(info.Origin)
}
working := document.Clone(model.baseline)
if baseline, found := selections.byPayload[historyPayloadRef{phase: historyPhaseBaseline, id: 0}]; found && baseline.after {
explanation.History = append(explanation.History, ValueChange{
After: valueSnapshot(working, path, r.secrets),
Origin: cloneOrigin(baseline.attribution.origin),
})
}
replayIndex := new(mappingIndex)
for payloadID, payload := range model.sourcePayloads {
if payload.kind != historySourceApplicationPayload {
continue
}
application := payload.application
before := valueSnapshot(working, path, r.secrets)
applyHistoryApplication(&working, application, replayIndex)
selected, relevant := selections.application(historyPayloadRef{phase: historyPhaseSources, id: payloadID})
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(selected.attribution.origin),
})
}
appendInheritanceExplanation(&explanation, &working, path, model, selections, r.secrets)
// Intermediate input replacements do not permanently suppress defaults:
// decoding applies them to the final input tree. Show that reconciliation
// explicitly before typed CLI contributions and domain resolution.
if checkpoint := model.preCLICheckpoint; checkpoint != nil {
selection, selected := selections.byPayload[historyPayloadRef{phase: historyPhasePreCLI, id: 0}]
replayed, _ := documentAtPath(working, path)
decoded, _ := documentAtPathWithLookup(checkpoint.root, path, model.documentLookup)
if selected && !configurationValuesEqual(replayed, decoded) {
origin, reasons := checkpointExplanationAttribution(selection)
explanation.History = append(explanation.History, ValueChange{
Before: valueSnapshot(working, path, r.secrets),
After: valueSnapshotWithLookup(checkpoint.root, path, r.secrets, model.documentLookup),
Origin: origin,
Reasons: reasons,
})
}
working = document.Clone(checkpoint.root)
}
for payloadID, payload := range model.cliPayloads {
if payload.kind != historySourceApplicationPayload {
continue
}
applicationRef := historyPayloadRef{phase: historyPhaseCLI, id: payloadID}
before := valueSnapshot(working, path, r.secrets)
applyHistoryApplication(&working, payload.application, replayIndex)
selected, relevant := selections.application(applicationRef)
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(selected.attribution.origin),
})
}
if checkpoint := model.postCLICheckpoint; checkpoint != nil {
selection, selected := selections.byPayload[historyPayloadRef{phase: historyPhasePostCLI, id: 0}]
replayed, _ := documentAtPath(working, path)
effectiveInput, _ := documentAtPathWithLookup(checkpoint.root, path, model.documentLookup)
if selected && !configurationValuesEqual(replayed, effectiveInput) {
origin, reasons := checkpointExplanationAttribution(selection)
explanation.History = append(explanation.History, ValueChange{
Before: valueSnapshot(working, path, r.secrets),
After: valueSnapshotWithLookup(checkpoint.root, path, r.secrets, model.documentLookup),
Origin: origin,
Reasons: reasons,
})
}
working = document.Clone(checkpoint.root)
}
if model.resolutionPayload != nil {
for _, group := range model.resolutionPayload.replayGroups {
selection, selected := selections.resolutionGroup(group)
if !selected || group.coveringRecordID < 0 || group.coveringRecordID >= len(model.resolutionPayload.snapshots) {
continue
}
covering := model.resolutionPayload.snapshots[group.coveringRecordID]
before := valueSnapshot(working, path, r.secrets)
replaceHistoryPath(&working, group.change.Path, covering.after)
after := valueSnapshot(working, path, r.secrets)
if !before.Present && !after.Present {
continue
}
explanation.History = append(explanation.History, ValueChange{
Before: before,
After: after,
Origin: cloneOrigin(selection.attribution.origin),
Reasons: append([]string(nil), group.change.Reasons...),
Dependencies: clonePaths(group.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
}
// historyReplaySelections is query-local dispatch state, not another evidence
// or attribution owner. Replay still follows the model's ordered payloads.
type historyReplaySelections struct {
byPayload map[historyPayloadRef]historySelection
applicationFallbacks map[historyPayloadRef]historySelection
}
func (model *historyModel) indexReplaySelections(ordered []historySelection) historyReplaySelections {
indexed := historyReplaySelections{
byPayload: make(map[historyPayloadRef]historySelection, len(ordered)),
applicationFallbacks: make(map[historyPayloadRef]historySelection),
}
for _, selection := range ordered {
ref := selection.payload
indexed.byPayload[ref] = selection
if selection.role != historyRoleOperation || ref.substep != 0 {
continue
}
var payloads []historySourcePayload
switch ref.phase {
case historyPhaseSources:
payloads = model.sourcePayloads
case historyPhaseCLI:
payloads = model.cliPayloads
default:
continue
}
if ref.id < 0 || ref.id >= len(payloads) {
continue
}
payload := payloads[ref.id]
if payload.kind == historySourceOverlayPayload && payload.hasApplicationRef {
indexed.applicationFallbacks[payload.applicationRef] = selection
}
}
return indexed
}
func (selections historyReplaySelections) application(ref historyPayloadRef) (historySelection, bool) {
if direct, found := selections.byPayload[ref]; found {
return direct, true
}
// Only removed/replaced paths lacking a direct contribution need fallback.
selection, found := selections.applicationFallbacks[ref]
return selection, found
}
func checkpointExplanationAttribution(selection historySelection) (Origin, []string) {
if selection.hasAttribution {
return cloneOrigin(selection.attribution.origin), append([]string(nil), selection.attribution.reasons...)
}
return Origin{Operation: ChangeDecoding}, nil
}
func (selections historyReplaySelections) resolutionGroup(group resolutionReplayGroup) (historySelection, bool) {
var fallback historySelection
found := false
for _, recordID := range group.originalRecordIDs {
selection, ok := selections.byPayload[historyPayloadRef{phase: historyPhaseResolution, id: recordID}]
if !ok {
continue
}
if recordID == group.coveringRecordID {
return selection, true
}
if !found {
fallback, found = selection, true
}
}
return fallback, found
}