Files
configkit/internal/engine/history_source.go

285 lines
10 KiB
Go

package engine
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
type historyPathOrigin struct {
path Path
origin Origin
sensitive bool
}
type historySourcePayloadKind uint8
const (
historySourceApplicationPayload historySourcePayloadKind = iota
historySourceOverlayPayload
historySourceSummaryPayload
)
// historySourcePayload keeps source values and replacement facts under the
// shared model's ownership. Application documents remain shared when immutable.
type historySourcePayload struct {
kind historySourcePayloadKind
application historyApplication
overlay overlayRecord
applicationRef historyPayloadRef
hasApplicationRef bool
path Path
origin Origin
contributions []historyPayloadRef
}
func (builder *historyBuilder) observeBaseline(root *document.Node) {
builder.requireOpen()
if root == nil {
return
}
builder.model.baseline = root
payload := builder.nextPayload(historyPhaseBaseline)
defaultAttribution := builder.addAttribution(Origin{Layer: LayerDefault}, nil, nil)
walkDocument(root, Path{}, func(path Path, node *document.Node) {
builder.addObservation(historyObservation{
path: path, phase: historyPhaseBaseline, payload: payload,
reportedPath: path, role: historyRoleDefinition,
afterPresent: true, sensitive: node.Sensitive,
attributionRef: defaultAttribution,
})
})
}
// observeSourceEvidence registers raw source contributions, replacement sides,
// and the surviving structural definition summaries in producer order.
func (builder *historyBuilder) observeSourceEvidence(root *document.Node, origins map[string]Origin, applications []historyApplication, records []overlayRecord) {
builder.requireOpen()
if len(applications) == 0 && len(records) == 0 {
return
}
applicationsBySource := make(map[string]historyPayloadRef)
applicationsByLayer := make(map[Layer][]historyPayloadRef)
for index := range applications {
application := applications[index]
payload := builder.addSourcePayload(&application, nil)
applicationsByLayer[application.layer] = append(applicationsByLayer[application.layer], payload)
walkHistoryApplication(application, func(_, effective Path, node *document.Node) {
if node.Location.SourceID != "" {
applicationsBySource[node.Location.SourceID] = payload
}
origin := sourceApplicationOrigin(application, node)
for _, location := range origin.Locations {
if location.SourceID != "" {
applicationsBySource[location.SourceID] = payload
}
}
attribution := builder.addAttribution(origin, nil, nil)
builder.addObservation(historyObservation{
path: effective, phase: historyPhaseSources, payload: payload,
reportedPath: effective, role: historyRoleContribution,
afterPresent: true, sensitive: node.Sensitive,
attributionRef: attribution,
})
})
}
for index := range records {
record := records[index]
payload := builder.addSourcePayload(nil, &record)
applicationRef, hasApplication := sourceApplicationForRecord(record, applicationsBySource, applicationsByLayer)
if hasApplication {
owned := &builder.model.sourcePayloads[payload.id]
owned.applicationRef = applicationRef
owned.hasApplicationRef = true
}
for displacedIndex, displaced := range record.displaced {
definitionPayload := payload
definitionPayload.substep = displacedIndex + 1
definition := builder.addAttribution(displaced.origin, nil, nil)
builder.addObservation(historyObservation{
path: displaced.path, phase: historyPhaseSources, payload: definitionPayload,
reportedPath: displaced.path, role: historyRoleDefinition,
beforePresent: true,
attributionRef: definition,
})
}
attribution := builder.addAttribution(record.change.Origin, nil, nil)
for _, displaced := range record.displaced {
builder.addObservation(historyObservation{
path: displaced.path, phase: historyPhaseSources, payload: payload,
reportedPath: record.change.Path, role: historyRoleOperation,
beforePresent: true, attributionRef: attribution,
})
}
for _, introduced := range record.introduced {
builder.addObservation(historyObservation{
path: introduced.path, phase: historyPhaseSources, payload: payload,
reportedPath: record.change.Path, role: historyRoleOperation,
afterPresent: true, sensitive: introduced.sensitive,
attributionRef: attribution,
})
}
}
if root == nil {
return
}
walkDocument(root, Path{}, func(path Path, node *document.Node) {
origin, found := origins[path.String()]
if !found || origin.Layer == "" || sourceOriginRepresentedByOperation(builder.model, path, origin) {
return
}
contributions := builder.sourceContributions(path, origin)
payload := builder.addSourceSummary(path, origin, contributions)
attribution := builder.addAttribution(origin, nil, nil)
builder.addObservation(historyObservation{
path: path, phase: historyPhaseSources, payload: payload,
reportedPath: path, role: historyRoleCompositeDefinition,
afterPresent: true, sensitive: node.Sensitive,
attributionRef: attribution,
})
})
}
func sourceApplicationForRecord(record overlayRecord, bySource map[string]historyPayloadRef, byLayer map[Layer][]historyPayloadRef) (historyPayloadRef, bool) {
for _, location := range record.change.Origin.Locations {
if reference, found := bySource[location.SourceID]; found && location.SourceID != "" {
return reference, true
}
}
applications := byLayer[record.change.Origin.Layer]
if len(applications) == 1 {
return applications[0], true
}
return historyPayloadRef{}, false
}
func (builder *historyBuilder) addSourcePayload(application *historyApplication, overlay *overlayRecord) historyPayloadRef {
payload := builder.nextPayload(historyPhaseSources)
if payload.id != len(builder.model.sourcePayloads) {
panic("configkit: source payload identifiers are out of order")
}
owned := historySourcePayload{}
switch {
case application != nil:
owned.kind = historySourceApplicationPayload
owned.application = cloneHistoryApplications([]historyApplication{*application})[0]
case overlay != nil:
owned.kind = historySourceOverlayPayload
owned.overlay = cloneOverlayRecords([]overlayRecord{*overlay})[0]
default:
panic("configkit: source payload requires a producer")
}
builder.model.sourcePayloads = append(builder.model.sourcePayloads, owned)
return payload
}
func (builder *historyBuilder) addSourceSummary(path Path, origin Origin, contributions []historyPayloadRef) historyPayloadRef {
payload := builder.nextPayload(historyPhaseSources)
if payload.id != len(builder.model.sourcePayloads) {
panic("configkit: source payload identifiers are out of order")
}
builder.model.sourcePayloads = append(builder.model.sourcePayloads, historySourcePayload{
kind: historySourceSummaryPayload, path: path.clone(), origin: cloneOrigin(origin),
contributions: append([]historyPayloadRef(nil), contributions...),
})
return payload
}
func (builder *historyBuilder) sourceContributions(path Path, summary Origin) []historyPayloadRef {
var contributions []historyPayloadRef
seen := make(map[historyPayloadRef]struct{})
for _, observationID := range builder.model.observationsByPointer[path.String()] {
observation := builder.model.observations[observationID]
if observation.phase != historyPhaseSources || observation.role != historyRoleContribution || !sameConcreteHistoryPath(path, observation.path) {
continue
}
attribution, ok := builder.model.resolveAttribution(observation.attributionRef)
if !ok || !sourceLocationsContainedBy(attribution.origin, summary) {
continue
}
if observation.payload.id < 0 || observation.payload.id >= len(builder.model.sourcePayloads) {
continue
}
payload := builder.model.sourcePayloads[observation.payload.id]
if payload.kind != historySourceApplicationPayload {
continue
}
local, ok := historyApplicationLocalPath(payload.application, path)
if !ok {
continue
}
if _, exists := documentAtPath(payload.application.value, local); !exists {
continue
}
if _, exists := seen[observation.payload]; exists {
continue
}
seen[observation.payload] = struct{}{}
contributions = append(contributions, observation.payload)
}
return contributions
}
func sourceLocationsContainedBy(candidate, summary Origin) bool {
if len(candidate.Locations) == 0 {
return false
}
available := make(map[Location]struct{}, len(summary.Locations))
for _, location := range summary.Locations {
available[location] = struct{}{}
}
for _, location := range candidate.Locations {
if _, found := available[location]; !found {
return false
}
}
return true
}
func sourceApplicationOrigin(application historyApplication, node *document.Node) Origin {
origin := cloneOrigin(application.origin)
origin.Layer = application.layer
if node.Location.SourceID != "" {
origin.Locations = []Location{publicLocation(node.Location)}
}
return origin
}
func sourceOriginRepresentedByOperation(model *historyModel, path Path, origin Origin) bool {
represented := false
model.reportedPathIndex.visitAncestors(path.String(), func(observationID int) {
observation := model.observations[observationID]
if observation.phase != historyPhaseSources || observation.role != historyRoleOperation {
return
}
operationOrigin, ok := model.resolveAttribution(observation.attributionRef)
if !ok {
return
}
if originRepresentedByChange(path, origin, Change{
Path: observation.reportedPath, Origin: operationOrigin.origin,
}) {
represented = true
}
})
return represented
}
func originRepresentedByChange(path Path, origin Origin, change Change) bool {
if change.Path.String() == path.String() {
return originContainedBy(origin, change.Origin)
}
if !pathContains(change.Path, path) || origin.Layer != change.Origin.Layer || origin.Operation != change.Origin.Operation {
return false
}
// A subtree replacement records descendant contributions only when their
// source identities are present in the ancestor operation attribution.
recordedSources := make(map[string]struct{}, len(change.Origin.Locations))
for _, recorded := range change.Origin.Locations {
recordedSources[recorded.SourceID] = struct{}{}
}
for _, location := range origin.Locations {
if _, found := recordedSources[location.SourceID]; !found {
return false
}
}
return true
}