From dfeaf9032089863f1acfca25421ee49d85ab58f3 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 23 Oct 2026 18:55:03 +0000 Subject: [PATCH] Integrate policy-aware item scopes --- docs/roadmap/implementation.md | 6 +- internal/engine/collection_policy_state.go | 20 ++ internal/engine/compose.go | 9 +- internal/engine/imports.go | 79 +++++-- internal/engine/item_policy_test.go | 229 +++++++++++++++++++++ internal/engine/load_input.go | 28 +-- internal/engine/source_version.go | 12 +- 7 files changed, 353 insertions(+), 30 deletions(-) create mode 100644 internal/engine/item_policy_test.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index a5256bd..b6b48db 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Sequence Identity And Combination Implementation Plan -Status: implementation in progress; Stages 1–11 complete. +Status: implementation in progress; Stages 1–12 complete. Audience: a Luna coding agent implementing one numbered stage per prompt, in order. This plan starts at commit @@ -466,6 +466,10 @@ scope boundaries from Stages 9–10. Validation: item/destination/import/composition/source-version tests and the everyday check. Keep current public behavior until load-path activation. +**Completion:** Implemented and verified with focused item-scope, destination, +import, composition, collection-policy, and source-version tests plus +`./scripts/check.sh`. + ## Stage 13 — Activate Ordinary Source Collection Declarations Scope: ordinary `Load` base-source assembly, excluding activation of the new diff --git a/internal/engine/collection_policy_state.go b/internal/engine/collection_policy_state.go index fde8b47..e8ff9e6 100644 --- a/internal/engine/collection_policy_state.go +++ b/internal/engine/collection_policy_state.go @@ -59,6 +59,26 @@ func (policy *collectionPolicy) setRecordPolicy(record collectionRecordPolicy) { policy.recordPolicies[record.owner] = cloneCollectionPolicyScope(record.scope) } +func (policy *collectionPolicy) setRecordPolicyAt(index int, record collectionRecordPolicy) { + if index < 0 { + return + } + if record.owner == nil { + record.owner = newCollectionRecordPolicy().owner + } + if policy.recordPolicies == nil { + policy.recordPolicies = make(map[*collectionRecordOwner]collectionPolicyScope) + } + for len(policy.recordOrder) <= index { + owner := newCollectionRecordPolicy().owner + policy.recordOrder = append(policy.recordOrder, owner) + policy.recordPolicies[owner] = collectionPolicyScope{} + } + delete(policy.recordPolicies, policy.recordOrder[index]) + policy.recordOrder[index] = record.owner + policy.recordPolicies[record.owner] = cloneCollectionPolicyScope(record.scope) +} + func (policy collectionPolicy) recordPolicyAt(index int) (collectionRecordPolicy, bool) { if index < 0 || index >= len(policy.recordOrder) { return collectionRecordPolicy{}, false diff --git a/internal/engine/compose.go b/internal/engine/compose.go index fd42233..c3e5b66 100644 --- a/internal/engine/compose.go +++ b/internal/engine/compose.go @@ -316,13 +316,14 @@ func resolveStrictCollectionPolicies( } identityFieldsByPath := make(map[string][]string) - collectionPolicies := cloneCollectionPolicyScope(lower) + effectiveLower := cloneCollectionPolicyScope(lower) + collectionPolicies := cloneCollectionPolicyScope(effectiveLower) var diagnostics []Diagnostic for _, path := range paths { key := path.String() currentDeclarations := declarationsByPath[key] - lowerEntry, hasLower := lower.collections[key] + lowerEntry, hasLower := effectiveLower.collections[key] var lowerPolicy *collectionPolicy if hasLower { copy := cloneCollectionPolicy(lowerEntry.policy) @@ -346,6 +347,10 @@ func resolveStrictCollectionPolicies( lowerPolicy.identityFields = nil lowerPolicy.identityPresent = false lowerPolicy.identityLocation = nil + lowerPolicy.recordOrder = nil + lowerPolicy.recordPolicies = nil + effectiveLower.retireCollectionPoliciesAtOrBelow(path) + collectionPolicies.retireCollectionPoliciesAtOrBelow(path) } resolution, found := resolveCollectionPolicy(path, currentDeclarations, applicationIdentity, lowerPolicy) diff --git a/internal/engine/imports.go b/internal/engine/imports.go index 738430d..0e351c1 100644 --- a/internal/engine/imports.go +++ b/internal/engine/imports.go @@ -201,10 +201,12 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem state.stack = append(state.stack, mainRoute) var body *document.Node var declarations []importDeclarationSpec + var sequenceDeclarations []sequenceDeclarationSpec var diagnostics []Diagnostic if processing != nil { body = processing.payload declarations = processing.imports + sequenceDeclarations = cloneSequenceDeclarations(processing.sequences) } else { body, declarations, diagnostics = splitImportDirectives(main.document) } @@ -213,6 +215,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem state.scope.contributions = append(state.scope.contributions, acquiredContribution{ source: cloneSourceInfo(mainInfo), identity: mainNodeKey, role: importRoleMain, scopeOwner: mainNodeKey, payload: body, document: body, processing: processing, + sequenceDeclarations: cloneSequenceDeclarations(sequenceDeclarations), }) diagnostics = state.walkDeclarations(basePath, declarations, 0, Path{}) } else { @@ -255,7 +258,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem contributions: []acquiredContribution{{ source: cloneSourceInfo(mainInfo), payload: body, identity: mainNodeKey, role: importRoleMain, scopeOwner: mainNodeKey, destination: destination.clone(), - processing: processing, pendingItem: &localScope, + processing: processing, sequenceDeclarations: cloneSequenceDeclarations(sequenceDeclarations), pendingItem: &localScope, }}, } } @@ -313,14 +316,17 @@ func composeAcquiredInputWithPolicies(input assembledInput, lower collectionPoli if len(scope.contributions) == 1 && scope.contributions[0].role == importRoleMain && scope.contributions[0].pendingItem != nil { main := scope.contributions[0] localScope := cloneAcquisitionScope(*main.pendingItem) - if diagnostics := traversal.composePendingItems(&localScope); len(diagnostics) != 0 { + if diagnostics := traversal.composePendingItems(&localScope, policy); len(diagnostics) != 0 { return nil, prependCompositionDiagnosticPaths(diagnostics, main.destination.Index(0)) } documents := make([]*document.Node, 0, len(localScope.contributions)) for _, contribution := range localScope.contributions { documents = append(documents, contribution.document) } - completed, diagnostics := composeDocuments(documents...) + completed, diagnostics := composeDocumentsWithCollectionPolicy( + policy, localScope.collectionPolicies, + acquiredSequenceDeclarations(localScope), documents..., + ) if len(diagnostics) != 0 { return nil, prependCompositionDiagnosticPaths(diagnostics, main.destination.Index(0)) } @@ -392,19 +398,20 @@ func composeAcquiredScopeWithPolicy( versionState *sourceVersionState, accounting *importAccounting, ) (*composedDocument, []Diagnostic) { - _ = lower traversal := importTraversal{ accounting: accounting, limits: defaultImportLimits(), rule: rule, versionState: versionState, importGraph: graph.clone(), } - if diagnostics := traversal.composePendingItems(scope); len(diagnostics) != 0 { + if diagnostics := traversal.composePendingItems(scope, policy); len(diagnostics) != 0 { return nil, diagnostics } documents := make([]*document.Node, 0, len(scope.contributions)) for _, contribution := range scope.contributions { documents = append(documents, contribution.document) } - composition, diagnostics := composeDocumentsWithPolicy(policy, documents...) + composition, diagnostics := composeDocumentsWithCollectionPolicy( + policy, lower, acquiredSequenceDeclarations(*scope), documents..., + ) if len(diagnostics) != 0 { return nil, diagnostics } @@ -617,6 +624,7 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo var body *document.Node var declarations []importDeclarationSpec + var sequenceDeclarations []sequenceDeclarationSpec var diagnostics []Diagnostic var processing *sourceVersionProcessingResult if s.versionState != nil && s.versionState.mode != sourceVersionModeDefault { @@ -652,7 +660,7 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo s.active[identity] = len(s.stack) s.visited[identity] = route s.stack = append(s.stack, route) - diagnostics := s.visitItemSource(canonical, nodeKey, destination, relativeDestination, depth, info, body, declarations, processing) + diagnostics := s.visitItemSource(canonical, nodeKey, destination, relativeDestination, depth, info, body, declarations, sequenceDeclarations, processing) delete(s.active, identity) s.stack = s.stack[:len(s.stack)-1] return diagnostics @@ -671,6 +679,7 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo source: cloneSourceInfo(info), payload: body, document: placed, identity: nodeKey, role: role, scopeOwner: s.scope.owner, destination: placementDestination.clone(), relativeDestination: relativeDestination.clone(), processing: processing, + sequenceDeclarations: cloneSequenceDeclarations(sequenceDeclarations), }) s.active[identity] = len(s.stack) s.visited[identity] = route @@ -701,7 +710,7 @@ func (s *importTraversal) sourceDestination(destination Path) Path { return s.itemFallback.clone() } -func (s *importTraversal) visitItemSource(canonical string, identity importNodeKey, destination, relativeDestination Path, depth int, info SourceInfo, body *document.Node, declarations []importDeclarationSpec, processing *sourceVersionProcessingResult) []Diagnostic { +func (s *importTraversal) visitItemSource(canonical string, identity importNodeKey, destination, relativeDestination Path, depth int, info SourceInfo, body *document.Node, declarations []importDeclarationSpec, sequenceDeclarations []sequenceDeclarationSpec, processing *sourceVersionProcessingResult) []Diagnostic { parentScope := s.scope parentItemScope, parentPrefix, parentFallback := s.itemScope, s.itemPrefix, s.itemFallback s.scope = acquisitionScope{owner: identity} @@ -715,7 +724,7 @@ func (s *importTraversal) visitItemSource(canonical string, identity importNodeK s.scope.contributions = append(s.scope.contributions, acquiredContribution{ source: cloneSourceInfo(info), payload: body, document: body, identity: identity, role: importRoleItem, scopeOwner: identity, - processing: processing, + processing: processing, sequenceDeclarations: cloneSequenceDeclarations(sequenceDeclarations), }) diagnostics := s.walkDeclarations(s.access.dir(canonical), declarations, depth, Path{}) itemScope := s.scope @@ -742,21 +751,24 @@ func (s *importTraversal) scheduleAcquisitionScopes(scope *acquisitionScope) { } } -func (s *importTraversal) composePendingItems(scope *acquisitionScope) []Diagnostic { +func (s *importTraversal) composePendingItems(scope *acquisitionScope, policy sequencePolicy) []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 { + if diagnostics := s.composePendingItems(itemScope, policy); len(diagnostics) != 0 { return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination, index) } documents := make([]*document.Node, 0, len(itemScope.contributions)) for _, nested := range itemScope.contributions { documents = append(documents, nested.document) } - completed, diagnostics := composeDocuments(documents...) + completed, diagnostics := composeDocumentsWithCollectionPolicy( + policy, itemScope.collectionPolicies, + acquiredSequenceDeclarations(*itemScope), documents..., + ) if len(diagnostics) != 0 { return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination, index) } @@ -783,6 +795,18 @@ func (s *importTraversal) composePendingItems(scope *acquisitionScope) []Diagnos return nil } +func acquiredSequenceDeclarations(scope acquisitionScope) []sequenceDeclarationSpec { + var declarations []sequenceDeclarationSpec + for _, contribution := range scope.contributions { + for _, declaration := range contribution.sequenceDeclarations { + projected := cloneSequenceDeclarations([]sequenceDeclarationSpec{declaration})[0] + projected.destination = appendDestination(contribution.relativeDestination, declaration.destination) + declarations = append(declarations, projected) + } + } + return declarations +} + func importedVersionClaim(documents []*document.Node, scope acquisitionScope, composed *composedDocument, rule *VersionRule) *Diagnostic { if composed == nil || rule == nil { return nil @@ -915,6 +939,37 @@ func projectNestedEvidence(composed *composedDocument, scope acquisitionScope) { projectNestedOrigins(composed.origins, contribution.nested.root, contribution.nested.origins, prefix) } composed.applications = projectedApplications + projectNestedCollectionPolicies(composed, scope, ranges) +} + +func projectNestedCollectionPolicies(composed *composedDocument, scope acquisitionScope, ranges map[sequenceRangeKey]sequenceRange) { + if composed == nil { + return + } + baseOffsets := make(map[string]int) + for sourceIndex, contribution := range scope.contributions { + if contribution.nested == nil || len(contribution.nested.collectionPolicies.collections) == 0 { + continue + } + itemIndex := priorSequenceLength([]acquisitionScope{{contributions: scope.contributions[:sourceIndex]}}, contribution.destination) + if assigned, found := ranges[sequenceRangeKey{sourceIndex: sourceIndex, path: contribution.destination.String()}]; found { + if projected, ok := assigned.outputForInput(0); ok { + itemIndex = projected + } + } + key := contribution.destination.String() + base, initialized := baseOffsets[key] + policy, _ := composed.collectionPolicies.at(contribution.destination) + if !initialized { + base = len(policy.recordOrder) + baseOffsets[key] = base + } + policy.setRecordPolicyAt(base+itemIndex, collectionRecordPolicy{ + owner: newCollectionRecordPolicy().owner, + scope: contribution.nested.collectionPolicies, + }) + composed.collectionPolicies.set(contribution.destination, policy) + } } func projectNestedOrigins(target map[string]Origin, root *document.Node, origins map[string]Origin, prefix Path) { diff --git a/internal/engine/item_policy_test.go b/internal/engine/item_policy_test.go new file mode 100644 index 0000000..7c272f8 --- /dev/null +++ b/internal/engine/item_policy_test.go @@ -0,0 +1,229 @@ +package engine + +import ( + "fmt" + "strings" + "testing" + + "gitea.maximumdirect.net/eric/ConfigKit/internal/document" + yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml" +) + +func parseItemPolicyDocument(t *testing.T, sourceID, input string) *document.Node { + t.Helper() + root, issues := yamladapter.Parse(strings.NewReader(input), sourceID, yamladapter.Limits{ + Depth: maxDocumentDepth, Nodes: maxDocumentNodes, + }) + if len(issues) != 0 { + t.Fatalf("parse document: %#v", issues) + } + return root +} + +func TestPendingItemCompositionKeepsLocalDeclarationsAndHelperIdentity(t *testing.T) { + t.Parallel() + + declaration := sequenceDeclarationSpec{ + destination: Keys("prompts"), identityFields: []string{"id"}, identityPresent: true, + } + itemScope := acquisitionScope{contributions: []acquiredContribution{ + { + document: parseItemPolicyDocument(t, "item", "name: authored\nprompts:\n - id: authored\n"), + sequenceDeclarations: []sequenceDeclarationSpec{declaration}, + }, + { + document: parseItemPolicyDocument(t, "helper", "id: supplied\nprompts:\n - id: helper\n"), + }, + }} + outer := acquisitionScope{contributions: []acquiredContribution{{ + source: SourceInfo{ID: "item"}, + payload: parseItemPolicyDocument(t, "outer", "{}\n"), + destination: Keys("items"), pendingItem: &itemScope, + }}} + + composed, diagnostics := composeAcquiredScopeWithPolicy( + &outer, collectionPolicyScope{}, sequencePolicy{}, newImportInclusionGraph(), nil, nil, nil, &importAccounting{}, + ) + if len(diagnostics) != 0 { + t.Fatalf("compose pending item scope: %#v", diagnostics) + } + + item := outer.contributions[0].payload + if got, ok := documentAtPath(item, Keys("id")); !ok || got == nil || got.Text != "supplied" { + t.Fatalf("helper-supplied record identity = %#v", got) + } + if got, ok := documentAtPath(item, Keys("prompts")); !ok || got == nil || got.Kind != document.Sequence || len(got.Sequence) != 2 { + t.Fatalf("composed prompts = %#v, want two identified entries", got) + } + itemPolicy, ok := composed.collectionPolicies.at(Keys("items")) + if !ok { + t.Fatalf("projected item collection policy missing") + } + owned, ok := itemPolicy.recordPolicyAt(0) + if !ok { + t.Fatalf("projected item owner missing: %#v", itemPolicy) + } + if policy, ok := owned.scope.at(Keys("prompts")); !ok || !policy.identityPresent || len(policy.identityFields) != 1 || policy.identityFields[0] != "id" { + t.Fatalf("item-owned prompt policy = %#v, %v", policy, ok) + } + if _, ok := composed.collectionPolicies.at(Keys("prompts")); ok { + t.Fatal("item-local declaration escaped its owning record") + } +} + +func TestPendingItemCompositionProjectsNestedIdentityDiagnostics(t *testing.T) { + t.Parallel() + + childScope := acquisitionScope{contributions: []acquiredContribution{ + {document: parseItemPolicyDocument(t, "child", "prompts:\n - id: same\n"), sequenceDeclarations: []sequenceDeclarationSpec{{ + destination: Keys("prompts"), identityFields: []string{"id"}, identityPresent: true, + }}}, + {document: parseItemPolicyDocument(t, "child-helper", "prompts:\n - id: same\n")}, + }} + itemScope := acquisitionScope{contributions: []acquiredContribution{{ + document: parseItemPolicyDocument(t, "item", "name: parent\n"), + }, { + source: SourceInfo{ID: "child"}, payload: parseItemPolicyDocument(t, "child", "{}\n"), + destination: Keys("children"), pendingItem: &childScope, + }}} + outer := acquisitionScope{contributions: []acquiredContribution{{ + source: SourceInfo{ID: "item"}, payload: parseItemPolicyDocument(t, "outer", "{}\n"), + destination: Keys("items"), pendingItem: &itemScope, + }}} + + traversal := importTraversal{accounting: &importAccounting{}, limits: defaultImportLimits()} + diagnostics := traversal.composePendingItems(&outer, sequencePolicy{}) + if len(diagnostics) != 1 { + t.Fatalf("diagnostics = %#v, want one conflict", diagnostics) + } + if diagnostics[0].Code != CodeComposeConflict || diagnostics[0].Path.String() != "/items/0/children/0/prompts" { + t.Fatalf("diagnostic = %#v, want projected collection path", diagnostics[0]) + } + if strings.Contains(fmt.Sprint(diagnostics[0]), "same") { + t.Fatalf("diagnostic disclosed identity value: %v", diagnostics[0]) + } +} + +func TestPendingItemCompositionAppliesApplicationIdentityConstraints(t *testing.T) { + t.Parallel() + + itemScope := acquisitionScope{contributions: []acquiredContribution{{ + document: parseItemPolicyDocument(t, "item", "prompts:\n - text: missing-id\n"), + }}} + outer := acquisitionScope{contributions: []acquiredContribution{{ + source: SourceInfo{ID: "item"}, payload: parseItemPolicyDocument(t, "outer", "{}\n"), + destination: Keys("items"), pendingItem: &itemScope, + }}} + policy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{{ + selector: sequenceIdentitySelector{literalSequenceSelector("prompts")}, identityFields: []string{"id"}, + }}) + if len(diagnostics) != 0 { + t.Fatalf("compile application identity: %#v", diagnostics) + } + + traversal := importTraversal{accounting: &importAccounting{}, limits: defaultImportLimits()} + diagnostics = traversal.composePendingItems(&outer, policy) + if len(diagnostics) != 1 || diagnostics[0].Code != CodeComposeInvalid || diagnostics[0].Path.String() != "/items/0/prompts/0/id" { + t.Fatalf("application identity diagnostics = %#v", diagnostics) + } +} + +func TestReplacingOuterCollectionRetiresDiscardedItemIdentity(t *testing.T) { + t.Parallel() + + outerPath := Keys("items") + nestedPath := Keys("items").Index(0).Key("prompts") + lower := collectionPolicyScope{} + outerPolicy := collectionPolicy{} + outerPolicy.setRecordPolicy(newCollectionRecordPolicy()) + lower.set(outerPath, outerPolicy) + lower.set(nestedPath, collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) + + declarations := []sequenceDeclarationSpec{{ + destination: outerPath, mode: sequenceModeReplace, modePresent: true, + }} + input := parseItemPolicyDocument(t, "replacement", "items:\n - prompts:\n - text: new\n") + composed, diagnostics := composeDocumentsWithCollectionPolicy(sequencePolicy{}, lower, declarations, input) + if len(diagnostics) != 0 { + t.Fatalf("compose replacement: %#v", diagnostics) + } + policy, ok := composed.collectionPolicies.at(nestedPath) + if !ok || policy.identityPresent || len(policy.recordOrder) != 0 { + t.Fatalf("discarded nested owner policy = %#v, %v", policy, ok) + } +} + +func TestNestedItemPolicyProjectionUsesStableOutputIndexes(t *testing.T) { + t.Parallel() + + itemsPath := Keys("items") + outerPolicy := collectionPolicy{} + retainedScope := collectionPolicyScope{} + retainedScope.set(Keys("retained"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) + outerPolicy.setRecordPolicy(collectionRecordPolicy{owner: newCollectionRecordPolicy().owner, scope: retainedScope}) + composed := &composedDocument{collectionPolicies: collectionPolicyScope{}} + composed.collectionPolicies.set(itemsPath, outerPolicy) + + firstScope := collectionPolicyScope{} + firstScope.set(Keys("prompts"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) + secondScope := collectionPolicyScope{} + secondScope.set(Keys("prompts"), collectionPolicy{identityFields: []string{"slug"}, identityPresent: true}) + scope := acquisitionScope{contributions: []acquiredContribution{ + {document: parseItemPolicyDocument(t, "first", "items:\n - id: first\n"), destination: itemsPath, nested: &composedDocument{collectionPolicies: firstScope}}, + {document: parseItemPolicyDocument(t, "second", "items:\n - id: second\n"), destination: itemsPath, nested: &composedDocument{collectionPolicies: secondScope}}, + }} + + projectNestedCollectionPolicies(composed, scope, nil) + policy, ok := composed.collectionPolicies.at(itemsPath) + if !ok || len(policy.recordOrder) != 3 { + t.Fatalf("record-owned policies = %#v, %v; want retained plus two item scopes", policy, ok) + } + for index, wantField := range []string{"id", "id", "slug"} { + record, ok := policy.recordPolicyAt(index) + if !ok { + t.Fatalf("record policy %d missing", index) + } + path := Keys("retained") + if index != 0 { + path = Keys("prompts") + } + nested, ok := record.scope.at(path) + if !ok || !nested.identityPresent || len(nested.identityFields) != 1 || nested.identityFields[0] != wantField { + t.Errorf("record policy %d = %#v, %v; want %q", index, nested, ok, wantField) + } + } +} + +func TestItemSourceVersionProcessingConsumesDeclarationsBeforeComposition(t *testing.T) { + t.Parallel() + + root := parseItemPolicyDocument(t, "item", `version: v1 +configkit: + sequences: + - destination: [prompts] + key: id +prompts: + - id: example +`) + rule := &VersionRule{Path: Keys("version"), Check: func(version Version, _ *Validation) error { + if !version.Present || version.Text != "v1" { + t.Errorf("version = %#v, want v1", version) + } + return nil + }} + processed := processSourceVersionWithDirectiveContext( + root, rule, sourceVersionContribution, Keys("items").Index(0), sourceVersionModeRequired, rootDirectiveContextSource, + ) + if len(processed.diagnostics) != 0 { + t.Fatalf("process item source version: %#v", processed.diagnostics) + } + if len(processed.sequences) != 1 || !processed.sequences[0].identityPresent { + t.Fatalf("extracted item declarations = %#v", processed.sequences) + } + if _, ok := documentAtPath(processed.payload, Keys("version")); ok { + t.Fatalf("consumed version header remains in item payload: %#v", processed.payload) + } + if _, ok := documentAtPath(processed.payload, Keys("configkit")); ok { + t.Fatalf("consumed controls remain in item payload: %#v", processed.payload) + } +} diff --git a/internal/engine/load_input.go b/internal/engine/load_input.go index 16486da..90c269c 100644 --- a/internal/engine/load_input.go +++ b/internal/engine/load_input.go @@ -23,18 +23,19 @@ type assembledInput struct { // The latter is used for composition; the former preserves the acquisition // 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 - placementCharged bool + source SourceInfo + identity importNodeKey + role importContributionRole + scopeOwner importNodeKey + payload *document.Node + document *document.Node + destination Path + relativeDestination Path + processing *sourceVersionProcessingResult + sequenceDeclarations []sequenceDeclarationSpec + nested *composedDocument + pendingItem *acquisitionScope + placementCharged bool } // acquisitionScope is the ordered strict mapping scope produced by one input @@ -59,7 +60,8 @@ func cloneAcquisitionScope(scope acquisitionScope) acquisitionScope { 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), pendingItem: cloneAcquisitionScopePointer(contribution.pendingItem), + sequenceDeclarations: cloneSequenceDeclarations(contribution.sequenceDeclarations), + nested: cloneComposedDocument(contribution.nested), pendingItem: cloneAcquisitionScopePointer(contribution.pendingItem), placementCharged: contribution.placementCharged, } } diff --git a/internal/engine/source_version.go b/internal/engine/source_version.go index e8aab88..c8a0e27 100644 --- a/internal/engine/source_version.go +++ b/internal/engine/source_version.go @@ -141,6 +141,7 @@ func (state *sourceVersionState) process(root *document.Node, role sourceVersion type sourceVersionProcessingResult struct { payload *document.Node imports []importDeclarationSpec + sequences []sequenceDeclarationSpec evidence *sourceVersionEvidence diagnostics []Diagnostic } @@ -188,6 +189,10 @@ type sourceVersionPath struct { } func processSourceVersion(root *document.Node, rule *VersionRule, role sourceVersionRole, destination Path, mode sourceVersionMode) sourceVersionProcessingResult { + return processSourceVersionWithDirectiveContext(root, rule, role, destination, mode, rootDirectiveContextImportsOnly) +} + +func processSourceVersionWithDirectiveContext(root *document.Node, rule *VersionRule, role sourceVersionRole, destination Path, mode sourceVersionMode, directiveContext rootDirectiveContext) sourceVersionProcessingResult { if role != sourceVersionMain && role != sourceVersionContribution { return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source role is invalid")}} } @@ -204,11 +209,14 @@ func processSourceVersion(root *document.Node, rule *VersionRule, role sourceVer return sourceVersionProcessingResult{} } - payload, imports, diagnostics := splitImportDirectives(root) + extracted, diagnostics := extractRootDirectives(root, directiveContext) if len(diagnostics) != 0 { return sourceVersionProcessingResult{diagnostics: diagnostics} } - result := sourceVersionProcessingResult{payload: payload, imports: imports} + result := sourceVersionProcessingResult{ + payload: extracted.body, imports: extracted.imports, + sequences: cloneSequenceDeclarations(extracted.sequences), + } if mode == sourceVersionModeDefault { return result }