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 }