Files
configkit/internal/engine/history_select.go

369 lines
11 KiB
Go

package engine
import (
"slices"
"sort"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
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
classify 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))
indices := make(map[string][]int)
add := func(path Path, sensitive, classify bool) int {
if !path.valid() {
return -1
}
pointer := path.String()
for _, index := range indices[pointer] {
if sameConcreteHistoryPath(candidates[index].path, path) {
candidates[index].sensitive = candidates[index].sensitive || sensitive
candidates[index].classify = candidates[index].classify || classify
return index
}
}
index := len(candidates)
candidates = append(candidates, historySensitivityCandidate{path: path.clone(), sensitive: sensitive, classify: classify})
indices[pointer] = append(indices[pointer], index)
return index
}
for _, observation := range model.observations {
concreteCandidate := observation.role == historyRoleControl
if observation.role == historyRoleContribution {
if application, found := model.applicationPayload(observation.payload); found && len(application.sequences) != 0 {
concreteCandidate = true
}
}
index := add(observation.path, observation.sensitive, concreteCandidate)
if parsed, err := ParsePath(observation.path.String()); err == nil {
add(parsed, observation.sensitive, true)
}
if index < 0 {
continue
}
if attribution, found := model.resolveAttribution(observation.attributionRef); found {
for _, dependency := range attribution.dependencies {
if dependency.valid() {
appendDistinctHistoryPath(&candidates[index].dependencies, dependency)
add(dependency, false, true)
}
}
}
}
_, inherited := model.sensitivityDependencies()
for _, link := range inherited {
add(link.target, false, true)
add(link.dependency, false, true)
}
return candidates
}
func appendDistinctHistoryPath(paths *[]Path, path Path) {
for _, existing := range *paths {
if sameConcreteHistoryPath(existing, path) {
return
}
}
*paths = append(*paths, path.clone())
}
// sensitivityDependencies returns the original attribution edges and the
// bidirectional links represented by inheritance payloads. It intentionally
// does not derive edges from the public metadata projection, which may omit
// evidence needed to keep historical values redacted.
func (model *historyModel) sensitivityDependencies() (dependencies, inherited []secretDependency) {
if model == nil {
return nil, nil
}
for _, observation := range model.observations {
if observation.phase == historyPhaseInheritance {
continue
}
attribution, found := model.resolveAttribution(observation.attributionRef)
if !found {
continue
}
for _, dependency := range attribution.dependencies {
if observation.reportedPath.valid() && dependency.valid() {
dependencies = append(dependencies, secretDependency{
target: observation.reportedPath.clone(), dependency: dependency.clone(),
})
}
}
}
for _, payload := range model.inheritancePayloads {
for _, operation := range payload.operations {
if operation.control != nil {
control := operation.control.path
for _, contributor := range operation.controlOrigin.contributors {
if control.valid() && contributor.source.valid() {
inherited = append(inherited, secretDependency{
target: control.clone(), dependency: contributor.source.clone(),
})
}
}
continue
}
if operation.transfer == nil || operation.parent == nil {
continue
}
transfer := operation.transfer
walkDocument(operation.parent, transfer.to, func(destination Path, _ *document.Node) {
relative, ok := relativeHistoryPath(transfer.to, destination)
if !ok {
return
}
source := appendInheritancePath(transfer.from, relative)
if _, found := operation.parentOrigins[source.String()]; !found {
return
}
inherited = append(inherited, secretDependency{
target: destination.clone(), dependency: source,
})
})
}
}
return dependencies, inherited
}
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
}
}