Normalize baseline and source history evidence

This commit is contained in:
2026-10-26 15:01:54 +00:00
parent 2419e63717
commit 3828b117e8
6 changed files with 472 additions and 31 deletions

View File

@@ -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.

View File

@@ -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:

View 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
}

View 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
}

View File

@@ -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]

View File

@@ -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
} }