Normalize baseline and source history evidence
This commit is contained in:
227
internal/engine/history_source.go
Normal file
227
internal/engine/history_source.go
Normal file
@@ -0,0 +1,227 @@
|
||||
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
|
||||
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
|
||||
}
|
||||
for index := range applications {
|
||||
application := applications[index]
|
||||
payload := builder.addSourcePayload(&application, nil)
|
||||
walkHistoryApplication(application, func(_, effective Path, node *document.Node) {
|
||||
origin := sourceApplicationOrigin(application, node)
|
||||
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)
|
||||
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 (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 _, 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
|
||||
}
|
||||
Reference in New Issue
Block a user