295 lines
8.7 KiB
Go
295 lines
8.7 KiB
Go
package engine
|
|
|
|
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
|
|
type historyPhase uint8
|
|
|
|
const (
|
|
historyPhaseBaseline historyPhase = iota
|
|
historyPhaseSources
|
|
historyPhaseInheritance
|
|
historyPhasePreCLI
|
|
historyPhaseCLI
|
|
historyPhasePostCLI
|
|
historyPhaseResolution
|
|
)
|
|
|
|
type historyRole uint8
|
|
|
|
const (
|
|
historyRoleDefinition historyRole = iota
|
|
historyRoleContribution
|
|
historyRoleCompositeDefinition
|
|
historyRoleOperation
|
|
historyRoleControl
|
|
historyRoleCheckpoint
|
|
historyRoleInventory
|
|
)
|
|
|
|
type historyPayloadRef struct {
|
|
phase historyPhase
|
|
id int
|
|
substep int
|
|
}
|
|
|
|
type historyAttributionRef int
|
|
|
|
const noHistoryAttribution historyAttributionRef = -1
|
|
|
|
type historyAttribution struct {
|
|
origin Origin
|
|
reasons []string
|
|
dependencies []Path
|
|
}
|
|
|
|
type historyObservation struct {
|
|
path Path
|
|
phase historyPhase
|
|
payload historyPayloadRef
|
|
reportedPath Path
|
|
role historyRole
|
|
beforePresent bool
|
|
afterPresent bool
|
|
sensitive bool
|
|
attributionRef historyAttributionRef
|
|
}
|
|
|
|
type historyModel struct {
|
|
observations []historyObservation
|
|
attributions []historyAttribution
|
|
baseline *document.Node
|
|
final *document.Node
|
|
inventoryRoots []*document.Node
|
|
sourcePayloads []historySourcePayload
|
|
inheritancePayloads []historyInheritancePayload
|
|
cliPayloads []historySourcePayload
|
|
resolutionPayload *historyResolutionPayload
|
|
preCLICheckpoint *historyCheckpointPayload
|
|
postCLICheckpoint *historyCheckpointPayload
|
|
|
|
shapesByPointer map[string][]Path
|
|
shapeOrder []Path
|
|
observationsByPointer map[string][]int
|
|
reportedPathIndex pathPrefixIndex
|
|
documentLookup *documentLookup
|
|
|
|
frozen bool
|
|
}
|
|
|
|
// historyBuilder owns the model while loading. Its scratch document is one
|
|
// mutable working projection used to resolve concrete sides of relative
|
|
// producer effects; it is not retained by the published model.
|
|
type historyBuilder struct {
|
|
model *historyModel
|
|
nextPayloadIDs map[historyPhase]int
|
|
scratch *document.Node
|
|
}
|
|
|
|
func newHistoryBuilder() *historyBuilder {
|
|
return &historyBuilder{
|
|
model: &historyModel{
|
|
shapesByPointer: make(map[string][]Path),
|
|
observationsByPointer: make(map[string][]int),
|
|
},
|
|
nextPayloadIDs: make(map[historyPhase]int),
|
|
}
|
|
}
|
|
|
|
func (builder *historyBuilder) nextPayload(phase historyPhase) historyPayloadRef {
|
|
builder.requireOpen()
|
|
id := builder.nextPayloadIDs[phase]
|
|
builder.nextPayloadIDs[phase] = id + 1
|
|
return historyPayloadRef{phase: phase, id: id}
|
|
}
|
|
|
|
func (builder *historyBuilder) addAttribution(origin Origin, reasons []string, dependencies []Path) historyAttributionRef {
|
|
builder.requireOpen()
|
|
ref := historyAttributionRef(len(builder.model.attributions))
|
|
builder.model.attributions = append(builder.model.attributions, historyAttribution{
|
|
origin: cloneOrigin(origin),
|
|
reasons: append([]string(nil), reasons...),
|
|
dependencies: clonePaths(dependencies),
|
|
})
|
|
return ref
|
|
}
|
|
|
|
func (builder *historyBuilder) addObservation(observation historyObservation) int {
|
|
builder.requireOpen()
|
|
if !observation.path.valid() || !concreteHistoryPath(observation.path) || !observation.reportedPath.valid() {
|
|
return -1
|
|
}
|
|
if observation.attributionRef < noHistoryAttribution || int(observation.attributionRef) >= len(builder.model.attributions) {
|
|
return -1
|
|
}
|
|
observation.path = observation.path.clone()
|
|
observation.reportedPath = observation.reportedPath.clone()
|
|
observationID := len(builder.model.observations)
|
|
builder.model.observations = append(builder.model.observations, observation)
|
|
builder.addShape(observation.path)
|
|
switch observation.role {
|
|
case historyRoleDefinition, historyRoleContribution, historyRoleCompositeDefinition, historyRoleCheckpoint:
|
|
pointer := observation.path.String()
|
|
builder.model.observationsByPointer[pointer] = append(builder.model.observationsByPointer[pointer], observationID)
|
|
if observation.role == historyRoleContribution {
|
|
builder.model.reportedPathIndex.insert(pointer, observationID)
|
|
}
|
|
case historyRoleOperation, historyRoleControl:
|
|
builder.model.reportedPathIndex.insert(observation.reportedPath.String(), observationID)
|
|
}
|
|
return observationID
|
|
}
|
|
|
|
func (builder *historyBuilder) observeDocument(root *document.Node, phase historyPhase) {
|
|
builder.observeDocumentAt(root, Path{}, phase)
|
|
}
|
|
|
|
func (builder *historyBuilder) observeDocumentAt(root *document.Node, prefix Path, phase historyPhase) {
|
|
builder.requireOpen()
|
|
if root == nil {
|
|
return
|
|
}
|
|
builder.registerDocumentRoot(root)
|
|
payload := builder.nextPayload(phase)
|
|
walkDocument(root, prefix, func(path Path, node *document.Node) {
|
|
builder.addInventoryObservation(path, path, payload, phase, node.Sensitive)
|
|
})
|
|
}
|
|
|
|
func (builder *historyBuilder) registerDocumentRoot(root *document.Node) {
|
|
builder.requireOpen()
|
|
if root != nil {
|
|
builder.model.inventoryRoots = append(builder.model.inventoryRoots, root)
|
|
}
|
|
}
|
|
|
|
func (builder *historyBuilder) addInventoryObservation(path, reportedPath Path, payload historyPayloadRef, phase historyPhase, sensitive bool) int {
|
|
return builder.addObservation(historyObservation{
|
|
path: path, phase: phase, payload: payload,
|
|
reportedPath: reportedPath, role: historyRoleInventory,
|
|
sensitive: sensitive, attributionRef: noHistoryAttribution,
|
|
})
|
|
}
|
|
|
|
func (builder *historyBuilder) setScratch(root *document.Node) {
|
|
builder.requireOpen()
|
|
builder.scratch = document.Clone(root)
|
|
}
|
|
|
|
func (builder *historyBuilder) scratchAt(path Path) (*document.Node, bool) {
|
|
builder.requireOpen()
|
|
return documentAtPath(builder.scratch, path)
|
|
}
|
|
|
|
// advanceScratch transfers the next working projection to the builder. Callers
|
|
// use it after applying existing value-replay helpers to the current scratch
|
|
// document, avoiding a retained tree copy for each operation.
|
|
func (builder *historyBuilder) advanceScratch(root *document.Node) {
|
|
builder.requireOpen()
|
|
builder.scratch = root
|
|
}
|
|
|
|
func (builder *historyBuilder) freeze() *historyModel {
|
|
builder.requireOpen()
|
|
builder.scratch = nil
|
|
builder.model.documentLookup = buildDocumentLookup(builder.model.documentRoots()...)
|
|
builder.model.frozen = true
|
|
model := builder.model
|
|
builder.model = nil
|
|
builder.nextPayloadIDs = nil
|
|
return model
|
|
}
|
|
|
|
func (model *historyModel) documentRoots() []*document.Node {
|
|
if model == nil {
|
|
return nil
|
|
}
|
|
roots := append([]*document.Node(nil), model.inventoryRoots...)
|
|
roots = append(roots, model.baseline, model.final)
|
|
for _, payload := range model.sourcePayloads {
|
|
if payload.kind == historySourceApplicationPayload {
|
|
roots = append(roots, payload.application.value)
|
|
}
|
|
}
|
|
for _, payload := range model.inheritancePayloads {
|
|
for _, operation := range payload.operations {
|
|
roots = append(roots, operation.parent, operation.effective)
|
|
}
|
|
}
|
|
for _, payload := range model.cliPayloads {
|
|
if payload.kind == historySourceApplicationPayload {
|
|
roots = append(roots, payload.application.value)
|
|
}
|
|
}
|
|
if model.preCLICheckpoint != nil {
|
|
roots = append(roots, model.preCLICheckpoint.root)
|
|
}
|
|
if model.postCLICheckpoint != nil {
|
|
roots = append(roots, model.postCLICheckpoint.root)
|
|
}
|
|
if model.resolutionPayload != nil {
|
|
for _, snapshot := range model.resolutionPayload.snapshots {
|
|
roots = append(roots, snapshot.before, snapshot.after)
|
|
}
|
|
}
|
|
return roots
|
|
}
|
|
|
|
func (model *historyModel) documentAt(root *document.Node, path Path) (*document.Node, bool) {
|
|
if model == nil || model.documentLookup == nil {
|
|
return nil, false
|
|
}
|
|
return documentAtPathWithLookup(root, path, model.documentLookup)
|
|
}
|
|
|
|
func (builder *historyBuilder) addShape(path Path) {
|
|
pointer := path.String()
|
|
for _, existing := range builder.model.shapesByPointer[pointer] {
|
|
if sameConcreteHistoryPath(existing, path) {
|
|
return
|
|
}
|
|
}
|
|
owned := path.clone()
|
|
builder.model.shapesByPointer[pointer] = append(builder.model.shapesByPointer[pointer], owned)
|
|
builder.model.shapeOrder = append(builder.model.shapeOrder, owned.clone())
|
|
}
|
|
|
|
func (builder *historyBuilder) requireOpen() {
|
|
if builder == nil || builder.model == nil || builder.model.frozen {
|
|
panic("configkit: history builder is no longer active")
|
|
}
|
|
}
|
|
|
|
func concreteHistoryPath(path Path) bool {
|
|
for _, segment := range path.segments {
|
|
if segment.kind == pathSegmentParsed {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func sameConcreteHistoryPath(left, right Path) bool {
|
|
if len(left.segments) != len(right.segments) {
|
|
return false
|
|
}
|
|
for index, leftSegment := range left.segments {
|
|
rightSegment := right.segments[index]
|
|
if leftSegment.kind != rightSegment.kind {
|
|
return false
|
|
}
|
|
switch leftSegment.kind {
|
|
case pathSegmentKey:
|
|
if leftSegment.key != rightSegment.key {
|
|
return false
|
|
}
|
|
case pathSegmentIndex:
|
|
if leftSegment.index != rightSegment.index {
|
|
return false
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|