Normalize original resolution history evidence
This commit is contained in:
127
internal/engine/history_resolution.go
Normal file
127
internal/engine/history_resolution.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package engine
|
||||
|
||||
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
|
||||
// resolutionReplayGroup is the explanation projection of one or more ordered
|
||||
// resolution records. The covering record identifies the before/after roots;
|
||||
// originalRecordIDs keep every coalesced record available to other projections.
|
||||
type resolutionReplayGroup struct {
|
||||
originalRecordIDs []int
|
||||
coveringRecordID int
|
||||
change Change
|
||||
}
|
||||
|
||||
type historyResolutionPayload struct {
|
||||
snapshots []resolutionSnapshot
|
||||
replayGroups []resolutionReplayGroup
|
||||
}
|
||||
|
||||
// observeResolutionEvidence retains audited records in producer order and
|
||||
// indexes the concrete shapes in every original before/after subtree. Snapshot
|
||||
// roots are already detached by runResolution and are shared with the replay
|
||||
// group that refers to their covering record.
|
||||
func (builder *historyBuilder) observeResolutionEvidence(snapshots []resolutionSnapshot) {
|
||||
builder.requireOpen()
|
||||
if len(snapshots) == 0 {
|
||||
return
|
||||
}
|
||||
if builder.model.resolutionPayload != nil {
|
||||
panic("configkit: resolution history already registered")
|
||||
}
|
||||
if builder.scratch == nil {
|
||||
panic("configkit: resolution history requires the pre-resolution document")
|
||||
}
|
||||
|
||||
payload := &historyResolutionPayload{
|
||||
snapshots: make([]resolutionSnapshot, len(snapshots)),
|
||||
replayGroups: coalescedResolutionSnapshots(snapshots),
|
||||
}
|
||||
attributions := make([]historyAttributionRef, len(snapshots))
|
||||
for recordID, snapshot := range snapshots {
|
||||
snapshot.change = cloneChange(snapshot.change)
|
||||
payload.snapshots[recordID] = snapshot
|
||||
|
||||
ref := builder.nextPayload(historyPhaseResolution)
|
||||
if ref.id != recordID {
|
||||
panic("configkit: resolution payload identifiers are out of order")
|
||||
}
|
||||
attribution := builder.addAttribution(snapshot.change.Origin, snapshot.change.Reasons, snapshot.change.Dependencies)
|
||||
attributions[recordID] = attribution
|
||||
beforePath, beforeFound := historyConcretePathAt(builder.scratch, snapshot.change.Path)
|
||||
if beforeFound {
|
||||
observeResolutionSubtree(builder, snapshot.before, beforePath, snapshot.change.Path, ref, attribution, true, false)
|
||||
}
|
||||
}
|
||||
for _, group := range payload.replayGroups {
|
||||
if group.coveringRecordID < 0 || group.coveringRecordID >= len(payload.snapshots) {
|
||||
continue
|
||||
}
|
||||
covering := payload.snapshots[group.coveringRecordID]
|
||||
replaceHistoryPath(&builder.scratch, group.change.Path, covering.after)
|
||||
}
|
||||
for recordID, snapshot := range payload.snapshots {
|
||||
ref := historyPayloadRef{phase: historyPhaseResolution, id: recordID}
|
||||
afterPath, afterFound := historyConcretePathAt(builder.scratch, snapshot.change.Path)
|
||||
if afterFound {
|
||||
observeResolutionSubtree(builder, snapshot.after, afterPath, snapshot.change.Path, ref, attributions[recordID], false, true)
|
||||
}
|
||||
}
|
||||
builder.model.resolutionPayload = payload
|
||||
}
|
||||
|
||||
func observeResolutionSubtree(builder *historyBuilder, root *document.Node, path, reportedPath Path, ref historyPayloadRef, attribution historyAttributionRef, before, after bool) {
|
||||
walkDocument(root, path, func(path Path, node *document.Node) {
|
||||
builder.addObservation(historyObservation{
|
||||
path: path,
|
||||
phase: historyPhaseResolution,
|
||||
payload: ref,
|
||||
reportedPath: reportedPath,
|
||||
role: historyRoleOperation,
|
||||
beforePresent: before,
|
||||
afterPresent: after,
|
||||
sensitive: node.Sensitive,
|
||||
attributionRef: attribution,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func historyConcretePathAt(root *document.Node, path Path) (Path, bool) {
|
||||
if root == nil || !path.valid() {
|
||||
return Path{}, false
|
||||
}
|
||||
current := root
|
||||
var concrete Path
|
||||
for _, segment := range path.segments {
|
||||
switch current.Kind {
|
||||
case document.Mapping:
|
||||
if segment.kind == pathSegmentIndex {
|
||||
return Path{}, false
|
||||
}
|
||||
found := false
|
||||
for _, entry := range current.Mapping {
|
||||
if entry.Key == segment.key {
|
||||
current = entry.Value
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return Path{}, false
|
||||
}
|
||||
concrete = concrete.Key(segment.key)
|
||||
case document.Sequence:
|
||||
index, ok := schemaIndex(segment)
|
||||
if !ok || index >= len(current.Sequence) {
|
||||
return Path{}, false
|
||||
}
|
||||
current = current.Sequence[index]
|
||||
concrete = concrete.Index(index)
|
||||
default:
|
||||
return Path{}, false
|
||||
}
|
||||
if current == nil {
|
||||
return Path{}, false
|
||||
}
|
||||
}
|
||||
return concrete, true
|
||||
}
|
||||
Reference in New Issue
Block a user