Files
configkit/internal/engine/history_select.go

259 lines
7.7 KiB
Go

package engine
import (
"slices"
"sort"
)
type historySelection struct {
path Path
phase historyPhase
payload historyPayloadRef
reportedPath Path
role historyRole
before bool
after bool
attribution historyAttribution
hasAttribution bool
previous []Origin
}
type historySensitivityCandidate struct {
path Path
sensitive bool
dependencies []Path
}
func (model *historyModel) selectHistory(path Path) []historySelection {
if model == nil || !path.valid() || !model.hasObservedShape(path) {
return nil
}
candidateSet := make(map[int]struct{})
for _, observationID := range model.observationsByPointer[path.String()] {
candidateSet[observationID] = struct{}{}
}
model.reportedPathIndex.visitAncestors(path.String(), func(observationID int) {
candidateSet[observationID] = struct{}{}
})
model.reportedPathIndex.visitDescendants(path.String(), func(observationID int) {
candidateSet[observationID] = struct{}{}
})
candidates := make([]int, 0, len(candidateSet))
for observationID := range candidateSet {
candidates = append(candidates, observationID)
}
slices.Sort(candidates)
selections := make([]historySelection, 0, len(candidates))
selectionByPayload := make(map[historyPayloadRef]int)
for _, observationID := range candidates {
observation := model.observations[observationID]
if observation.role == historyRoleInventory {
continue
}
reportedPath := observation.reportedPath
attributionRef := observation.attributionRef
if observation.role == historyRoleContribution {
if !historyObservationMatchesQuery(path, observation.path) {
continue
}
application, found := model.applicationPayload(observation.payload)
if !found {
continue
}
if pathContains(observation.path, path) && len(observation.path.segments) < len(path.segments) {
local, ok := historyApplicationLocalPath(application, path)
if !ok {
continue
}
if _, present := documentAtPathWithLookup(application.value, local, model.documentLookup); !present {
continue
}
}
if operationPath, relevant := applicationOperationPath(application, path, model.documentLookup); relevant {
reportedPath = operationPath
}
} else if !historyObservationMatchesQuery(path, observation.path) {
continue
}
selectionIndex, found := selectionByPayload[observation.payload]
if !found {
selectionIndex = len(selections)
selectionByPayload[observation.payload] = selectionIndex
selection := historySelection{
path: path.clone(),
phase: observation.phase,
payload: observation.payload,
reportedPath: reportedPath.clone(),
role: observation.role,
}
if attribution, ok := model.resolveAttribution(attributionRef); ok {
selection.attribution = attribution
selection.hasAttribution = true
}
selections = append(selections, selection)
}
selection := &selections[selectionIndex]
selection.before = selection.before || observation.beforePresent
selection.after = selection.after || observation.afterPresent
}
if len(selections) == 0 {
return nil
}
var previous *Origin
for index := range selections {
selection := &selections[index]
if selection.hasAttribution && metadataHistoryRole(selection.role) {
if previous != nil {
selection.previous = []Origin{cloneOrigin(*previous)}
}
origin := cloneOrigin(selection.attribution.origin)
previous = &origin
}
}
return selections
}
func historyObservationMatchesQuery(query, observed Path) bool {
if matchesPathShape(query, observed) {
return true
}
return pathContains(query, observed) && pathShapePrefixMatches(query, observed)
}
func (model *historyModel) hasObservedShape(path Path) bool {
if model == nil || !path.valid() {
return false
}
for _, candidate := range model.shapesByPointer[path.String()] {
if matchesPathShape(path, candidate) {
return true
}
}
return false
}
func (model *historyModel) concreteShapes(pointer string) []Path {
if model == nil {
return nil
}
shapes := model.shapesByPointer[pointer]
cloned := make([]Path, len(shapes))
for index, path := range shapes {
cloned[index] = path.clone()
}
return cloned
}
func (model *historyModel) observedPaths() []Path {
if model == nil {
return nil
}
paths := make([]Path, len(model.shapeOrder))
for index, path := range model.shapeOrder {
paths[index] = path.clone()
}
return paths
}
func (model *historyModel) historicalPaths() []Path {
if model == nil {
return []Path{}
}
pointers := make([]string, 0, len(model.shapesByPointer))
for pointer := range model.shapesByPointer {
pointers = append(pointers, pointer)
}
sort.Strings(pointers)
paths := make([]Path, 0, len(pointers))
for _, pointer := range pointers {
path, err := ParsePath(pointer)
if err == nil {
paths = append(paths, path)
}
}
return paths
}
func (model *historyModel) latestHistoricalLocation(path Path) (Location, bool) {
selections := model.selectHistory(path)
for selectionIndex := len(selections) - 1; selectionIndex >= 0; selectionIndex-- {
selection := selections[selectionIndex]
if selection.hasAttribution {
if location, found := latestOriginDocumentLocation(selection.attribution.origin); found {
return Location{SourceID: location.SourceID, Line: location.Line, Column: location.Column}, true
}
}
for previousIndex := len(selection.previous) - 1; previousIndex >= 0; previousIndex-- {
if location, found := latestOriginDocumentLocation(selection.previous[previousIndex]); found {
return Location{SourceID: location.SourceID, Line: location.Line, Column: location.Column}, true
}
}
}
return Location{}, false
}
func (model *historyModel) sensitivityCandidates() []historySensitivityCandidate {
if model == nil {
return nil
}
candidates := make([]historySensitivityCandidate, 0, len(model.observations))
for _, observation := range model.observations {
candidate := historySensitivityCandidate{path: observation.path.clone(), sensitive: observation.sensitive}
if attribution, found := model.resolveAttribution(observation.attributionRef); found {
candidate.dependencies = attribution.dependencies
}
candidates = append(candidates, candidate)
}
return candidates
}
func (model *historyModel) resolveAttribution(ref historyAttributionRef) (historyAttribution, bool) {
if model == nil || ref < 0 || int(ref) >= len(model.attributions) {
return historyAttribution{}, false
}
attribution := model.attributions[ref]
return historyAttribution{
origin: cloneOrigin(attribution.origin),
reasons: append([]string(nil), attribution.reasons...),
dependencies: clonePaths(attribution.dependencies),
}, true
}
func (model *historyModel) applicationPayload(ref historyPayloadRef) (historyApplication, bool) {
if model == nil || ref.id < 0 {
return historyApplication{}, false
}
var payloads []historySourcePayload
switch ref.phase {
case historyPhaseSources:
payloads = model.sourcePayloads
case historyPhaseCLI:
payloads = model.cliPayloads
default:
return historyApplication{}, false
}
if ref.id >= len(payloads) || payloads[ref.id].kind != historySourceApplicationPayload {
return historyApplication{}, false
}
return payloads[ref.id].application, true
}
func pathShapePrefixMatches(left, right Path) bool {
length := min(len(left.segments), len(right.segments))
leftPrefix := Path{segments: clonePathSegments(left.segments[:length], 0)}
rightPrefix := Path{segments: clonePathSegments(right.segments[:length], 0)}
return matchesPathShape(leftPrefix, rightPrefix)
}
func metadataHistoryRole(role historyRole) bool {
switch role {
case historyRoleDefinition, historyRoleCompositeDefinition, historyRoleOperation, historyRoleControl:
return true
default:
return false
}
}