From c19825620c60d6d875d61211b0882d5192ffcf5a Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 16 Oct 2026 19:11:08 +0000 Subject: [PATCH] Project version protections through item placement --- docs/roadmap/implementation.md | 18 ++- .../engine/destination_integration_test.go | 37 ++++++ internal/engine/directives.go | 48 ++++++-- internal/engine/imports.go | 66 +++++++---- internal/engine/load.go | 2 +- internal/engine/load_input.go | 2 +- internal/engine/load_request.go | 20 ++-- internal/engine/selected_overlay.go | 2 +- internal/engine/source_version.go | 20 +++- internal/engine/source_version_test.go | 111 ++++++++++++++++++ 10 files changed, 276 insertions(+), 50 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index d11654c..1baf6a8 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–9 completed. +Status: implementation in progress; stages 1–10 completed. ## Purpose and authority @@ -595,6 +595,22 @@ Validation: source-version/version/inheritance/override/resolution tests, `go test -race ./internal/engine`, and `./scripts/check.sh`. Completion: item placement changes coordinates, not version or structural policy. +Completion evidence: enabled source-version rules now derive their effective +paths through the main item's assigned index, while default-mode coordinates +remain unchanged. Structural import and underlay checks use assigned sequence +ranges so they protect the actual main item without rejecting unrelated peers; +source-local checking still runs once per original source, and projected header +evidence is used for sensitivity. Tests cover ordinary and layered placement, +protected versus peer indexes, nested projections, and redacted version +evidence. These checks passed with Go `go1.26.9 linux/amd64`: + +- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'SourceVersion|Version|Inheritance|Override|Resolution' -count=1` +- `GOTOOLCHAIN=local GOPROXY=off go test -race ./internal/engine` +- `GOTOOLCHAIN=local ./scripts/check.sh` +- `git diff --check` + +No environmental limitation affected validation; checks ran on Linux/amd64. + ## Stage 11 — Expose root-owned controls and detached placement evidence Scope: root facade declarations, conversions, and public integration tests. diff --git a/internal/engine/destination_integration_test.go b/internal/engine/destination_integration_test.go index e59a279..b1c5f11 100644 --- a/internal/engine/destination_integration_test.go +++ b/internal/engine/destination_integration_test.go @@ -193,6 +193,43 @@ func TestLayeredMountedVersionProtectionCoversInheritanceAndUpperTiers(t *testin }) } +func TestLayeredItemVersionProtectionTracksAssignedPeerIndexes(t *testing.T) { + t.Parallel() + + definition := Definition[map[string]any]{ + Version: &VersionRule{Path: Keys("schema", "version"), Check: func(Version, *Validation) error { return nil }}, + sequenceRegistrations: []sequenceRegistration{{path: Keys("catalog", "entries"), identityField: "id"}}, + } + item := func(label string, value map[string]any) Input { + return Input{Source: Mapping(label, value), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem} + } + options := func(inputs ...Input) LayeredOptions[map[string]any] { + return LayeredOptions[map[string]any]{Definition: definition, SourceVersionMode: sourceVersionModeIfPresent, Underlays: []Underlay{{Inputs: inputs}}} + } + main := item("main", map[string]any{"id": "main", "schema": map[string]any{"version": "v1"}}) + + t.Run("same assigned main path is protected", func(t *testing.T) { + _, err := LoadLayered(main, testSecretBinding(), options(item("underlay", map[string]any{"id": "underlay", "schema": nil}))) + if got := groupedErrorCode(t, err); got != CodeUnderlayVersion { + t.Fatalf("protected item claim code = %s, want %s", got, CodeUnderlayVersion) + } + }) + + t.Run("appended peer does not claim main path", func(t *testing.T) { + loaded, err := LoadLayered(main, testSecretBinding(), options( + item("first", map[string]any{"id": "first", "schema": map[string]any{"other": "value"}}), + item("peer", map[string]any{"id": "peer", "schema": nil}), + )) + if err != nil { + t.Fatalf("unrelated appended peer rejected: %v", err) + } + entries := layeredMapValue(t, testLoaded(loaded).Config, "catalog", "entries").([]any) + if len(entries) != 1 || entries[0].(map[string]any)["id"] != "main" { + t.Fatalf("main item projection = %#v", entries) + } + }) +} + type destinationDecodeProbe string var destinationDecodeProbeValues []string diff --git a/internal/engine/directives.go b/internal/engine/directives.go index f4a6724..206c337 100644 --- a/internal/engine/directives.go +++ b/internal/engine/directives.go @@ -163,26 +163,54 @@ func nestedDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic } func fragmentVersionClaim(root *document.Node, rule *VersionRule) *document.Node { + return fragmentVersionClaimProjected(root, rule, -1, nil) +} + +func fragmentVersionClaimProjected(root *document.Node, rule *VersionRule, sourceIndex int, ranges []sequenceRange) *document.Node { if rule == nil { return nil } current := root + currentPath := Path{} if len(rule.Path.segments) == 0 { return current } for _, segment := range rule.Path.segments { - if current.Kind != document.Mapping { + switch current.Kind { + case document.Mapping: + if segment.kind == pathSegmentIndex { + return current + } + index := documentEntryIndex(current.Mapping, segment.key) + if index < 0 { + return nil + } + current = current.Mapping[index].Value + currentPath = currentPath.Key(segment.key) + case document.Sequence: + if segment.kind != pathSegmentIndex { + return current + } + index := segment.index + for _, assigned := range ranges { + if assigned.sourceIndex != sourceIndex || assigned.path.String() != currentPath.String() { + continue + } + if index < assigned.start || index >= assigned.end { + return nil + } + index -= assigned.start + break + } + if index < 0 || index >= len(current.Sequence) { + return nil + } + current = current.Sequence[index] + currentPath = currentPath.Index(index) + default: + // A scalar or null at an ancestor replaces the protected subtree. return current } - if segment.kind == pathSegmentIndex { - return nil - } - key := segment.key - index := documentEntryIndex(current.Mapping, key) - if index < 0 { - return nil - } - current = current.Mapping[index].Value } return current } diff --git a/internal/engine/imports.go b/internal/engine/imports.go index 3009e78..b3c38b0 100644 --- a/internal/engine/imports.go +++ b/internal/engine/imports.go @@ -230,6 +230,11 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess, if len(failures) != 0 { return nil, cloneSourceInfos(state.sources), prependDiagnosticPaths(failures, destination.Index(0)) } + if versionState != nil { + if diagnostic := importedVersionClaim(documents, completed, versionState.localRule, 0); diagnostic != nil { + return nil, cloneSourceInfos(state.sources), []Diagnostic{*diagnostic} + } + } completed.acquisition = []acquisitionScope{cloneAcquisitionScope(localScope)} projectNestedEvidence(completed, localScope) _, placed, err := placeDocumentWithMode(completed.root, destination, placementModeSequenceItem, body.Location, maxDocumentDepth, maxDocumentNodes) @@ -260,6 +265,11 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess, documents = append(documents, contribution.document) } composed, diagnostics := composeDocumentsWithPolicy(policy, documents...) + if composed != nil { + if diagnostic := importedVersionClaim(documents, composed, rule, 0); diagnostic != nil { + return nil, cloneSourceInfos(state.sources), []Diagnostic{*diagnostic} + } + } if composed != nil { projectNestedEvidence(composed, state.scope) projectAcquiredDestinations(composed, state.scope, state.sources) @@ -416,7 +426,11 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati var diagnostics []Diagnostic var processing *sourceVersionProcessingResult if s.versionState != nil && s.versionState.mode != sourceVersionModeDefault { - processed, failures := s.versionState.process(root, sourceVersionContribution, destination, sourceID) + versionDestination := destination + if s.itemScope { + versionDestination = appendDestination(s.itemPrefix.Index(0), destination) + } + processed, failures := s.versionState.process(root, sourceVersionContribution, versionDestination, sourceID, declaration.placement) if len(failures) != 0 { return failures } @@ -452,18 +466,6 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)} } s.accounting.nodes += placementNodes - protectionRule := s.rule - if s.versionState != nil && s.versionState.effectiveRule != nil { - protectionRule = s.versionState.effectiveRule - } - if versionNode := fragmentVersionClaim(placed, protectionRule); versionNode != nil { - versionLocation := publicLocation(versionNode.Location) - return []Diagnostic{{ - Code: CodeImportVersion, Severity: SeverityError, Path: protectionRule.Path.clone(), - Message: "imported fragments cannot contribute to the schema-version path", Primary: &versionLocation, - }} - } - route := importRoute{canonical: canonical, locations: currentRoute.locations} s.scope.contributions = append(s.scope.contributions, acquiredContribution{ source: cloneSourceInfo(info), payload: body, document: placed, @@ -530,17 +532,6 @@ func (s *importTraversal) visitItemSource(canonical string, declaration importDe return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &declaration.location)} } s.accounting.nodes += placementNodes - protectionRule := s.rule - if s.versionState != nil && s.versionState.effectiveRule != nil { - protectionRule = s.versionState.effectiveRule - } - if versionNode := fragmentVersionClaim(placed, protectionRule); versionNode != nil { - versionLocation := publicLocation(versionNode.Location) - return []Diagnostic{{ - Code: CodeImportVersion, Severity: SeverityError, Path: protectionRule.Path.clone(), - Message: "imported fragments cannot contribute to the schema-version path", Primary: &versionLocation, - }} - } s.scope.contributions = append(s.scope.contributions, acquiredContribution{ source: cloneSourceInfo(info), payload: completed.root, document: placed, destination: pl.clone(), relativeDestination: relativeDestination.clone(), processing: processing, @@ -549,6 +540,25 @@ func (s *importTraversal) visitItemSource(canonical string, declaration importDe return nil } +func importedVersionClaim(documents []*document.Node, composed *composedDocument, rule *VersionRule, mainIndex int) *Diagnostic { + if composed == nil || rule == nil { + return nil + } + for index, contribution := range documents { + if index == mainIndex { + continue + } + if node := fragmentVersionClaimProjected(contribution, rule, index, composed.sequenceRanges); node != nil { + location := publicLocation(node.Location) + return &Diagnostic{ + Code: CodeImportVersion, Severity: SeverityError, Path: rule.Path.clone(), + Message: "imported fragments cannot contribute to the schema-version path", Primary: &location, + } + } + } + return nil +} + func prependDiagnosticPaths(diagnostics []Diagnostic, prefix Path) []Diagnostic { cloned := append([]Diagnostic(nil), diagnostics...) for index := range cloned { @@ -647,6 +657,7 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc } contributionPrefix = appendDestination(container, Path{}.Index(start)) contributionInsideItem = true + projectSourceVersionEvidence(contribution.processing, contributionPrefix) } else if contributionInsideItem { exactDestination := appendDestination(contributionPrefix, contribution.destination) contribution.source.Destination = exactDestination.clone() @@ -656,6 +667,7 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc sources[position].Destination = exactDestination sources[position].destinationUnresolved = false } + projectSourceVersionEvidence(contribution.processing, exactDestination) } if contribution.nested != nil && contribution.nested.acquisition != nil { for _, nestedScope := range contribution.nested.acquisition { @@ -667,6 +679,12 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc projectScope(composed, scope, Path{}, false, "") } +func projectSourceVersionEvidence(processing *sourceVersionProcessingResult, destination Path) { + if processing != nil && processing.evidence != nil { + processing.evidence.destination = destination.clone() + } +} + func (s *importTraversal) readImport(name, sourceID string, optional bool) ([]byte, bool, *Diagnostic) { remaining := s.limits.bytes - s.accounting.bytes limit := min(int64(maxSourceBytes), remaining) diff --git a/internal/engine/load.go b/internal/engine/load.go index 450a3fc..937542f 100644 --- a/internal/engine/load.go +++ b/internal/engine/load.go @@ -154,7 +154,7 @@ func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup se definition: definition, sequencePolicy: policy, overlay: options.Overlay, overrides: options.Overrides, ordinary: true, sourceVersionMode: options.SourceVersionMode, - effectiveVersion: effectiveSourceVersionRule(options.Definition.Version, Path{}, options.SourceVersionMode), + effectiveVersion: effectiveSourceVersionRuleForPlacement(options.Definition.Version, options.destination, options.SourceVersionMode, options.placementMode), } return executeLoadRequest(request, secrets, lookup) } diff --git a/internal/engine/load_input.go b/internal/engine/load_input.go index b0b48da..557fcdf 100644 --- a/internal/engine/load_input.go +++ b/internal/engine/load_input.go @@ -111,7 +111,7 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d if parsed.source != nil { sourceID = parsed.source.ID } - processed, failures := versionState.process(parsed.document, role, destination, sourceID) + processed, failures := versionState.process(parsed.document, role, destination, sourceID, options.placementMode) if len(failures) != 0 { return input, failures } diff --git a/internal/engine/load_request.go b/internal/engine/load_request.go index 4382c8e..5ceaf4a 100644 --- a/internal/engine/load_request.go +++ b/internal/engine/load_request.go @@ -49,7 +49,7 @@ func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBi }}) request.main.destination = mainOptions.destination diagnostics = append(diagnostics, mainDiagnostics...) - request.effectiveVersion = effectiveSourceVersionRule(request.definition.Version, request.main.destination, request.sourceVersionMode) + request.effectiveVersion = effectiveSourceVersionRuleForPlacement(request.definition.Version, request.main.destination, request.sourceVersionMode, request.main.placementMode) for groupIndex := range request.underlays { for inputIndex := range request.underlays[groupIndex].inputs { input := &request.underlays[groupIndex].inputs[inputIndex] @@ -180,13 +180,6 @@ func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *Ver if acquired.document == nil { continue } - if node := fragmentVersionClaim(acquired.document, rule); node != nil { - location := publicLocation(node.Location) - return nil, sources, contexts, []Diagnostic{{ - Code: CodeUnderlayVersion, Severity: SeverityError, Path: rule.Path.clone(), - Message: "underlay inputs cannot contribute to the schema-version path", Primary: &location, - }} - } scopes = append(scopes, cloneAcquisitionScopes(acquired.acquisition)...) } var documents []*document.Node @@ -202,6 +195,17 @@ func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *Ver if len(diagnostics) != 0 { return nil, sources, contexts, diagnostics } + if composition != nil && rule != nil { + for sourceIndex, document := range documents { + if node := fragmentVersionClaimProjected(document, rule, sourceIndex, composition.sequenceRanges); node != nil { + location := publicLocation(node.Location) + return nil, sources, contexts, []Diagnostic{{ + Code: CodeUnderlayVersion, Severity: SeverityError, Path: rule.Path.clone(), + Message: "underlay inputs cannot contribute to the schema-version path", Primary: &location, + }} + } + } + } composition.inputBytes = accounting.bytes - initialBytes composition.inputNodes = accounting.nodes - initialNodes composition.acquisition = cloneAcquisitionScopes(scopes) diff --git a/internal/engine/selected_overlay.go b/internal/engine/selected_overlay.go index 33dd28e..3d7c616 100644 --- a/internal/engine/selected_overlay.go +++ b/internal/engine/selected_overlay.go @@ -136,7 +136,7 @@ func loadSelectedOverlayWithAccessAndVersion(access *filesystemAccess, base *ass } overlayPayload := overlayRoot if versionState != nil && versionState.mode != sourceVersionModeDefault { - processed, failures := versionState.process(overlayRoot, sourceVersionContribution, Path{}, sourceID) + processed, failures := versionState.process(overlayRoot, sourceVersionContribution, Path{}, sourceID, placementModeMapping) if len(failures) != 0 { return sources, failures } diff --git a/internal/engine/source_version.go b/internal/engine/source_version.go index 5ecaa2a..a4d2e15 100644 --- a/internal/engine/source_version.go +++ b/internal/engine/source_version.go @@ -91,11 +91,15 @@ func sourceVersionCheckForEvidence(evidence *sourceVersionEvidence, sensitivity } } -func (state *sourceVersionState) process(root *document.Node, role sourceVersionRole, destination Path, sourceID string) (sourceVersionProcessingResult, []Diagnostic) { +func (state *sourceVersionState) process(root *document.Node, role sourceVersionRole, destination Path, sourceID string, placement placementMode) (sourceVersionProcessingResult, []Diagnostic) { if state == nil || state.mode == sourceVersionModeDefault { return sourceVersionProcessingResult{}, nil } - result := processSourceVersion(root, state.localRule, role, destination, state.mode) + projectedDestination := destination.clone() + if placement == placementModeSequenceItem { + projectedDestination = projectedDestination.Index(0) + } + result := processSourceVersion(root, state.localRule, role, projectedDestination, state.mode) if result.evidence != nil { result.evidence.sourceID = sourceID state.evidence = append(state.evidence, result.evidence) @@ -119,6 +123,10 @@ type sourceVersionProcessingResult struct { } func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourceVersionMode) *VersionRule { + return effectiveSourceVersionRuleForPlacement(local, destination, mode, placementModeMapping) +} + +func effectiveSourceVersionRuleForPlacement(local *VersionRule, destination Path, mode sourceVersionMode, placement placementMode) *VersionRule { if local == nil { return nil } @@ -126,9 +134,13 @@ func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourc return local } effective := *local + prefix := destination.clone() + if placement == placementModeSequenceItem { + prefix = prefix.Index(0) + } effective.Path = Path{ - segments: append(clonePathSegments(destination.segments, len(local.Path.segments)), local.Path.segments...), - invalid: destination.invalid || local.Path.invalid, + segments: append(clonePathSegments(prefix.segments, len(local.Path.segments)), local.Path.segments...), + invalid: prefix.invalid || local.Path.invalid, } return &effective } diff --git a/internal/engine/source_version_test.go b/internal/engine/source_version_test.go index 1430bb3..b1fd252 100644 --- a/internal/engine/source_version_test.go +++ b/internal/engine/source_version_test.go @@ -37,6 +37,40 @@ func TestSourceVersionAbsentPathKeepsUnresolvedSegmentIntent(t *testing.T) { } } +func TestFragmentVersionClaimUsesAssignedSequenceProjection(t *testing.T) { + t.Parallel() + + fragment := sourceVersionTestDocument(t, "items: [{version: v1}]\n") + rangeForPeer := []sequenceRange{{sourceIndex: 1, path: Keys("items"), start: 1, end: 2}} + if node := fragmentVersionClaimProjected(fragment, &VersionRule{Path: Keys("items").Index(0).Key("version")}, 1, rangeForPeer); node != nil { + t.Fatalf("peer append claimed main index 0: %#v", node) + } + if node := fragmentVersionClaimProjected(fragment, &VersionRule{Path: Keys("items").Index(1).Key("version")}, 1, rangeForPeer); node == nil || node.Text != "v1" { + t.Fatalf("assigned peer index 1 claim = %#v, want its version node", node) + } + nested := sourceVersionTestDocument(t, "items: [{nested: [{version: nested-v}]}]\n") + nestedRanges := []sequenceRange{ + {sourceIndex: 1, path: Keys("items"), start: 1, end: 2}, + {sourceIndex: 1, path: Keys("items").Index(0).Key("nested"), start: 2, end: 3}, + } + if node := fragmentVersionClaimProjected(nested, &VersionRule{Path: Keys("items").Index(1).Key("nested").Index(2).Key("version")}, 1, nestedRanges); node == nil || node.Text != "nested-v" { + t.Fatalf("nested assigned claim = %#v, want nested version", node) + } + ancestor := sourceVersionTestDocument(t, "items: {version: replacement}\n") + if node := fragmentVersionClaim(ancestor, &VersionRule{Path: Keys("items").Index(0).Key("version")}); node == nil || node.Kind != document.Mapping { + t.Fatalf("destructive sequence ancestor claim = %#v, want mapping node", node) + } + + effective := effectiveSourceVersionRuleForPlacement(&VersionRule{Path: Keys("version")}, Keys("catalog", "entries"), sourceVersionModeRequired, placementModeSequenceItem) + if got, want := effective.Path.String(), "/catalog/entries/0/version"; got != want { + t.Fatalf("item effective version path = %q, want %q", got, want) + } + defaultRule := effectiveSourceVersionRuleForPlacement(&VersionRule{Path: Keys("version")}, Keys("catalog", "entries"), sourceVersionModeDefault, placementModeSequenceItem) + if got, want := defaultRule.Path.String(), "/version"; got != want { + t.Fatalf("default effective version path = %q, want %q", got, want) + } +} + type sourceVersionMountedService struct { Version string `yaml:"version"` } @@ -473,6 +507,83 @@ func TestSourceVersionLoadChecksMountedMainLocallyAndProtectsEffectivePath(t *te } } +func TestSourceVersionItemPlacementKeepsLocalChecksAndProtectsProjectedPath(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + load func(Definition[map[string]any]) error + }{ + { + name: "ordinary", + load: func(definition Definition[map[string]any]) error { + _, err := Load(Mapping("main", map[string]any{"version": "v1", "name": "ordinary"}), testSecretBinding(), Options[map[string]any]{ + Definition: definition, SourceVersionMode: sourceVersionModeRequired, + destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem, + Overrides: []Override{{Path: Keys("catalog", "entries"), Value: []any{}, Flag: "--entries"}}, + }) + return err + }, + }, + { + name: "layered", + load: func(definition Definition[map[string]any]) error { + _, err := LoadLayered(Input{Source: Mapping("main", map[string]any{"version": "v1", "name": "layered"}), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem}, testSecretBinding(), LayeredOptions[map[string]any]{ + Definition: definition, SourceVersionMode: sourceVersionModeRequired, + Overrides: []Override{{Path: Keys("catalog", "entries"), Value: []any{}, Flag: "--entries"}}, + }) + return err + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + checks := 0 + definition := Definition[map[string]any]{Version: &VersionRule{ + Path: Keys("version"), + Check: func(version Version, _ *Validation) error { + checks++ + if !version.Present || version.Text != "v1" { + t.Errorf("source-local version = %#v, want v1", version) + } + return nil + }, + }} + err := test.load(definition) + var loadErr *LoadError + if !errors.As(err, &loadErr) { + t.Fatalf("item placement override error = %T %v, want *LoadError", err, err) + } + requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeOverrideVersion, "/catalog/entries") + if checks != 1 { + t.Fatalf("source-version checker called %d times, want once", checks) + } + }) + } +} + +func TestSourceVersionItemEvidenceUsesProjectedSensitivityPath(t *testing.T) { + t.Parallel() + + const canary = "PROJECTED-VERSION-EVIDENCE-CANARY" + secretPath := Keys("catalog", "entries").Index(0).Key("version") + loaded, err := Load(Mapping("main", map[string]any{"version": canary}), testSecretBinding(), Options[map[string]any]{ + Definition: Definition[map[string]any]{ + Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }}, + Secret: func(path Path) bool { return path.String() == secretPath.String() }, + }, + SourceVersionMode: sourceVersionModeRequired, + destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem, + }) + if err != nil { + t.Fatalf("item source-version load: %v", err) + } + sources := loaded.Inspection.Sources() + if len(sources) != 1 || sources[0].VersionCheck == nil || !sources[0].VersionCheck.Redacted || sources[0].VersionCheck.Version.Text != "" { + t.Fatalf("projected source-version evidence was not redacted: %#v", sources) + } +} + func TestSourceVersionLoadChecksNullAndNoSourceMain(t *testing.T) { t.Parallel()