Normalize baseline and source history evidence
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Interface Contract Remediation Implementation Plan
|
# Interface Contract Remediation Implementation Plan
|
||||||
|
|
||||||
Status: in progress. Stages 1–3 are complete; Stages 4–18 remain pending.
|
Status: in progress. Stages 1–4 are complete; Stages 5–18 remain pending.
|
||||||
Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`.
|
Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`.
|
||||||
Audience: a Luna coding agent executing the stages sequentially.
|
Audience: a Luna coding agent executing the stages sequentially.
|
||||||
|
|
||||||
@@ -366,7 +366,10 @@ to consume. Focused check once buildable:
|
|||||||
## Stage 4 — Normalize Baseline And Ordinary Source Evidence
|
## Stage 4 — Normalize Baseline And Ordinary Source Evidence
|
||||||
|
|
||||||
Owners: new `history_source.go`, `load_metadata.go`, `overlay.go`, composition
|
Owners: new `history_source.go`, `load_metadata.go`, `overlay.go`, composition
|
||||||
evidence consumers. Status: pending.
|
evidence consumers. Status: complete. Validation:
|
||||||
|
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'History.*Source|Compose|Overlay|Provenance|Explain' -count=1`,
|
||||||
|
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -count=1`,
|
||||||
|
and `git diff --check`.
|
||||||
|
|
||||||
1. Register baseline definitions by walking the canonical baseline with concrete
|
1. Register baseline definitions by walking the canonical baseline with concrete
|
||||||
paths and default origins. Keep absent roots and explicit null distinct.
|
paths and default origins. Keep absent roots and explicit null distinct.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type historyRole uint8
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
historyRoleDefinition historyRole = iota
|
historyRoleDefinition historyRole = iota
|
||||||
|
historyRoleContribution
|
||||||
historyRoleCompositeDefinition
|
historyRoleCompositeDefinition
|
||||||
historyRoleOperation
|
historyRoleOperation
|
||||||
historyRoleControl
|
historyRoleControl
|
||||||
@@ -54,8 +55,10 @@ type historyObservation struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type historyModel struct {
|
type historyModel struct {
|
||||||
observations []historyObservation
|
observations []historyObservation
|
||||||
attributions []historyAttribution
|
attributions []historyAttribution
|
||||||
|
baseline *document.Node
|
||||||
|
sourcePayloads []historySourcePayload
|
||||||
|
|
||||||
shapesByPointer map[string][]Path
|
shapesByPointer map[string][]Path
|
||||||
shapeOrder []Path
|
shapeOrder []Path
|
||||||
@@ -116,7 +119,7 @@ func (builder *historyBuilder) addObservation(observation historyObservation) in
|
|||||||
builder.model.observations = append(builder.model.observations, observation)
|
builder.model.observations = append(builder.model.observations, observation)
|
||||||
builder.addShape(observation.path)
|
builder.addShape(observation.path)
|
||||||
switch observation.role {
|
switch observation.role {
|
||||||
case historyRoleDefinition, historyRoleCompositeDefinition, historyRoleCheckpoint:
|
case historyRoleDefinition, historyRoleContribution, historyRoleCompositeDefinition, historyRoleCheckpoint:
|
||||||
pointer := observation.path.String()
|
pointer := observation.path.String()
|
||||||
builder.model.observationsByPointer[pointer] = append(builder.model.observationsByPointer[pointer], observationID)
|
builder.model.observationsByPointer[pointer] = append(builder.model.observationsByPointer[pointer], observationID)
|
||||||
case historyRoleOperation, historyRoleControl:
|
case historyRoleOperation, historyRoleControl:
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
211
internal/engine/history_source_test.go
Normal file
211
internal/engine/history_source_test.go
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHistorySourceObservationsPreserveRawAndCompositeDefinitions(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
baseline := parseDocumentForDecode(t, "defaults: {nullable: null}\n")
|
||||||
|
base := parseDocumentForDecode(t, "section: {same: equal, empty: {}}\n")
|
||||||
|
overlay := parseDocumentForDecode(t, "section: {same: equal, empty: {}}\n")
|
||||||
|
setHistoryDocumentSource(base, "base")
|
||||||
|
setHistoryDocumentSource(overlay, "overlay")
|
||||||
|
|
||||||
|
baseOrigins := make(map[string]Origin)
|
||||||
|
collectOverlayOrigins(baseOrigins, base, Path{}, LayerBase)
|
||||||
|
baseApplication := historyApplication{
|
||||||
|
layer: LayerBase, value: base,
|
||||||
|
origin: overlayOrigin(LayerBase, base.Location),
|
||||||
|
}
|
||||||
|
composition := &composedDocument{
|
||||||
|
root: document.Clone(base), origins: baseOrigins,
|
||||||
|
applications: []historyApplication{baseApplication},
|
||||||
|
}
|
||||||
|
overlayOrigins := make(map[string]Origin)
|
||||||
|
collectOverlayOrigins(overlayOrigins, overlay, Path{}, LayerOverlay)
|
||||||
|
assembly := newAssemblyAccumulator(composition)
|
||||||
|
overlayApplication := historyApplication{
|
||||||
|
layer: LayerOverlay, value: overlay,
|
||||||
|
origin: overlayOrigin(LayerOverlay, overlay.Location),
|
||||||
|
}
|
||||||
|
if diagnostics := assembly.applyOverlay(overlay, overlayOrigins, []historyApplication{overlayApplication}, LayerOverlay); len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("apply overlay diagnostics = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := newHistoryBuilder()
|
||||||
|
builder.observeBaseline(baseline)
|
||||||
|
builder.observeSourceEvidence(assembly.current.root, assembly.current.origins, assembly.current.applications, assembly.current.overlays)
|
||||||
|
model := builder.freeze()
|
||||||
|
|
||||||
|
defaultPath := Keys("defaults", "nullable")
|
||||||
|
baselineSelections := model.selectHistory(defaultPath)
|
||||||
|
if len(baselineSelections) != 1 || baselineSelections[0].phase != historyPhaseBaseline || baselineSelections[0].attribution.origin.Layer != LayerDefault || !baselineSelections[0].after {
|
||||||
|
t.Fatalf("baseline definition selections = %#v", baselineSelections)
|
||||||
|
}
|
||||||
|
if selections := model.selectHistory(Keys("defaults", "absent")); len(selections) != 0 {
|
||||||
|
t.Fatalf("absent baseline path selections = %#v", selections)
|
||||||
|
}
|
||||||
|
|
||||||
|
sameSelections := model.selectHistory(Keys("section", "same"))
|
||||||
|
var rawSources []string
|
||||||
|
var replacement bool
|
||||||
|
for _, selection := range sameSelections {
|
||||||
|
if selection.role == historyRoleContribution && selection.hasAttribution {
|
||||||
|
rawSources = append(rawSources, selection.attribution.origin.Locations[0].SourceID)
|
||||||
|
}
|
||||||
|
if selection.role == historyRoleOperation {
|
||||||
|
replacement = selection.before && selection.after
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(rawSources, []string{"base", "overlay"}) || !replacement {
|
||||||
|
t.Fatalf("equal raw contributions and replacement = sources %#v, selections %#v", rawSources, sameSelections)
|
||||||
|
}
|
||||||
|
|
||||||
|
emptySelections := model.selectHistory(Keys("section", "empty"))
|
||||||
|
var composite *historyAttribution
|
||||||
|
for index := range emptySelections {
|
||||||
|
if emptySelections[index].role == historyRoleCompositeDefinition {
|
||||||
|
composite = &emptySelections[index].attribution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if composite == nil || !reflect.DeepEqual(locationSourceIDs(composite.origin.Locations), []string{"base", "overlay"}) {
|
||||||
|
t.Fatalf("empty mapping composite attribution = %#v; selections %#v", composite, emptySelections)
|
||||||
|
}
|
||||||
|
var emptySummary *historySourcePayload
|
||||||
|
for index := range model.sourcePayloads {
|
||||||
|
payload := &model.sourcePayloads[index]
|
||||||
|
if payload.kind == historySourceSummaryPayload && sameConcreteHistoryPath(payload.path, Keys("section", "empty")) {
|
||||||
|
emptySummary = payload
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if emptySummary == nil || len(emptySummary.contributions) != 2 || emptySummary.contributions[0].id != 0 || emptySummary.contributions[1].id != 1 {
|
||||||
|
t.Fatalf("empty mapping summary contribution references = %#v", emptySummary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHistorySourceReplacementTracksConcreteBeforeAndAfterShapes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
base := parseDocumentForDecode(t, "shape: {'0': {leaf: old}}\n")
|
||||||
|
overlay := parseDocumentForDecode(t, "shape: [{leaf: new}]\n")
|
||||||
|
setHistoryDocumentSource(base, "base")
|
||||||
|
setHistoryDocumentSource(overlay, "overlay")
|
||||||
|
baseOrigins := make(map[string]Origin)
|
||||||
|
collectOverlayOrigins(baseOrigins, base, Path{}, LayerBase)
|
||||||
|
overlayOrigins := make(map[string]Origin)
|
||||||
|
collectOverlayOrigins(overlayOrigins, overlay, Path{}, LayerOverlay)
|
||||||
|
composition := &composedDocument{
|
||||||
|
root: document.Clone(base), origins: baseOrigins,
|
||||||
|
applications: []historyApplication{{layer: LayerBase, value: base, origin: overlayOrigin(LayerBase, base.Location)}},
|
||||||
|
}
|
||||||
|
assembly := newAssemblyAccumulator(composition)
|
||||||
|
if diagnostics := assembly.applyOverlay(overlay, overlayOrigins, []historyApplication{{layer: LayerOverlay, value: overlay, origin: overlayOrigin(LayerOverlay, overlay.Location)}}, LayerOverlay); len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("apply overlay diagnostics = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(assembly.current.overlays) != 1 {
|
||||||
|
t.Fatalf("replacement records = %#v", assembly.current.overlays)
|
||||||
|
}
|
||||||
|
record := assembly.current.overlays[0]
|
||||||
|
if len(record.displaced) != 3 || len(record.introduced) != 3 {
|
||||||
|
t.Fatalf("displaced/introduction path counts = %d/%d", len(record.displaced), len(record.introduced))
|
||||||
|
}
|
||||||
|
if got := record.introduced[2].path; !sameConcreteHistoryPath(got, Keys("shape").Index(0).Key("leaf")) {
|
||||||
|
t.Fatalf("introduced descendant path = %#v", got)
|
||||||
|
}
|
||||||
|
builder := newHistoryBuilder()
|
||||||
|
builder.observeSourceEvidence(assembly.current.root, assembly.current.origins, assembly.current.applications, assembly.current.overlays)
|
||||||
|
model := builder.freeze()
|
||||||
|
modelIntroduction := model.sourcePayloads[2].overlay.introduced[2]
|
||||||
|
|
||||||
|
cloned := cloneOverlayRecords(assembly.current.overlays)
|
||||||
|
record.introduced[2].path.segments[2].key = "mutated"
|
||||||
|
record.introduced[2].origin.Locations[0].SourceID = "mutated"
|
||||||
|
if got := cloned[0].introduced[2]; !sameConcreteHistoryPath(got.path, Keys("shape").Index(0).Key("leaf")) || got.origin.Locations[0].SourceID != "overlay" {
|
||||||
|
t.Fatalf("cloned introduced evidence changed with producer: %#v", got)
|
||||||
|
}
|
||||||
|
if !sameConcreteHistoryPath(modelIntroduction.path, Keys("shape").Index(0).Key("leaf")) || modelIntroduction.origin.Locations[0].SourceID != "overlay" {
|
||||||
|
t.Fatalf("model-owned introduced evidence changed with producer: %#v", modelIntroduction)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldPath := Keys("shape", "0", "leaf")
|
||||||
|
newPath := Keys("shape").Index(0).Key("leaf")
|
||||||
|
oldSelections := model.selectHistory(oldPath)
|
||||||
|
newSelections := model.selectHistory(newPath)
|
||||||
|
if len(oldSelections) < 2 || oldSelections[len(oldSelections)-1].role != historyRoleOperation || !oldSelections[len(oldSelections)-1].before || oldSelections[len(oldSelections)-1].after {
|
||||||
|
t.Fatalf("removed key path selections = %#v", oldSelections)
|
||||||
|
}
|
||||||
|
if len(newSelections) < 2 || newSelections[len(newSelections)-1].role != historyRoleOperation || newSelections[len(newSelections)-1].before || !newSelections[len(newSelections)-1].after {
|
||||||
|
t.Fatalf("introduced index path selections = %#v", newSelections)
|
||||||
|
}
|
||||||
|
parsed, err := ParsePath("/shape/0/leaf")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
parsedSelections := model.selectHistory(parsed)
|
||||||
|
if len(parsedSelections) == 0 || !parsedSelections[len(parsedSelections)-1].before || !parsedSelections[len(parsedSelections)-1].after {
|
||||||
|
t.Fatalf("parsed path did not combine replacement sides: %#v", parsedSelections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverlayReplacementCapturesIntroducedPathOrigins(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
existing := parseDocumentForDecode(t, "old: value\n")
|
||||||
|
incoming := parseDocumentForDecode(t, "new: value\n")
|
||||||
|
setHistoryDocumentSource(existing, "base")
|
||||||
|
setHistoryDocumentSource(incoming, "overlay")
|
||||||
|
path := Keys("section")
|
||||||
|
origins := make(map[string]Origin)
|
||||||
|
collectOverlayOrigins(origins, existing, path, LayerBase)
|
||||||
|
incomingOrigins := make(map[string]Origin)
|
||||||
|
collectOverlayOrigins(incomingOrigins, incoming, path, LayerOverlay)
|
||||||
|
result := &composedDocument{
|
||||||
|
origins: origins,
|
||||||
|
}
|
||||||
|
recordOverlayReplacement(result, path, existing, incoming, LayerOverlay, true, incomingOrigins)
|
||||||
|
if len(result.overlays) != 1 || len(result.overlays[0].introduced) != 2 {
|
||||||
|
t.Fatalf("replacement introduced evidence = %#v", result.overlays)
|
||||||
|
}
|
||||||
|
introduced := result.overlays[0].introduced[1]
|
||||||
|
if !sameConcreteHistoryPath(introduced.path, path.Key("new")) || introduced.origin.Layer != LayerOverlay || introduced.origin.Locations[0].SourceID != "overlay" {
|
||||||
|
t.Fatalf("introduced replacement evidence = %#v", introduced)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHistorySourceEvidenceLeavesEmptySourcePhaseUnpopulated(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
builder := newHistoryBuilder()
|
||||||
|
builder.observeBaseline(parseDocumentForDecode(t, "defaults: {enabled: true}\n"))
|
||||||
|
builder.observeSourceEvidence(nil, nil, nil, nil)
|
||||||
|
model := builder.freeze()
|
||||||
|
if len(model.sourcePayloads) != 0 {
|
||||||
|
t.Fatalf("empty source input retained payloads: %#v", model.sourcePayloads)
|
||||||
|
}
|
||||||
|
for _, observation := range model.observations {
|
||||||
|
if observation.phase == historyPhaseSources {
|
||||||
|
t.Fatalf("empty source phase has an observation: %#v", observation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHistoryDocumentSource(root *document.Node, sourceID string) {
|
||||||
|
walkDocument(root, Path{}, func(_ Path, node *document.Node) {
|
||||||
|
node.Location.SourceID = sourceID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationSourceIDs(locations []Location) []string {
|
||||||
|
ids := make([]string, len(locations))
|
||||||
|
for index, location := range locations {
|
||||||
|
ids[index] = location.SourceID
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
@@ -147,28 +147,6 @@ func appendInputOriginEvent(events map[string][]Change, baseline map[string][]Ch
|
|||||||
appendInputChangeEvent(events, baseline, pointer, Change{Path: path.clone(), Origin: cloneOrigin(origin)})
|
appendInputChangeEvent(events, baseline, pointer, Change{Path: path.clone(), Origin: cloneOrigin(origin)})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 already records its descendants' contributions.
|
|
||||||
// Their coordinates differ from the container's, but their sources must be
|
|
||||||
// covered by the operation; matching only the precedence role is insufficient.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendInputChangeEvent(events map[string][]Change, baseline map[string][]Change, pointer string, value Change) {
|
func appendInputChangeEvent(events map[string][]Change, baseline map[string][]Change, pointer string, value Change) {
|
||||||
change := cloneChange(value)
|
change := cloneChange(value)
|
||||||
history := events[pointer]
|
history := events[pointer]
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type overlayRecord struct {
|
type overlayRecord struct {
|
||||||
change Change
|
change Change
|
||||||
displaced []displacedDefinition
|
displaced []displacedDefinition
|
||||||
|
introduced []historyPathOrigin
|
||||||
}
|
}
|
||||||
|
|
||||||
type displacedDefinition struct {
|
type displacedDefinition struct {
|
||||||
@@ -357,7 +358,8 @@ func mergeOverlayMappings(result *composedDocument, target, overlay *document.No
|
|||||||
Path: path.clone(), Origin: incomingOrigin(incomingOrigins, path, incoming, layer),
|
Path: path.clone(), Origin: incomingOrigin(incomingOrigins, path, incoming, layer),
|
||||||
Previous: previousOrigins(previous),
|
Previous: previousOrigins(previous),
|
||||||
},
|
},
|
||||||
displaced: displaced,
|
displaced: displaced,
|
||||||
|
introduced: overlayPathOrigins(incoming, incomingOrigins, path, layer),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -395,7 +397,7 @@ func recordOverlayReplacement(
|
|||||||
Path: path.clone(), Origin: incomingOrigin(incomingOrigins, path, incoming, layer),
|
Path: path.clone(), Origin: incomingOrigin(incomingOrigins, path, incoming, layer),
|
||||||
Previous: previousOrigins(previous),
|
Previous: previousOrigins(previous),
|
||||||
},
|
},
|
||||||
displaced: displaced,
|
displaced: displaced, introduced: overlayPathOrigins(incoming, incomingOrigins, path, layer),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -518,6 +520,17 @@ func collectOverlayOriginsFrom(origins, incomingOrigins map[string]Origin, node
|
|||||||
collectOverlayOriginsAt(origins, incomingOrigins, node, path, path, layer)
|
collectOverlayOriginsAt(origins, incomingOrigins, node, path, path, layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func overlayPathOrigins(node *document.Node, incomingOrigins map[string]Origin, path Path, layer Layer) []historyPathOrigin {
|
||||||
|
var introduced []historyPathOrigin
|
||||||
|
walkDocument(node, path, func(affected Path, value *document.Node) {
|
||||||
|
introduced = append(introduced, historyPathOrigin{
|
||||||
|
path: affected.clone(), origin: incomingOrigin(incomingOrigins, affected, value, layer),
|
||||||
|
sensitive: value.Sensitive,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return introduced
|
||||||
|
}
|
||||||
|
|
||||||
func collectOverlayOriginsAt(origins, incomingOrigins map[string]Origin, node *document.Node, from, to Path, layer Layer) {
|
func collectOverlayOriginsAt(origins, incomingOrigins map[string]Origin, node *document.Node, from, to Path, layer Layer) {
|
||||||
walkDocument(node, Path{}, func(relative Path, value *document.Node) {
|
walkDocument(node, Path{}, func(relative Path, value *document.Node) {
|
||||||
source := appendDestination(from, relative)
|
source := appendDestination(from, relative)
|
||||||
@@ -633,6 +646,12 @@ func cloneOverlayRecords(records []overlayRecord) []overlayRecord {
|
|||||||
path: displaced.path.clone(), origin: cloneOrigin(displaced.origin),
|
path: displaced.path.clone(), origin: cloneOrigin(displaced.origin),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cloned[index].introduced = make([]historyPathOrigin, len(record.introduced))
|
||||||
|
for introducedIndex, introduced := range record.introduced {
|
||||||
|
cloned[index].introduced[introducedIndex] = historyPathOrigin{
|
||||||
|
path: introduced.path.clone(), origin: cloneOrigin(introduced.origin), sensitive: introduced.sensitive,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return cloned
|
return cloned
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user