Files
configkit/internal/engine/history_model.go

295 lines
8.5 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
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
prefixReady bool
}
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
}
payload := builder.nextPayload(phase)
walkDocument(root, prefix, func(path Path, node *document.Node) {
builder.addInventoryObservation(path, path, payload, phase, node.Sensitive)
})
}
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
}
var roots []*document.Node
appendRoot := func(root *document.Node) {
if root != nil {
roots = append(roots, root)
}
}
appendRoot(model.baseline)
appendRoot(model.final)
for _, payload := range model.sourcePayloads {
if payload.kind == historySourceApplicationPayload {
appendRoot(payload.application.value)
}
}
for _, payload := range model.inheritancePayloads {
for _, operation := range payload.operations {
appendRoot(operation.parent)
appendRoot(operation.effective)
}
}
for _, payload := range model.cliPayloads {
if payload.kind == historySourceApplicationPayload {
appendRoot(payload.application.value)
}
}
if model.preCLICheckpoint != nil {
appendRoot(model.preCLICheckpoint.root)
}
if model.postCLICheckpoint != nil {
appendRoot(model.postCLICheckpoint.root)
}
if model.resolutionPayload != nil {
for _, snapshot := range model.resolutionPayload.snapshots {
appendRoot(snapshot.before)
appendRoot(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
}