Files
configkit/internal/engine/history_resolution.go

124 lines
4.3 KiB
Go

package engine
import "go.maximumdirect.net/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))
lookup := buildDocumentLookup(builder.scratch)
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, lookup)
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)
}
lookup = buildDocumentLookup(builder.scratch)
for recordID, snapshot := range payload.snapshots {
ref := historyPayloadRef{phase: historyPhaseResolution, id: recordID}
afterPath, afterFound := historyConcretePathAt(builder.scratch, snapshot.change.Path, lookup)
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, lookup *documentLookup) (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
}
position := lookup.find(current, segment.key)
if position < 0 {
return Path{}, false
}
current = current.Mapping[position].Value
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
}