Integrate policy-aware item scopes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
229
internal/engine/item_policy_test.go
Normal file
229
internal/engine/item_policy_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user