From 1a04bb38356ff4346912ab2882bdf545d291cd13 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 16 Oct 2026 15:55:29 +0000 Subject: [PATCH] Add private sequence-item placement --- docs/roadmap/implementation.md | 18 ++++- internal/engine/destination.go | 59 +++++++++++++-- internal/engine/destination_load_test.go | 92 ++++++++++++++++++++++++ internal/engine/destination_test.go | 81 +++++++++++++++++++++ internal/engine/layered.go | 13 ++-- internal/engine/load.go | 6 +- internal/engine/load_input.go | 5 +- internal/engine/load_request.go | 37 +++++----- internal/engine/options.go | 11 +++ internal/engine/records.go | 22 +++--- 10 files changed, 298 insertions(+), 46 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index a16d77f..bc62ecd 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Implementation Plan: Sequence Placement and Composition -Status: implementation in progress; stages 1–6 completed. +Status: implementation in progress; stages 1–7 completed. ## Purpose and authority @@ -451,6 +451,22 @@ Scope: placement primitives and engine per-input options; not public exposure. Validation: placement/accounting/option tests and `./scripts/check.sh`. Completion: the private operation places a mapping once with correct ownership. +Completion evidence: added private placement modes to engine options, load +requests, and source records, with preflight rejection of unknown modes and +item placement at an empty destination. A completed mapping can now be wrapped +in one singleton sequence at its destination; nil payloads remain absent and +empty mappings remain real items. Wrapper accounting includes both destination +mapping nodes and the added sequence depth/node, with limits checked before +allocation. Focused tests cover mapping zero-value equivalence, nested paths, +empty and absent payloads, invalid modes/destinations, and exact/exceeded +allowances. These checks passed with Go `go1.26.9 linux/amd64`: + +- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'Destination|Import|Mounted|PlaceDocument|Options|Layered' -count=1` +- `GOTOOLCHAIN=local ./scripts/check.sh` +- `git diff --check` + +No environmental limitation affected validation; checks ran on Linux/amd64. + ## Stage 8 — Assemble item scopes and YAML path/glob imports Scope: directive parsing and scoped acquisition/composition. diff --git a/internal/engine/destination.go b/internal/engine/destination.go index 1b1451f..ab0c49c 100644 --- a/internal/engine/destination.go +++ b/internal/engine/destination.go @@ -11,6 +11,13 @@ var ( errDestinationLimit = errors.New("configkit: destination exceeds document limits") ) +type placementMode uint8 + +const ( + placementModeMapping placementMode = iota + placementModeSequenceItem +) + func normalizeDestination(path Path) (Path, error) { if !path.valid() { return Path{}, errInvalidDestination @@ -47,18 +54,39 @@ func invalidDestinationDiagnostic(destination Path) Diagnostic { // placeDocument checks all per-document bounds before allocating destination // wrappers. Aggregate accounting remains the caller's responsibility. func placeDocument(body *document.Node, destination Path, sourceRoot document.Location, maxDepth, maxNodes int) (Path, *document.Node, error) { + return placeDocumentWithMode(body, destination, placementModeMapping, sourceRoot, maxDepth, maxNodes) +} + +// placeDocumentWithMode places a source mapping beneath its literal destination. +// Sequence-item placement adds one sequence containing the complete mapping. +func placeDocumentWithMode(body *document.Node, destination Path, mode placementMode, sourceRoot document.Location, maxDepth, maxNodes int) (Path, *document.Node, error) { normalized, err := normalizeDestination(destination) if err != nil { return Path{}, nil, err } + if mode != placementModeMapping && mode != placementModeSequenceItem { + return Path{}, nil, errInvalidDestination + } + if mode == placementModeSequenceItem && len(normalized.segments) == 0 { + return Path{}, nil, errInvalidDestination + } if body == nil { return normalized, nil, nil } - if !destinationWithinLimits(body, normalized, maxDepth, maxNodes) { + if mode == placementModeSequenceItem && body.Kind != document.Mapping { + return normalized, nil, errInvalidDestination + } + if !destinationWithinLimitsWithMode(body, normalized, mode, maxDepth, maxNodes) { return normalized, nil, errDestinationLimit } placed := body + if mode == placementModeSequenceItem { + placed = &document.Node{ + Kind: document.Sequence, Location: sourceRoot, + Sequence: []*document.Node{placed}, + } + } for index := len(normalized.segments) - 1; index >= 0; index-- { key := normalized.segments[index].key placed = &document.Node{ @@ -77,28 +105,49 @@ func placeDocument(body *document.Node, destination Path, sourceRoot document.Lo // destinationWithinLimits expects a normalized mapping-key destination. func destinationWithinLimits(body *document.Node, destination Path, maxDepth, maxNodes int) bool { + return destinationWithinLimitsWithMode(body, destination, placementModeMapping, maxDepth, maxNodes) +} + +func destinationWithinLimitsWithMode(body *document.Node, destination Path, mode placementMode, maxDepth, maxNodes int) bool { if body == nil { return true } - if maxDepth < 0 || maxNodes < 0 || !destination.valid() { + if maxDepth < 0 || maxNodes < 0 || !destination.valid() || (mode != placementModeMapping && mode != placementModeSequenceItem) { return false } segments := len(destination.segments) - if segments > maxDepth || segments > maxNodes/2 { + sequenceNodes := 0 + sequenceDepth := 0 + if mode == placementModeSequenceItem { + if segments == 0 { + return false + } + sequenceNodes = 1 + sequenceDepth = 1 + } + if segments > maxDepth-sequenceDepth || segments > (maxNodes-sequenceNodes)/2 { return false } - wrapperNodes := destinationWrapperNodeCount(destination) + wrapperNodes := destinationWrapperNodeCount(destination) + sequenceNodes for _, segment := range destination.segments { if segment.kind != pathSegmentKey { return false } } - if documentDepth(body) > maxDepth-segments { + if documentDepth(body) > maxDepth-segments-sequenceDepth { return false } return countInputNodes(body) <= maxNodes-wrapperNodes } +func destinationWrapperNodeCountForMode(destination Path, mode placementMode) int { + count := destinationWrapperNodeCount(destination) + if mode == placementModeSequenceItem { + count++ + } + return count +} + func destinationWrapperNodeCount(destination Path) int { return 2 * len(destination.segments) } diff --git a/internal/engine/destination_load_test.go b/internal/engine/destination_load_test.go index fa4eb2a..cebed17 100644 --- a/internal/engine/destination_load_test.go +++ b/internal/engine/destination_load_test.go @@ -12,6 +12,98 @@ type destinationLoadConfig struct { Value string `yaml:"value"` } +type destinationItemConfig struct { + Catalog struct { + Entries []map[string]string `yaml:"entries"` + } `yaml:"catalog"` +} + +func TestLayeredLoadPlacesCompletedMappingAsOneItem(t *testing.T) { + t.Parallel() + + loaded, err := LoadLayered[destinationItemConfig]( + Input{Source: NoSource(), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem}, + testSecretBinding(), + LayeredOptions[destinationItemConfig]{Underlays: []Underlay{{Inputs: []Input{{ + Source: Mapping("empty-item", map[string]any{}), Destination: Keys("catalog", "entries"), + placementMode: placementModeSequenceItem, + }}}}}, + ) + if err != nil { + t.Fatalf("LoadLayered: %v", err) + } + result := testLoaded(loaded) + if len(result.Config.Catalog.Entries) != 1 || result.Config.Catalog.Entries[0] == nil || len(result.Config.Catalog.Entries[0]) != 0 { + t.Fatalf("empty mapping item = %#v", result.Config.Catalog.Entries) + } + if _, ok := result.Metadata().Lookup(Keys("catalog", "entries", "0")); ok { + t.Fatal("sequence index was treated as a literal mapping key during metadata lookup") + } + sources := result.Sources() + if len(sources) != 1 || sources[0].placementMode != placementModeSequenceItem || sources[0].destinationUnresolved { + t.Fatalf("source placement evidence = %#v", sources) + } + if got := sources[0].Destination.String(); got != "/catalog/entries" { + t.Fatalf("source destination = %q", got) + } + + absent, err := LoadLayered[destinationItemConfig]( + Input{Source: NoSource(), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem}, + testSecretBinding(), + ) + if err != nil { + t.Fatalf("NoSource item load: %v", err) + } + if got := len(testLoaded(absent).Config.Catalog.Entries); got != 0 { + t.Fatalf("absent item created %d entries", got) + } +} + +func TestLayeredItemPlacementPreflightsModeAndDestination(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + mode placementMode + path Path + }{ + {name: "root item", mode: placementModeSequenceItem}, + {name: "unknown mode", mode: placementMode(255), path: Keys("catalog")}, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + reader := bytes.NewBufferString("value: unused\n") + _, err := LoadLayered[destinationItemConfig]( + Input{Source: Reader("reader", reader), Destination: test.path, placementMode: test.mode}, + testSecretBinding(), + ) + if err == nil || reader.Len() == 0 { + t.Fatalf("preflight err %v, unread bytes %d", err, reader.Len()) + } + loadError, ok := err.(*LoadError) + if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != CodeOptionsInvalid { + t.Fatalf("preflight error = %#v", err) + } + }) + } +} + +func TestLoadOptionsPlaceOneMappingItem(t *testing.T) { + t.Parallel() + + loaded, err := Load[destinationItemConfig]( + Mapping("ordinary", map[string]any{}), testSecretBinding(), + Options[destinationItemConfig]{destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem}, + ) + if err != nil { + t.Fatalf("Load: %v", err) + } + result := testLoaded(loaded) + if len(result.Config.Catalog.Entries) != 1 || result.Config.Catalog.Entries[0] == nil { + t.Fatalf("ordinary item placement = %#v", result.Config.Catalog.Entries) + } +} + func TestLayeredLoadPlacesNonImportMainAndUnderlayInputs(t *testing.T) { t.Parallel() diff --git a/internal/engine/destination_test.go b/internal/engine/destination_test.go index 1065421..b2766b9 100644 --- a/internal/engine/destination_test.go +++ b/internal/engine/destination_test.go @@ -2,6 +2,7 @@ package engine import ( "errors" + "reflect" "testing" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" @@ -39,6 +40,29 @@ func TestNormalizeDestinationUsesLiteralMappingKeys(t *testing.T) { } } +func TestMappingPlacementModeMatchesZeroValue(t *testing.T) { + t.Parallel() + + body := &document.Node{ + Kind: document.Mapping, + Mapping: []document.Entry{{ + Key: "name", Value: &document.Node{Kind: document.String, Text: "entry"}, + }}, + } + destination := Keys("catalog", "entry") + path, zero, err := placeDocument(body, destination, body.Location, 5, 32) + if err != nil { + t.Fatal(err) + } + explicitPath, explicit, err := placeDocumentWithMode(body, destination, placementModeMapping, body.Location, 5, 32) + if err != nil { + t.Fatal(err) + } + if path.String() != explicitPath.String() || !reflect.DeepEqual(zero, explicit) { + t.Fatalf("zero placement (%q, %#v) differs from mapping mode (%q, %#v)", path.String(), zero, explicitPath.String(), explicit) + } +} + func TestAppendDestinationCopiesLiteralSegments(t *testing.T) { t.Parallel() @@ -204,3 +228,60 @@ func TestPlaceDocumentRootAndBounds(t *testing.T) { t.Fatalf("absent body should not consume placement limits: node %p, error %v", absent, err) } } + +func TestPlaceDocumentSequenceItemAndMountedBounds(t *testing.T) { + t.Parallel() + + root := document.Location{SourceID: "item", Line: 1, Column: 1} + body := &document.Node{Kind: document.Mapping, Location: root} + destination := Keys("catalog", "entries") + placedPath, placed, err := placeDocumentWithMode(body, destination, placementModeSequenceItem, root, 3, 6) + if err != nil { + t.Fatal(err) + } + if placedPath.String() != "/catalog/entries" || placed.Kind != document.Mapping { + t.Fatalf("placed root = %q %#v", placedPath.String(), placed) + } + container, ok := documentAtPath(placed, destination) + if !ok || container.Kind != document.Sequence || len(container.Sequence) != 1 || container.Sequence[0] != body { + t.Fatalf("item destination = %#v, present %v", container, ok) + } + if got, want := countInputNodes(placed), countInputNodes(body)+destinationWrapperNodeCount(destination)+1; got != want { + t.Fatalf("item node count = %d, want %d", got, want) + } + if container.Location != root || container.Sequence[0].Location != root { + t.Fatal("synthetic sequence or authored mapping lost source coordinates") + } + + if _, _, err := placeDocumentWithMode(body, Path{}, placementModeSequenceItem, root, 3, 6); !errors.Is(err, errInvalidDestination) { + t.Fatalf("root item placement error = %v, want invalid destination", err) + } + if _, _, err := placeDocumentWithMode(&document.Node{Kind: document.String}, destination, placementModeSequenceItem, root, 3, 6); !errors.Is(err, errInvalidDestination) { + t.Fatalf("non-mapping item placement error = %v, want invalid destination", err) + } + if _, _, err := placeDocumentWithMode(body, destination, placementMode(255), root, 3, 6); !errors.Is(err, errInvalidDestination) { + t.Fatalf("unknown placement mode error = %v, want invalid destination", err) + } + + for _, test := range []struct { + name string + depth int + nodes int + wantAccept bool + }{ + {name: "exact allowance", depth: 3, nodes: 6, wantAccept: true}, + {name: "one too shallow", depth: 2, nodes: 6}, + {name: "one node short", depth: 3, nodes: 5}, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + _, _, err := placeDocumentWithMode(body, destination, placementModeSequenceItem, root, test.depth, test.nodes) + if test.wantAccept && err != nil { + t.Fatalf("exact bounds rejected: %v", err) + } + if !test.wantAccept && !errors.Is(err, errDestinationLimit) { + t.Fatalf("over-bound placement error = %v", err) + } + }) + } +} diff --git a/internal/engine/layered.go b/internal/engine/layered.go index 05d4e63..8b0d732 100644 --- a/internal/engine/layered.go +++ b/internal/engine/layered.go @@ -4,10 +4,11 @@ import "os" // Input selects one source, its destination, and its independent import policy. type Input struct { - Source Source - Imports bool - Filesystem *FilesystemContext - Destination Path + Source Source + Imports bool + Filesystem *FilesystemContext + Destination Path + placementMode placementMode } // Underlay contains strictly composed contributions at one precedence tier. @@ -36,7 +37,7 @@ func LoadLayered[T any](main Input, secrets SecretBinding, supplied ...LayeredOp request := loadRequest[T]{ main: loadInput{ source: main.Source, imports: main.Imports, filesystem: main.Filesystem, - destination: main.Destination.clone(), + destination: main.Destination.clone(), placementMode: main.placementMode, }, definition: options.Definition, sourceVersionMode: options.SourceVersionMode, overlay: options.Overlay, @@ -49,7 +50,7 @@ func LoadLayered[T any](main Input, secrets SecretBinding, supplied ...LayeredOp for inputIndex, input := range underlay.Inputs { request.underlays[index].inputs[inputIndex] = loadInput{ source: input.Source, imports: input.Imports, filesystem: input.Filesystem, - destination: input.Destination.clone(), + destination: input.Destination.clone(), placementMode: input.placementMode, } } } diff --git a/internal/engine/load.go b/internal/engine/load.go index 0bdbd30..489283a 100644 --- a/internal/engine/load.go +++ b/internal/engine/load.go @@ -30,6 +30,8 @@ type Options[T any] struct { Filesystem *FilesystemContext Overrides []Override SourceVersionMode sourceVersionMode + destination Path + placementMode placementMode } // FilesystemContext supplies filesystem roots for imports and overlays. @@ -147,7 +149,7 @@ func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup se definition := options.Definition definition.sequenceRegistrations = nil request := loadRequest[T]{ - main: loadInput{source: source, imports: options.Imports, filesystem: options.Filesystem}, + main: loadInput{source: source, imports: options.Imports, filesystem: options.Filesystem, destination: options.destination.clone(), placementMode: options.placementMode}, definition: definition, sequencePolicy: policy, overlay: options.Overlay, overrides: options.Overrides, ordinary: true, sourceVersionMode: options.SourceVersionMode, @@ -159,7 +161,7 @@ func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup se func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lookup secretLookup) (*Loaded[T], error) { accumulator := diagnosticAccumulator{} accounting := &importAccounting{} - mainOptions := Options[T]{Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem} + mainOptions := Options[T]{Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem, destination: request.main.destination.clone(), placementMode: request.main.placementMode} mainOptions.SourceVersionMode = request.sourceVersionMode mainOptions.Definition.Version = request.protectionVersion() mainOptions.Overrides = request.overrides diff --git a/internal/engine/load_input.go b/internal/engine/load_input.go index fc818d2..8593b40 100644 --- a/internal/engine/load_input.go +++ b/internal/engine/load_input.go @@ -82,6 +82,7 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d parsed, diagnostics := parseSource(source, "source-"+strconv.Itoa(accounting.sources+1), parseLimits) if parsed.source != nil { parsed.source.Destination = destination.clone() + parsed.source.placementMode = options.placementMode } input := assembledInput{parsed: parsed, document: parsed.document, sources: sourceInfos(parsed.source), versionState: versionState} if len(diagnostics) != 0 { @@ -136,7 +137,7 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d } } if !options.Imports && input.document != nil { - _, placed, err := placeDocument(input.document, destination, input.document.Location, parseLimits.document.Depth, parseLimits.document.Nodes) + _, placed, err := placeDocumentWithMode(input.document, destination, options.placementMode, input.document.Location, parseLimits.document.Depth, parseLimits.document.Nodes) if err != nil { return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the source depth or node limit after destination placement")} } @@ -150,7 +151,7 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d }}, }} rawNodes := countInputNodes(parsed.document) - placementNodes := destinationWrapperNodeCount(destination) + placementNodes := destinationWrapperNodeCountForMode(destination, options.placementMode) if accounting.sources+1 > limits.sources || accounting.bytes+parsed.bytes > limits.bytes || accounting.nodes+rawNodes+placementNodes > limits.nodes { return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the aggregate input limit")} } diff --git a/internal/engine/load_request.go b/internal/engine/load_request.go index d841550..b4fac8c 100644 --- a/internal/engine/load_request.go +++ b/internal/engine/load_request.go @@ -27,10 +27,11 @@ func (request loadRequest[T]) protectionVersion() *VersionRule { } type loadInput struct { - source Source - imports bool - filesystem *FilesystemContext - destination Path + source Source + imports bool + filesystem *FilesystemContext + destination Path + placementMode placementMode } type loadGroup struct{ inputs []loadInput } @@ -42,26 +43,18 @@ func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBi request.sequencePolicy = policy diagnostics = append(diagnostics, policyDiagnostics...) request.definition.sequenceRegistrations = nil - if normalized, err := normalizeDestination(request.main.destination); err != nil { - diagnostics = append(diagnostics, invalidDestinationDiagnostic(request.main.destination)) - } else { - request.main.destination = normalized - } - request.effectiveVersion = effectiveSourceVersionRule(request.definition.Version, request.main.destination, request.sourceVersionMode) - _, mainDiagnostics := selectOptions(request.main.source, []Options[T]{{ - Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem, - SourceVersionMode: request.sourceVersionMode, + mainOptions, mainDiagnostics := selectOptions(request.main.source, []Options[T]{{ + Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem, destination: request.main.destination.clone(), + SourceVersionMode: request.sourceVersionMode, placementMode: request.main.placementMode, }}) + request.main.destination = mainOptions.destination diagnostics = append(diagnostics, mainDiagnostics...) + request.effectiveVersion = effectiveSourceVersionRule(request.definition.Version, request.main.destination, request.sourceVersionMode) for groupIndex := range request.underlays { for inputIndex := range request.underlays[groupIndex].inputs { input := &request.underlays[groupIndex].inputs[inputIndex] - if normalized, err := normalizeDestination(input.destination); err != nil { - diagnostics = append(diagnostics, invalidDestinationDiagnostic(input.destination)) - } else { - input.destination = normalized - } - _, inputDiagnostics := selectOptions(input.source, []Options[T]{{Imports: input.imports, Filesystem: input.filesystem}}) + inputOptions, inputDiagnostics := selectOptions(input.source, []Options[T]{{Imports: input.imports, Filesystem: input.filesystem, destination: input.destination.clone(), placementMode: input.placementMode}}) + input.destination = inputOptions.destination diagnostics = append(diagnostics, inputDiagnostics...) } } @@ -165,7 +158,11 @@ func acquireUnderlayGroupWithVersion(group loadGroup, index int, rule *VersionRu var sources []SourceInfo var contexts map[string]ResourceContext for _, contribution := range group.inputs { - options := Options[struct{}]{Imports: contribution.imports, Filesystem: contribution.filesystem, Definition: Definition[struct{}]{Version: rule}} + options := Options[struct{}]{ + Imports: contribution.imports, Filesystem: contribution.filesystem, + Definition: Definition[struct{}]{Version: rule}, destination: contribution.destination.clone(), + placementMode: contribution.placementMode, + } acquired, diagnostics := assembleBaseInputTrackedForRole(contribution.source, options, contribution.destination, accounting, visited, hostToken, versionState, sourceVersionContribution) for _, source := range acquired.sources { source.Layer = LayerUnderlay diff --git a/internal/engine/options.go b/internal/engine/options.go index 591e8c7..651fa64 100644 --- a/internal/engine/options.go +++ b/internal/engine/options.go @@ -9,6 +9,17 @@ func selectOptions[T any](source Source, supplied []Options[T]) (Options[T], []D options = supplied[0] } var diagnostics []Diagnostic + if options.placementMode != placementModeMapping && options.placementMode != placementModeSequenceItem { + diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "input placement mode is invalid")) + } + if normalized, err := normalizeDestination(options.destination); err != nil { + diagnostics = append(diagnostics, invalidDestinationDiagnostic(options.destination)) + } else { + options.destination = normalized + } + if options.placementMode == placementModeSequenceItem && len(options.destination.segments) == 0 { + diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "sequence-item placement requires a nonempty destination")) + } definition := options.Definition if !validOverlayName(options.Overlay) { diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "overlay name must contain only ASCII letters, digits, underscore, or hyphen")) diff --git a/internal/engine/records.go b/internal/engine/records.go index e4ccca0..9c0b45e 100644 --- a/internal/engine/records.go +++ b/internal/engine/records.go @@ -183,16 +183,18 @@ type Location struct { // namespace-relative filesystem path; synthetic sources have no Path. // ImportedFrom identifies the declaration that included this source. type SourceInfo struct { - ID string - Label string - Path string - Destination Path - Layer Layer - Kind SourceKind - UnderlayIndex int - ImportedFrom *Location - VersionCheck *SourceVersionCheck - namespace *namespaceToken + ID string + Label string + Path string + Destination Path + Layer Layer + Kind SourceKind + UnderlayIndex int + ImportedFrom *Location + VersionCheck *SourceVersionCheck + namespace *namespaceToken + placementMode placementMode + destinationUnresolved bool } // SourceVersionCheck is a detached, public-safe record of a source-local