Retain import inclusion graph during acquisition

This commit is contained in:
2026-10-19 17:34:21 +00:00
parent 1aa121df71
commit fbe9ae6d8b
5 changed files with 377 additions and 38 deletions

View File

@@ -8,6 +8,7 @@ import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
type composedDocument struct {
root *document.Node
acquisition []acquisitionScope
importGraph importInclusionGraph
claims []compositionClaim
sequenceRanges []sequenceRange
origins map[string]Origin
@@ -51,6 +52,7 @@ func cloneComposedDocument(base *composedDocument) *composedDocument {
return &composedDocument{
root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims),
acquisition: cloneAcquisitionScopes(base.acquisition),
importGraph: base.importGraph.clone(),
sequenceRanges: cloneSequenceRanges(base.sequenceRanges),
origins: cloneOriginIndex(base.origins), overlays: cloneOverlayRecords(base.overlays),
applications: cloneHistoryApplications(base.applications),

View File

@@ -0,0 +1,190 @@
package engine
import (
"fmt"
"path/filepath"
"strings"
)
type importNodeKey struct {
namespaceKind string
namespaceAnchor string
resourceKind string
resourcePath string
}
func (key importNodeKey) dependencyKey() dependencyKey {
return dependencyKey{key.namespaceKind, key.namespaceAnchor, key.resourceKind, key.resourcePath}
}
type importContributionRole string
const (
importRoleMain importContributionRole = "main"
importRoleMapping importContributionRole = "mapping-import"
importRoleItem importContributionRole = "item-import"
)
type importGraphNode struct {
key importNodeKey
canonical string
sourceID string
role importContributionRole
}
type importInclusionEdge struct {
importer importNodeKey
imported importNodeKey
location Location
}
// importInclusionGraph retains source inclusion independently of mapping-scope
// ownership. Scheduling edges point from each imported prerequisite to its
// importer, matching the shared dependency scheduler's convention.
type importInclusionGraph struct {
nodes []importGraphNode
edges []importInclusionEdge
index map[importNodeKey]int
}
func newImportInclusionGraph() importInclusionGraph {
return importInclusionGraph{index: make(map[importNodeKey]int)}
}
func (graph *importInclusionGraph) addNode(node importGraphNode) int {
if graph.index == nil {
graph.index = make(map[importNodeKey]int)
}
if index, exists := graph.index[node.key]; exists {
return index
}
index := len(graph.nodes)
graph.index[node.key] = index
graph.nodes = append(graph.nodes, node)
return index
}
func (graph *importInclusionGraph) addEdge(importer, imported importNodeKey, location Location) int {
index := len(graph.edges)
graph.edges = append(graph.edges, importInclusionEdge{
importer: importer, imported: imported, location: location,
})
return index
}
func (graph importInclusionGraph) clone() importInclusionGraph {
cloned := importInclusionGraph{
nodes: append([]importGraphNode(nil), graph.nodes...),
edges: make([]importInclusionEdge, len(graph.edges)),
index: make(map[importNodeKey]int, len(graph.index)),
}
for index, edge := range graph.edges {
cloned.edges[index] = importInclusionEdge{
importer: edge.importer, imported: edge.imported, location: edge.location,
}
}
for key, index := range graph.index {
cloned.index[key] = index
}
return cloned
}
func (graph importInclusionGraph) scheduleCycle(closingEdge int, location Location) (Diagnostic, bool) {
keys := make([]dependencyKey, len(graph.nodes))
for index, node := range graph.nodes {
keys[index] = node.key.dependencyKey()
}
edges := make([]dependencyEdge, 0, len(graph.edges))
for _, edge := range graph.edges {
importer, importerOK := graph.index[edge.importer]
imported, importedOK := graph.index[edge.imported]
if !importerOK || !importedOK {
continue
}
edges = append(edges, dependencyEdge{prerequisite: imported, dependent: importer})
}
_, cycle := scheduleDependencies(keys, edges)
if len(cycle) < 2 {
return Diagnostic{}, false
}
// Scheduler edges reverse inclusion edges. Reverse its closed cycle to
// render the authored import route while keeping the canonical start node.
route := make([]int, 0, len(cycle))
route = append(route, cycle[0])
for index := len(cycle) - 2; index > 0; index-- {
route = append(route, cycle[index])
}
route = append(route, cycle[0])
paths := make([]string, len(route))
for index, node := range route {
paths[index] = graph.nodes[node].canonical
}
related := make([]Location, 0, len(route)-2)
for index := 0; index+1 < len(route); index++ {
importerKey := graph.nodes[route[index]].key
importedKey := graph.nodes[route[index+1]].key
edgeIndex := -1
for candidate, edge := range graph.edges {
if edge.importer == importerKey && edge.imported == importedKey {
edgeIndex = candidate
if candidate == closingEdge {
break
}
}
}
if edgeIndex == closingEdge {
continue
}
if edgeIndex >= 0 {
related = append(related, graph.edges[edgeIndex].location)
}
}
return Diagnostic{
Code: CodeImportCycle, Severity: SeverityError,
Message: "import cycle detected: " + strings.Join(paths, " -> "),
Primary: cloneLocation(&location), Related: related,
}, true
}
func importKeyFor(access *filesystemAccess, canonical string, synthetic bool) importNodeKey {
kind := "host"
resourceKind := "file"
resourcePath := filepath.ToSlash(canonical)
if access.native != nil {
kind = "native"
}
if synthetic {
resourceKind = "synthetic"
resourcePath = ""
}
return importNodeKey{
namespaceKind: kind,
// This traversal owns the main input position; grouped inputs receive
// their explicit positions when their acquisition graphs are combined.
namespaceAnchor: fmt.Sprintf("%020d/%020d", 0, 0),
resourceKind: resourceKind, resourcePath: resourcePath,
}
}
func importKeyForMainSource(source *SourceInfo) importNodeKey {
if source == nil {
return importNodeKey{}
}
namespaceKind := "host"
if source.Kind == SourceKindFilesystem {
namespaceKind = "native"
}
synthetic := source.Kind != SourceKindFile && source.Kind != SourceKindFilesystem
resourceKind := "file"
resourcePath := filepath.ToSlash(source.Path)
if synthetic {
resourceKind = "synthetic"
resourcePath = ""
}
return importNodeKey{
namespaceKind: namespaceKind, namespaceAnchor: fmt.Sprintf("%020d/%020d", 0, 0),
resourceKind: resourceKind, resourcePath: resourcePath,
}
}

View File

@@ -35,6 +35,7 @@ func defaultImportLimits() importLimits {
type importRoute struct {
canonical string
locations []Location
key importNodeKey
}
type importAccounting struct {
@@ -59,6 +60,7 @@ type importTraversal struct {
active map[resourceIdentity]int
visited map[resourceIdentity]importRoute
stack []importRoute
importGraph importInclusionGraph
}
func loadImportedDocuments(source Source, context *FilesystemContext, main parsedSource, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) {
@@ -139,11 +141,12 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
if mainPath != "" {
mainInfo.Path = mainPath
}
mainKey := access.identity(mainPath)
mainResource := access.identity(mainPath)
mainNodeKey := importKeyFor(access, mainPath, mainPath == "")
if mainPath == "" {
mainKey = access.syntheticIdentity()
mainResource = access.syntheticIdentity()
}
if previous, repeated := visited[mainKey]; repeated {
if previous, repeated := visited[mainResource]; repeated {
_ = access.close()
return nil, sourceInfos(main.source), []Diagnostic{importRepeatedDiagnostic(Location{SourceID: main.source.ID}, previous, importRoute{})}
}
@@ -152,9 +155,10 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
accounting.nodes += countInputNodes(main.document)
state := importTraversal{
access: access, accounting: accounting, limits: limits, rule: rule, versionState: versionState,
scope: acquisitionScope{destination: destination.clone()},
scope: acquisitionScope{owner: mainNodeKey, destination: destination.clone()},
sources: []SourceInfo{mainInfo},
active: make(map[resourceIdentity]int), visited: visited,
importGraph: newImportInclusionGraph(),
}
if mainIsItem {
state.itemScope = true
@@ -176,8 +180,10 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
mainIdentity = "<selected-source>"
}
mainRoute := importRoute{canonical: mainIdentity}
state.active[mainKey] = 0
state.visited[mainKey] = mainRoute
state.importGraph.addNode(importGraphNode{key: mainNodeKey, canonical: mainIdentity, sourceID: mainInfo.ID, role: importRoleMain})
mainRoute.key = mainNodeKey
state.active[mainResource] = 0
state.visited[mainResource] = mainRoute
state.stack = append(state.stack, mainRoute)
var body *document.Node
var declarations []importDeclaration
@@ -191,7 +197,8 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
if len(diagnostics) == 0 {
if mainIsItem {
state.scope.contributions = append(state.scope.contributions, acquiredContribution{
source: cloneSourceInfo(mainInfo), payload: body, document: body, processing: processing,
source: cloneSourceInfo(mainInfo), identity: mainNodeKey, role: importRoleMain,
scopeOwner: mainNodeKey, payload: body, document: body, processing: processing,
})
diagnostics = state.walkDeclarations(basePath, declarations, 0, Path{})
} else {
@@ -205,12 +212,16 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
accounting.nodes += placementNodes
state.scope.contributions = append(state.scope.contributions, acquiredContribution{
source: cloneSourceInfo(mainInfo), payload: body, document: placed,
identity: mainNodeKey, role: importRoleMain, scopeOwner: mainNodeKey,
destination: destination.clone(), processing: processing,
})
diagnostics = state.walkDeclarations(basePath, declarations, 0, destination)
}
}
}
if len(diagnostics) == 0 {
diagnostics = state.composePendingItems(&state.scope)
}
closeErr := access.close()
if len(diagnostics) != 0 {
@@ -267,13 +278,16 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
if composed != nil {
synthetic := acquisitionScope{contributions: []acquiredContribution{{
source: cloneSourceInfo(mainInfo), payload: completed.root, document: placed,
identity: mainNodeKey, role: importRoleMain, scopeOwner: mainNodeKey,
destination: destination.clone(), processing: processing, nested: completed,
}}}
synthetic.owner = mainNodeKey
projectNestedEvidence(composed, synthetic)
projectAcquiredDestinations(composed, synthetic, state.sources)
composed.acquisition = []acquisitionScope{cloneAcquisitionScope(synthetic)}
composed.inputBytes = accounting.bytes
composed.inputNodes = accounting.nodes
composed.importGraph = state.importGraph.clone()
}
return composed, cloneSourceInfos(state.sources), failures
}
@@ -293,6 +307,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
composed.acquisition = []acquisitionScope{cloneAcquisitionScope(state.scope)}
composed.inputBytes = accounting.bytes
composed.inputNodes = accounting.nodes
composed.importGraph = state.importGraph.clone()
}
return composed, cloneSourceInfos(state.sources), diagnostics
}
@@ -386,11 +401,18 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
currentRoute := s.currentRoute(location)
identity := s.access.identity(canonical)
nodeKey := importKeyFor(s.access, canonical, false)
importerKey := s.currentImportNode()
if activeIndex, ok := s.active[identity]; ok {
closingEdge := s.importGraph.addEdge(importerKey, nodeKey, location)
if diagnostic, ok := s.importGraph.scheduleCycle(closingEdge, location); ok {
return []Diagnostic{diagnostic}
}
cycle := append([]importRoute(nil), s.stack[activeIndex:]...)
return []Diagnostic{importCycleDiagnostic(canonical, location, cycle)}
}
if previous, ok := s.visited[identity]; ok {
s.importGraph.addEdge(importerKey, nodeKey, location)
return []Diagnostic{importRepeatedDiagnostic(location, previous, currentRoute)}
}
if s.accounting.sources+1 > s.limits.sources {
@@ -467,12 +489,18 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
if declaration.placement == placementModeSequenceItem {
mode = placementModeSequenceItem
}
role := importRoleMapping
if mode == placementModeSequenceItem {
route := importRoute{canonical: canonical, locations: currentRoute.locations}
role = importRoleItem
}
s.importGraph.addNode(importGraphNode{key: nodeKey, canonical: canonical, sourceID: sourceID, role: role})
s.importGraph.addEdge(importerKey, nodeKey, location)
if mode == placementModeSequenceItem {
route := importRoute{canonical: canonical, locations: currentRoute.locations, key: nodeKey}
s.active[identity] = len(s.stack)
s.visited[identity] = route
s.stack = append(s.stack, route)
diagnostics := s.visitItemSource(canonical, declaration, destination, relativeDestination, depth, info, body, declarations, processing)
diagnostics := s.visitItemSource(canonical, nodeKey, declaration, destination, relativeDestination, depth, info, body, declarations, processing)
delete(s.active, identity)
s.stack = s.stack[:len(s.stack)-1]
return diagnostics
@@ -486,9 +514,10 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)}
}
s.accounting.nodes += placementNodes
route := importRoute{canonical: canonical, locations: currentRoute.locations}
route := importRoute{canonical: canonical, locations: currentRoute.locations, key: nodeKey}
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
source: cloneSourceInfo(info), payload: body, document: placed,
identity: nodeKey, role: role, scopeOwner: s.scope.owner,
destination: placementDestination.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
})
s.active[identity] = len(s.stack)
@@ -500,6 +529,17 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
return diagnostics
}
func (s *importTraversal) currentImportNode() importNodeKey {
if len(s.stack) != 0 {
return s.stack[len(s.stack)-1].key
}
key := s.scope.owner
if _, exists := s.importGraph.index[key]; !exists {
s.importGraph.addNode(importGraphNode{key: key, canonical: "<selected-source>", role: importRoleMain})
}
return key
}
func (s *importTraversal) sourceDestination(destination Path) Path {
if !s.itemScope {
return destination.clone()
@@ -509,10 +549,10 @@ func (s *importTraversal) sourceDestination(destination Path) Path {
return s.itemFallback.clone()
}
func (s *importTraversal) visitItemSource(canonical string, declaration importDeclaration, destination, relativeDestination Path, depth int, info SourceInfo, body *document.Node, declarations []importDeclaration, processing *sourceVersionProcessingResult) []Diagnostic {
func (s *importTraversal) visitItemSource(canonical string, identity importNodeKey, declaration importDeclaration, destination, relativeDestination Path, depth int, info SourceInfo, body *document.Node, declarations []importDeclaration, processing *sourceVersionProcessingResult) []Diagnostic {
parentScope := s.scope
parentItemScope, parentPrefix, parentFallback := s.itemScope, s.itemPrefix, s.itemFallback
s.scope = acquisitionScope{}
s.scope = acquisitionScope{owner: identity}
s.itemScope = true
if parentItemScope {
s.itemPrefix = appendDestination(parentPrefix, destination)
@@ -522,6 +562,7 @@ func (s *importTraversal) visitItemSource(canonical string, declaration importDe
}
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
source: cloneSourceInfo(info), payload: body, document: body,
identity: identity, role: importRoleItem, scopeOwner: identity,
processing: processing,
})
diagnostics := s.walkDeclarations(s.access.dir(canonical), declarations, depth, Path{})
@@ -530,35 +571,53 @@ func (s *importTraversal) visitItemSource(canonical string, declaration importDe
if len(diagnostics) != 0 {
return projectItemDiagnosticPaths(diagnostics, parentScope, destination)
}
documents := make([]*document.Node, 0, len(itemScope.contributions))
for _, contribution := range itemScope.contributions {
documents = append(documents, contribution.document)
}
completed, diagnostics := composeDocuments(documents...)
if len(diagnostics) != 0 {
return projectItemDiagnosticPaths(diagnostics, parentScope, destination)
}
itemScope.destination = Path{}
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(itemScope)}
projectNestedEvidence(completed, itemScope)
pl := destination
_, placed, err := placeDocumentWithMode(completed.root, pl, placementModeSequenceItem, body.Location, maxDocumentDepth, maxDocumentNodes)
if err != nil {
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source depth or node limit after destination placement", &declaration.location)}
}
placementNodes := destinationWrapperNodeCountForMode(pl, placementModeSequenceItem)
if s.accounting.nodes+placementNodes > s.limits.nodes {
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &declaration.location)}
}
s.accounting.nodes += placementNodes
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
source: cloneSourceInfo(info), payload: completed.root, document: placed,
destination: pl.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
nested: completed,
source: cloneSourceInfo(info), payload: body,
identity: identity, role: importRoleItem, scopeOwner: parentScope.owner,
destination: destination.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
pendingItem: &itemScope,
})
return nil
}
func (s *importTraversal) composePendingItems(scope *acquisitionScope) []Diagnostic {
for index := range scope.contributions {
contribution := &scope.contributions[index]
if contribution.pendingItem == nil {
continue
}
itemScope := contribution.pendingItem
if diagnostics := s.composePendingItems(itemScope); len(diagnostics) != 0 {
return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination)
}
documents := make([]*document.Node, 0, len(itemScope.contributions))
for _, nested := range itemScope.contributions {
documents = append(documents, nested.document)
}
completed, diagnostics := composeDocuments(documents...)
if len(diagnostics) != 0 {
return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination)
}
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(*itemScope)}
projectNestedEvidence(completed, *itemScope)
_, placed, err := placeDocumentWithMode(completed.root, contribution.destination, placementModeSequenceItem, contribution.payload.Location, maxDocumentDepth, maxDocumentNodes)
if err != nil {
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source depth or node limit after destination placement", contribution.source.ImportedFrom)}
}
placementNodes := destinationWrapperNodeCountForMode(contribution.destination, placementModeSequenceItem)
if s.accounting.nodes+placementNodes > s.limits.nodes {
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", contribution.source.ImportedFrom)}
}
s.accounting.nodes += placementNodes
contribution.payload = completed.root
contribution.document = placed
contribution.nested = completed
contribution.pendingItem = nil
}
return nil
}
func importedVersionClaim(documents []*document.Node, composed *composedDocument, rule *VersionRule, mainIndex int) *Diagnostic {
if composed == nil || rule == nil {
return nil

View File

@@ -656,6 +656,22 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
}
})
t.Run("cycle through an item scope", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports:
- path: item.yml
destination: [items]
placement: sequence-item
`)
writeImportFile(t, directory, "item.yml", "configkit: {imports: [{path: main.yml}]}\n")
loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportCycle)
diagnostic := loadError.Diagnostics()[0]
if diagnostic.Primary == nil || len(diagnostic.Related) != 1 || !strings.Contains(diagnostic.Message, "main.yml") || !strings.Contains(diagnostic.Message, "item.yml") {
t.Fatalf("nested item cycle diagnostic = %#v", diagnostic)
}
})
t.Run("diamond", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: left.yml}, {path: right.yml}]\n")
@@ -686,6 +702,61 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
})
}
func TestItemAcquisitionRetainsInclusionGraphAndScopeOwnership(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports:
- path: item.yml
destination: [items]
placement: sequence-item
`)
itemPath := writeImportFile(t, directory, "item.yml", "configkit: {imports: [{path: helper.yml}]}\nname: item\n")
helperPath := writeImportFile(t, directory, "helper.yml", "id: supplied\n")
input, diagnostics := assembleBaseInput[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
if len(diagnostics) != 0 {
t.Fatalf("assemble input: %#v", diagnostics)
}
graph := input.composition.importGraph
if len(graph.nodes) != 3 || len(graph.edges) != 2 {
t.Fatalf("inclusion graph has %d nodes and %d edges, want 3 and 2", len(graph.nodes), len(graph.edges))
}
for index, want := range []struct {
path string
role importContributionRole
}{
{path: canonicalTestPath(t, mainPath), role: importRoleMain},
{path: canonicalTestPath(t, itemPath), role: importRoleItem},
{path: canonicalTestPath(t, helperPath), role: importRoleMapping},
} {
node := graph.nodes[index]
if node.canonical != want.path || node.role != want.role || node.sourceID != "source-"+strconv.Itoa(index+1) {
t.Errorf("graph node %d = %#v, want %s, %q, source-%d", index, node, want.role, want.path, index+1)
}
}
rootScope := input.composition.acquisition[0]
if rootScope.owner != graph.nodes[0].key || len(rootScope.contributions) != 2 {
t.Fatalf("root acquisition scope = %#v", rootScope)
}
item := rootScope.contributions[1]
if item.identity != graph.nodes[1].key || item.role != importRoleItem || item.scopeOwner != rootScope.owner || item.nested == nil {
t.Fatalf("outer item contribution = %#v", item)
}
itemScope := item.nested.acquisition[0]
if itemScope.owner != item.identity || len(itemScope.contributions) != 2 {
t.Fatalf("nested item scope = %#v", itemScope)
}
for _, contribution := range itemScope.contributions {
if contribution.scopeOwner != item.identity {
t.Errorf("nested contribution scope owner = %#v, want item %v", contribution.scopeOwner, item.identity)
}
}
if graph.edges[0].importer != graph.nodes[0].key || graph.edges[0].imported != graph.nodes[1].key || graph.edges[0].location.SourceID != "source-1" ||
graph.edges[1].importer != graph.nodes[1].key || graph.edges[1].imported != graph.nodes[2].key || graph.edges[1].location.SourceID != "source-2" {
t.Fatalf("inclusion edges = %#v", graph.edges)
}
}
func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
t.Skip("initial filesystem contract covers Linux and macOS")

View File

@@ -23,37 +23,51 @@ type assembledInput struct {
// boundary for later scoped assembly and evidence.
type acquiredContribution struct {
source SourceInfo
identity importNodeKey
role importContributionRole
scopeOwner importNodeKey
payload *document.Node
document *document.Node
destination Path
relativeDestination Path
processing *sourceVersionProcessingResult
nested *composedDocument
pendingItem *acquisitionScope
}
// acquisitionScope is the ordered strict mapping scope produced by one input
// and its ordinary imports.
type acquisitionScope struct {
owner importNodeKey
destination Path
contributions []acquiredContribution
}
func cloneAcquisitionScope(scope acquisitionScope) acquisitionScope {
cloned := acquisitionScope{destination: scope.destination.clone()}
cloned := acquisitionScope{owner: scope.owner, destination: scope.destination.clone()}
if scope.contributions != nil {
cloned.contributions = make([]acquiredContribution, len(scope.contributions))
for index, contribution := range scope.contributions {
cloned.contributions[index] = acquiredContribution{
source: cloneSourceInfo(contribution.source), payload: contribution.payload,
source: cloneSourceInfo(contribution.source), identity: contribution.identity,
role: contribution.role, scopeOwner: contribution.scopeOwner, payload: contribution.payload,
document: contribution.document, destination: contribution.destination.clone(),
relativeDestination: contribution.relativeDestination.clone(), processing: contribution.processing,
nested: cloneComposedDocument(contribution.nested),
nested: cloneComposedDocument(contribution.nested), pendingItem: cloneAcquisitionScopePointer(contribution.pendingItem),
}
}
}
return cloned
}
func cloneAcquisitionScopePointer(scope *acquisitionScope) *acquisitionScope {
if scope == nil {
return nil
}
cloned := cloneAcquisitionScope(*scope)
return &cloned
}
func cloneAcquisitionScopes(scopes []acquisitionScope) []acquisitionScope {
if scopes == nil {
return nil
@@ -145,10 +159,13 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
}
localPayload := input.document
input.document = placed
identity := importKeyForMainSource(parsed.source)
input.acquisition = []acquisitionScope{{
owner: identity,
destination: destination.clone(),
contributions: []acquiredContribution{{
source: cloneSourceInfo(*parsed.source), payload: localPayload,
identity: identity, role: importRoleMain, scopeOwner: identity,
document: placed, destination: destination.clone(), processing: versionProcessing,
}},
}}