Integrate policy-aware item scopes
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Sequence Identity And Combination Implementation Plan
|
# 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
|
Audience: a Luna coding agent implementing one numbered stage per prompt, in
|
||||||
order. This plan starts at commit
|
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
|
Validation: item/destination/import/composition/source-version tests and the
|
||||||
everyday check. Keep current public behavior until load-path activation.
|
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
|
## Stage 13 — Activate Ordinary Source Collection Declarations
|
||||||
|
|
||||||
Scope: ordinary `Load` base-source assembly, excluding activation of the new
|
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)
|
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) {
|
func (policy collectionPolicy) recordPolicyAt(index int) (collectionRecordPolicy, bool) {
|
||||||
if index < 0 || index >= len(policy.recordOrder) {
|
if index < 0 || index >= len(policy.recordOrder) {
|
||||||
return collectionRecordPolicy{}, false
|
return collectionRecordPolicy{}, false
|
||||||
|
|||||||
@@ -316,13 +316,14 @@ func resolveStrictCollectionPolicies(
|
|||||||
}
|
}
|
||||||
|
|
||||||
identityFieldsByPath := make(map[string][]string)
|
identityFieldsByPath := make(map[string][]string)
|
||||||
collectionPolicies := cloneCollectionPolicyScope(lower)
|
effectiveLower := cloneCollectionPolicyScope(lower)
|
||||||
|
collectionPolicies := cloneCollectionPolicyScope(effectiveLower)
|
||||||
var diagnostics []Diagnostic
|
var diagnostics []Diagnostic
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
key := path.String()
|
key := path.String()
|
||||||
currentDeclarations := declarationsByPath[key]
|
currentDeclarations := declarationsByPath[key]
|
||||||
|
|
||||||
lowerEntry, hasLower := lower.collections[key]
|
lowerEntry, hasLower := effectiveLower.collections[key]
|
||||||
var lowerPolicy *collectionPolicy
|
var lowerPolicy *collectionPolicy
|
||||||
if hasLower {
|
if hasLower {
|
||||||
copy := cloneCollectionPolicy(lowerEntry.policy)
|
copy := cloneCollectionPolicy(lowerEntry.policy)
|
||||||
@@ -346,6 +347,10 @@ func resolveStrictCollectionPolicies(
|
|||||||
lowerPolicy.identityFields = nil
|
lowerPolicy.identityFields = nil
|
||||||
lowerPolicy.identityPresent = false
|
lowerPolicy.identityPresent = false
|
||||||
lowerPolicy.identityLocation = nil
|
lowerPolicy.identityLocation = nil
|
||||||
|
lowerPolicy.recordOrder = nil
|
||||||
|
lowerPolicy.recordPolicies = nil
|
||||||
|
effectiveLower.retireCollectionPoliciesAtOrBelow(path)
|
||||||
|
collectionPolicies.retireCollectionPoliciesAtOrBelow(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
resolution, found := resolveCollectionPolicy(path, currentDeclarations, applicationIdentity, lowerPolicy)
|
resolution, found := resolveCollectionPolicy(path, currentDeclarations, applicationIdentity, lowerPolicy)
|
||||||
|
|||||||
@@ -201,10 +201,12 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem
|
|||||||
state.stack = append(state.stack, mainRoute)
|
state.stack = append(state.stack, mainRoute)
|
||||||
var body *document.Node
|
var body *document.Node
|
||||||
var declarations []importDeclarationSpec
|
var declarations []importDeclarationSpec
|
||||||
|
var sequenceDeclarations []sequenceDeclarationSpec
|
||||||
var diagnostics []Diagnostic
|
var diagnostics []Diagnostic
|
||||||
if processing != nil {
|
if processing != nil {
|
||||||
body = processing.payload
|
body = processing.payload
|
||||||
declarations = processing.imports
|
declarations = processing.imports
|
||||||
|
sequenceDeclarations = cloneSequenceDeclarations(processing.sequences)
|
||||||
} else {
|
} else {
|
||||||
body, declarations, diagnostics = splitImportDirectives(main.document)
|
body, declarations, diagnostics = splitImportDirectives(main.document)
|
||||||
}
|
}
|
||||||
@@ -213,6 +215,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem
|
|||||||
state.scope.contributions = append(state.scope.contributions, acquiredContribution{
|
state.scope.contributions = append(state.scope.contributions, acquiredContribution{
|
||||||
source: cloneSourceInfo(mainInfo), identity: mainNodeKey, role: importRoleMain,
|
source: cloneSourceInfo(mainInfo), identity: mainNodeKey, role: importRoleMain,
|
||||||
scopeOwner: mainNodeKey, payload: body, document: body, processing: processing,
|
scopeOwner: mainNodeKey, payload: body, document: body, processing: processing,
|
||||||
|
sequenceDeclarations: cloneSequenceDeclarations(sequenceDeclarations),
|
||||||
})
|
})
|
||||||
diagnostics = state.walkDeclarations(basePath, declarations, 0, Path{})
|
diagnostics = state.walkDeclarations(basePath, declarations, 0, Path{})
|
||||||
} else {
|
} else {
|
||||||
@@ -255,7 +258,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem
|
|||||||
contributions: []acquiredContribution{{
|
contributions: []acquiredContribution{{
|
||||||
source: cloneSourceInfo(mainInfo), payload: body, identity: mainNodeKey,
|
source: cloneSourceInfo(mainInfo), payload: body, identity: mainNodeKey,
|
||||||
role: importRoleMain, scopeOwner: mainNodeKey, destination: destination.clone(),
|
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 {
|
if len(scope.contributions) == 1 && scope.contributions[0].role == importRoleMain && scope.contributions[0].pendingItem != nil {
|
||||||
main := scope.contributions[0]
|
main := scope.contributions[0]
|
||||||
localScope := cloneAcquisitionScope(*main.pendingItem)
|
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))
|
return nil, prependCompositionDiagnosticPaths(diagnostics, main.destination.Index(0))
|
||||||
}
|
}
|
||||||
documents := make([]*document.Node, 0, len(localScope.contributions))
|
documents := make([]*document.Node, 0, len(localScope.contributions))
|
||||||
for _, contribution := range localScope.contributions {
|
for _, contribution := range localScope.contributions {
|
||||||
documents = append(documents, contribution.document)
|
documents = append(documents, contribution.document)
|
||||||
}
|
}
|
||||||
completed, diagnostics := composeDocuments(documents...)
|
completed, diagnostics := composeDocumentsWithCollectionPolicy(
|
||||||
|
policy, localScope.collectionPolicies,
|
||||||
|
acquiredSequenceDeclarations(localScope), documents...,
|
||||||
|
)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return nil, prependCompositionDiagnosticPaths(diagnostics, main.destination.Index(0))
|
return nil, prependCompositionDiagnosticPaths(diagnostics, main.destination.Index(0))
|
||||||
}
|
}
|
||||||
@@ -392,19 +398,20 @@ func composeAcquiredScopeWithPolicy(
|
|||||||
versionState *sourceVersionState,
|
versionState *sourceVersionState,
|
||||||
accounting *importAccounting,
|
accounting *importAccounting,
|
||||||
) (*composedDocument, []Diagnostic) {
|
) (*composedDocument, []Diagnostic) {
|
||||||
_ = lower
|
|
||||||
traversal := importTraversal{
|
traversal := importTraversal{
|
||||||
accounting: accounting, limits: defaultImportLimits(), rule: rule,
|
accounting: accounting, limits: defaultImportLimits(), rule: rule,
|
||||||
versionState: versionState, importGraph: graph.clone(),
|
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
|
return nil, diagnostics
|
||||||
}
|
}
|
||||||
documents := make([]*document.Node, 0, len(scope.contributions))
|
documents := make([]*document.Node, 0, len(scope.contributions))
|
||||||
for _, contribution := range scope.contributions {
|
for _, contribution := range scope.contributions {
|
||||||
documents = append(documents, contribution.document)
|
documents = append(documents, contribution.document)
|
||||||
}
|
}
|
||||||
composition, diagnostics := composeDocumentsWithPolicy(policy, documents...)
|
composition, diagnostics := composeDocumentsWithCollectionPolicy(
|
||||||
|
policy, lower, acquiredSequenceDeclarations(*scope), documents...,
|
||||||
|
)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return nil, diagnostics
|
return nil, diagnostics
|
||||||
}
|
}
|
||||||
@@ -617,6 +624,7 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo
|
|||||||
|
|
||||||
var body *document.Node
|
var body *document.Node
|
||||||
var declarations []importDeclarationSpec
|
var declarations []importDeclarationSpec
|
||||||
|
var sequenceDeclarations []sequenceDeclarationSpec
|
||||||
var diagnostics []Diagnostic
|
var diagnostics []Diagnostic
|
||||||
var processing *sourceVersionProcessingResult
|
var processing *sourceVersionProcessingResult
|
||||||
if s.versionState != nil && s.versionState.mode != sourceVersionModeDefault {
|
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.active[identity] = len(s.stack)
|
||||||
s.visited[identity] = route
|
s.visited[identity] = route
|
||||||
s.stack = append(s.stack, 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)
|
delete(s.active, identity)
|
||||||
s.stack = s.stack[:len(s.stack)-1]
|
s.stack = s.stack[:len(s.stack)-1]
|
||||||
return diagnostics
|
return diagnostics
|
||||||
@@ -671,6 +679,7 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo
|
|||||||
source: cloneSourceInfo(info), payload: body, document: placed,
|
source: cloneSourceInfo(info), payload: body, document: placed,
|
||||||
identity: nodeKey, role: role, scopeOwner: s.scope.owner,
|
identity: nodeKey, role: role, scopeOwner: s.scope.owner,
|
||||||
destination: placementDestination.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
|
destination: placementDestination.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
|
||||||
|
sequenceDeclarations: cloneSequenceDeclarations(sequenceDeclarations),
|
||||||
})
|
})
|
||||||
s.active[identity] = len(s.stack)
|
s.active[identity] = len(s.stack)
|
||||||
s.visited[identity] = route
|
s.visited[identity] = route
|
||||||
@@ -701,7 +710,7 @@ func (s *importTraversal) sourceDestination(destination Path) Path {
|
|||||||
return s.itemFallback.clone()
|
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
|
parentScope := s.scope
|
||||||
parentItemScope, parentPrefix, parentFallback := s.itemScope, s.itemPrefix, s.itemFallback
|
parentItemScope, parentPrefix, parentFallback := s.itemScope, s.itemPrefix, s.itemFallback
|
||||||
s.scope = acquisitionScope{owner: identity}
|
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{
|
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
||||||
source: cloneSourceInfo(info), payload: body, document: body,
|
source: cloneSourceInfo(info), payload: body, document: body,
|
||||||
identity: identity, role: importRoleItem, scopeOwner: identity,
|
identity: identity, role: importRoleItem, scopeOwner: identity,
|
||||||
processing: processing,
|
processing: processing, sequenceDeclarations: cloneSequenceDeclarations(sequenceDeclarations),
|
||||||
})
|
})
|
||||||
diagnostics := s.walkDeclarations(s.access.dir(canonical), declarations, depth, Path{})
|
diagnostics := s.walkDeclarations(s.access.dir(canonical), declarations, depth, Path{})
|
||||||
itemScope := s.scope
|
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 {
|
for index := range scope.contributions {
|
||||||
contribution := &scope.contributions[index]
|
contribution := &scope.contributions[index]
|
||||||
if contribution.pendingItem == nil {
|
if contribution.pendingItem == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
itemScope := contribution.pendingItem
|
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)
|
return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination, index)
|
||||||
}
|
}
|
||||||
documents := make([]*document.Node, 0, len(itemScope.contributions))
|
documents := make([]*document.Node, 0, len(itemScope.contributions))
|
||||||
for _, nested := range itemScope.contributions {
|
for _, nested := range itemScope.contributions {
|
||||||
documents = append(documents, nested.document)
|
documents = append(documents, nested.document)
|
||||||
}
|
}
|
||||||
completed, diagnostics := composeDocuments(documents...)
|
completed, diagnostics := composeDocumentsWithCollectionPolicy(
|
||||||
|
policy, itemScope.collectionPolicies,
|
||||||
|
acquiredSequenceDeclarations(*itemScope), documents...,
|
||||||
|
)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination, index)
|
return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination, index)
|
||||||
}
|
}
|
||||||
@@ -783,6 +795,18 @@ func (s *importTraversal) composePendingItems(scope *acquisitionScope) []Diagnos
|
|||||||
return nil
|
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 {
|
func importedVersionClaim(documents []*document.Node, scope acquisitionScope, composed *composedDocument, rule *VersionRule) *Diagnostic {
|
||||||
if composed == nil || rule == nil {
|
if composed == nil || rule == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -915,6 +939,37 @@ func projectNestedEvidence(composed *composedDocument, scope acquisitionScope) {
|
|||||||
projectNestedOrigins(composed.origins, contribution.nested.root, contribution.nested.origins, prefix)
|
projectNestedOrigins(composed.origins, contribution.nested.root, contribution.nested.origins, prefix)
|
||||||
}
|
}
|
||||||
composed.applications = projectedApplications
|
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) {
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ type acquiredContribution struct {
|
|||||||
destination Path
|
destination Path
|
||||||
relativeDestination Path
|
relativeDestination Path
|
||||||
processing *sourceVersionProcessingResult
|
processing *sourceVersionProcessingResult
|
||||||
|
sequenceDeclarations []sequenceDeclarationSpec
|
||||||
nested *composedDocument
|
nested *composedDocument
|
||||||
pendingItem *acquisitionScope
|
pendingItem *acquisitionScope
|
||||||
placementCharged bool
|
placementCharged bool
|
||||||
@@ -59,6 +60,7 @@ func cloneAcquisitionScope(scope acquisitionScope) acquisitionScope {
|
|||||||
role: contribution.role, scopeOwner: contribution.scopeOwner, payload: contribution.payload,
|
role: contribution.role, scopeOwner: contribution.scopeOwner, payload: contribution.payload,
|
||||||
document: contribution.document, destination: contribution.destination.clone(),
|
document: contribution.document, destination: contribution.destination.clone(),
|
||||||
relativeDestination: contribution.relativeDestination.clone(), processing: contribution.processing,
|
relativeDestination: contribution.relativeDestination.clone(), processing: contribution.processing,
|
||||||
|
sequenceDeclarations: cloneSequenceDeclarations(contribution.sequenceDeclarations),
|
||||||
nested: cloneComposedDocument(contribution.nested), pendingItem: cloneAcquisitionScopePointer(contribution.pendingItem),
|
nested: cloneComposedDocument(contribution.nested), pendingItem: cloneAcquisitionScopePointer(contribution.pendingItem),
|
||||||
placementCharged: contribution.placementCharged,
|
placementCharged: contribution.placementCharged,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ func (state *sourceVersionState) process(root *document.Node, role sourceVersion
|
|||||||
type sourceVersionProcessingResult struct {
|
type sourceVersionProcessingResult struct {
|
||||||
payload *document.Node
|
payload *document.Node
|
||||||
imports []importDeclarationSpec
|
imports []importDeclarationSpec
|
||||||
|
sequences []sequenceDeclarationSpec
|
||||||
evidence *sourceVersionEvidence
|
evidence *sourceVersionEvidence
|
||||||
diagnostics []Diagnostic
|
diagnostics []Diagnostic
|
||||||
}
|
}
|
||||||
@@ -188,6 +189,10 @@ type sourceVersionPath struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func processSourceVersion(root *document.Node, rule *VersionRule, role sourceVersionRole, destination Path, mode sourceVersionMode) sourceVersionProcessingResult {
|
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 {
|
if role != sourceVersionMain && role != sourceVersionContribution {
|
||||||
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source role is invalid")}}
|
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source role is invalid")}}
|
||||||
}
|
}
|
||||||
@@ -204,11 +209,14 @@ func processSourceVersion(root *document.Node, rule *VersionRule, role sourceVer
|
|||||||
return sourceVersionProcessingResult{}
|
return sourceVersionProcessingResult{}
|
||||||
}
|
}
|
||||||
|
|
||||||
payload, imports, diagnostics := splitImportDirectives(root)
|
extracted, diagnostics := extractRootDirectives(root, directiveContext)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return sourceVersionProcessingResult{diagnostics: diagnostics}
|
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 {
|
if mode == sourceVersionModeDefault {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user