package engine import "go.maximumdirect.net/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 affectedPathIndex 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, historyRoleOperation, historyRoleControl: pointer := observation.path.String() builder.model.observationsByPointer[pointer] = append(builder.model.observationsByPointer[pointer], observationID) if observation.role == historyRoleContribution || observation.role == historyRoleOperation || observation.role == historyRoleControl { // Index the concrete affected path, not the common reported ancestor. // A leaf query must not enumerate every sibling fact of one operation. builder.model.affectedPathIndex.insert(pointer, 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 }