Track collection policy ownership

This commit is contained in:
2026-10-23 14:16:22 +00:00
parent 0314e587b6
commit b202dd187f
5 changed files with 547 additions and 4 deletions

View File

@@ -287,6 +287,9 @@ Scope: private scoped-state transformations built on Stage 4's resolution helper
Validation: focused collection-policy tests and the everyday check. This stage
must not add path-existence, shape, nullability, or global YAML final-check rules.
**Completion:** Implemented and verified with focused collection-policy tests,
`go test -race ./internal/engine`, and `./scripts/check.sh`.
## Stage 6 — Implement The Shared Sequence Operations
Scope: a pure document-level combination helper, not separate implementations

View File

@@ -7,6 +7,8 @@ type collectionPolicy struct {
explicitMode sequenceMode
explicitModePresent bool
modeLocation *Location
recordOrder []*collectionRecordOwner
recordPolicies map[*collectionRecordOwner]collectionPolicyScope
}
type collectionPolicyResolution struct {

View File

@@ -0,0 +1,242 @@
package engine
import (
"sort"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
type collectionPolicyScope struct {
collections map[string]collectionPolicyEntry
}
type collectionPolicyEntry struct {
destination Path
policy collectionPolicy
}
type collectionRecordPolicy struct {
owner *collectionRecordOwner
scope collectionPolicyScope
}
// The nonzero field ensures simultaneously live record owners have distinct
// addresses. The token is per-record ownership, not a source or identity key.
type collectionRecordOwner struct {
marker byte
}
type projectedCollectionPolicy struct {
path Path
policy collectionPolicy
}
func newCollectionRecordPolicy() collectionRecordPolicy {
return collectionRecordPolicy{owner: &collectionRecordOwner{marker: 1}}
}
func retainedCollectionRecordPolicy(record collectionRecordPolicy) collectionRecordPolicy {
return collectionRecordPolicy{
owner: record.owner,
scope: cloneCollectionPolicyScope(record.scope),
}
}
func replacedCollectionRecordPolicy() collectionRecordPolicy {
return newCollectionRecordPolicy()
}
func (policy *collectionPolicy) setRecordPolicy(record collectionRecordPolicy) {
if record.owner == nil {
record.owner = newCollectionRecordPolicy().owner
}
if policy.recordPolicies == nil {
policy.recordPolicies = make(map[*collectionRecordOwner]collectionPolicyScope)
}
if _, exists := policy.recordPolicies[record.owner]; !exists {
policy.recordOrder = append(policy.recordOrder, 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
}
owner := policy.recordOrder[index]
scope, ok := policy.recordPolicies[owner]
if !ok {
return collectionRecordPolicy{}, false
}
return collectionRecordPolicy{owner: owner, scope: cloneCollectionPolicyScope(scope)}, true
}
func collectionPolicyForReplacement(lower *collectionPolicy, current collectionPolicy) collectionPolicy {
result := cloneCollectionPolicy(current)
if !current.explicitModePresent && lower != nil && lower.explicitModePresent {
result.explicitMode = lower.explicitMode
result.explicitModePresent = true
result.modeLocation = cloneLocation(lower.modeLocation)
}
return result
}
// collectionPolicyWithoutValue keeps identity and record ownership because no
// collection value was replaced. A key-only declaration can remain pending
// when no sequence is present, or validate the retained sequence immediately.
func collectionPolicyWithoutValue(
lower *collectionPolicy,
current collectionPolicy,
retainedValue *document.Node,
path Path,
) (collectionPolicy, []Diagnostic) {
result := collectionPolicy{}
if lower != nil {
result = cloneCollectionPolicy(*lower)
}
if lower != nil && lower.identityPresent && current.identityPresent && !equalSequenceIdentityFields(lower.identityFields, current.identityFields) {
return collectionPolicy{}, []Diagnostic{collectionPolicyConflictDiagnostic(
path, current.identityLocation, lower.identityLocation,
"collection identity declaration conflicts with retained identity",
)}
}
if current.identityPresent {
result.identityFields = cloneStrings(current.identityFields)
result.identityPresent = true
result.identityLocation = cloneLocation(current.identityLocation)
}
if current.explicitModePresent {
result.explicitMode = current.explicitMode
result.explicitModePresent = true
result.modeLocation = cloneLocation(current.modeLocation)
}
if current.identityPresent && retainedValue != nil && retainedValue.Kind == document.Sequence {
_, diagnostics := validateSequenceIdentity(retainedValue, path, result.identityFields, sequenceIdentityDiagnosticCodes{
invalid: CodeSequenceInvalid, conflict: CodeSequenceConflict,
})
if len(diagnostics) != 0 {
return collectionPolicy{}, diagnostics
}
}
return result, nil
}
func (scope *collectionPolicyScope) set(destination Path, policy collectionPolicy) {
if scope.collections == nil {
scope.collections = make(map[string]collectionPolicyEntry)
}
scope.collections[destination.String()] = collectionPolicyEntry{
destination: destination.clone(), policy: cloneCollectionPolicy(policy),
}
}
func (scope collectionPolicyScope) at(destination Path) (collectionPolicy, bool) {
entry, ok := scope.collections[destination.String()]
if !ok {
return collectionPolicy{}, false
}
return cloneCollectionPolicy(entry.policy), true
}
// retireCollectionPoliciesAtOrBelow clears YAML identity and record-owned state
// below a replaced ancestor while retaining independently scoped explicit modes.
func (scope *collectionPolicyScope) retireCollectionPoliciesAtOrBelow(ancestor Path) {
if scope == nil {
return
}
for key, entry := range scope.collections {
if !pathHasPrefix(entry.destination, ancestor) {
continue
}
entry.policy.identityFields = nil
entry.policy.identityPresent = false
entry.policy.identityLocation = nil
entry.policy.recordOrder = nil
entry.policy.recordPolicies = nil
scope.collections[key] = entry
}
}
func projectCollectionPolicyScope(scope collectionPolicyScope, prefix Path) []projectedCollectionPolicy {
keys := make([]string, 0, len(scope.collections))
for key := range scope.collections {
keys = append(keys, key)
}
sort.Strings(keys)
var projected []projectedCollectionPolicy
for _, key := range keys {
entry := scope.collections[key]
concrete := appendDestination(prefix, entry.destination)
projected = append(projected, projectedCollectionPolicy{path: concrete, policy: cloneCollectionPolicy(entry.policy)})
for index, owner := range entry.policy.recordOrder {
if recordScope, ok := entry.policy.recordPolicies[owner]; ok {
projected = append(projected, projectCollectionPolicyScope(recordScope, concrete.Index(index))...)
}
}
}
return projected
}
func cloneCollectionPolicyScope(scope collectionPolicyScope) collectionPolicyScope {
if scope.collections == nil {
return collectionPolicyScope{}
}
cloned := collectionPolicyScope{collections: make(map[string]collectionPolicyEntry, len(scope.collections))}
for key, entry := range scope.collections {
cloned.collections[key] = collectionPolicyEntry{
destination: entry.destination.clone(), policy: cloneCollectionPolicy(entry.policy),
}
}
return cloned
}
func cloneCollectionPolicyScopeForInheritance(scope collectionPolicyScope) collectionPolicyScope {
if scope.collections == nil {
return collectionPolicyScope{}
}
cloned := collectionPolicyScope{collections: make(map[string]collectionPolicyEntry, len(scope.collections))}
for key, entry := range scope.collections {
cloned.collections[key] = collectionPolicyEntry{
destination: entry.destination.clone(), policy: cloneCollectionPolicyForInheritance(entry.policy),
}
}
return cloned
}
func cloneCollectionPolicy(policy collectionPolicy) collectionPolicy {
cloned := policy
cloned.identityFields = cloneStrings(policy.identityFields)
cloned.identityLocation = cloneLocation(policy.identityLocation)
cloned.modeLocation = cloneLocation(policy.modeLocation)
if policy.recordOrder != nil {
cloned.recordOrder = append([]*collectionRecordOwner(nil), policy.recordOrder...)
}
if policy.recordPolicies != nil {
cloned.recordPolicies = make(map[*collectionRecordOwner]collectionPolicyScope, len(policy.recordPolicies))
for owner, scope := range policy.recordPolicies {
cloned.recordPolicies[owner] = cloneCollectionPolicyScope(scope)
}
}
return cloned
}
func cloneCollectionPolicyForInheritance(policy collectionPolicy) collectionPolicy {
cloned := policy
cloned.identityFields = cloneStrings(policy.identityFields)
cloned.identityLocation = cloneLocation(policy.identityLocation)
cloned.modeLocation = cloneLocation(policy.modeLocation)
if policy.recordOrder != nil {
cloned.recordOrder = make([]*collectionRecordOwner, 0, len(policy.recordOrder))
}
if policy.recordPolicies != nil {
cloned.recordPolicies = make(map[*collectionRecordOwner]collectionPolicyScope, len(policy.recordPolicies))
for _, sourceOwner := range policy.recordOrder {
owner := newCollectionRecordPolicy().owner
cloned.recordOrder = append(cloned.recordOrder, owner)
if sourceScope, ok := policy.recordPolicies[sourceOwner]; ok {
cloned.recordPolicies[owner] = cloneCollectionPolicyScopeForInheritance(sourceScope)
}
}
}
return cloned
}

View File

@@ -0,0 +1,292 @@
package engine
import (
"reflect"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
func TestCollectionPolicyWithoutValueRetainsIdentityAndMode(t *testing.T) {
lower := collectionPolicy{
identityFields: []string{"id"},
identityPresent: true,
explicitMode: sequenceModeAppend,
explicitModePresent: true,
}
lower.setRecordPolicy(newCollectionRecordPolicy())
current := collectionPolicy{explicitMode: sequenceModeReplace, explicitModePresent: true}
retained, diagnostics := collectionPolicyWithoutValue(&lower, current, nil, Keys("prompts"))
if len(diagnostics) != 0 || !retained.identityPresent || !reflect.DeepEqual(retained.identityFields, []string{"id"}) {
t.Fatalf("no-value replace reset identity: %#v, %#v", retained, diagnostics)
}
if retained.explicitMode != sequenceModeReplace || !retained.explicitModePresent || len(retained.recordOrder) != 1 {
t.Fatalf("no-value policy update lost mode or record ownership: %#v", retained)
}
retainedRecord, _ := retained.recordPolicyAt(0)
lowerRecord, _ := lower.recordPolicyAt(0)
if retainedRecord.owner != lowerRecord.owner {
t.Fatal("no-value policy update changed record ownership")
}
}
func TestCollectionPolicyWithoutValueLeavesAbsentAndNonSequenceDestinationsUnconstrained(t *testing.T) {
current := collectionPolicy{identityFields: []string{"id"}, identityPresent: true}
for _, value := range []*document.Node{nil, parsePolicyNode(t, "value: scalar\n").Mapping[0].Value} {
retained, diagnostics := collectionPolicyWithoutValue(nil, current, value, Keys("prompts"))
if len(diagnostics) != 0 || !retained.identityPresent || !reflect.DeepEqual(retained.identityFields, []string{"id"}) {
t.Fatalf("pending identity at absent/non-sequence destination = %#v, %#v", retained, diagnostics)
}
}
}
func TestCollectionPolicyWithoutValueValidatesRetainedSequenceImmediately(t *testing.T) {
current := collectionPolicy{identityFields: []string{"id"}, identityPresent: true}
valid := parsePolicyNode(t, "items: [{id: one}, {id: two}]\n").Mapping[0].Value
retained, diagnostics := collectionPolicyWithoutValue(nil, current, valid, Keys("prompts"))
if len(diagnostics) != 0 || !retained.identityPresent {
t.Fatalf("valid retained records were rejected: %#v, %#v", retained, diagnostics)
}
invalid := parsePolicyNode(t, "items: [{name: missing-id}]\n").Mapping[0].Value
retained, diagnostics = collectionPolicyWithoutValue(nil, current, invalid, Keys("prompts"))
if !reflect.DeepEqual(retained, collectionPolicy{}) || len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceInvalid || diagnostics[0].Path.String() != "/prompts/0/id" {
t.Fatalf("invalid retained records = %#v, %#v", retained, diagnostics)
}
}
func TestCollectionPolicyWithoutValueRejectsIdentityChange(t *testing.T) {
lower := collectionPolicy{identityFields: []string{"id"}, identityPresent: true}
current := collectionPolicy{identityFields: []string{"name"}, identityPresent: true}
retained, diagnostics := collectionPolicyWithoutValue(&lower, current, nil, Keys("prompts"))
if !reflect.DeepEqual(retained, collectionPolicy{}) || len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceConflict {
t.Fatalf("no-value identity change = %#v, %#v", retained, diagnostics)
}
}
func TestCollectionPolicyLifetimePersistenceAndReplacementTable(t *testing.T) {
tests := []struct {
name string
current collectionPolicy
replaceValue bool
wantIdentity []string
wantIdentitySet bool
wantMode sequenceMode
wantRecords int
}{
{
name: "mode-only replace declaration does not reset retained identity",
current: collectionPolicy{
explicitMode: sequenceModeReplace, explicitModePresent: true,
},
wantIdentity: []string{"id"}, wantIdentitySet: true,
wantMode: sequenceModeReplace, wantRecords: 1,
},
{
name: "whole-sequence replacement resets lower identity",
replaceValue: true,
wantIdentitySet: false,
wantMode: sequenceModeAppend,
wantRecords: 0,
},
{
name: "replacement can establish a new identity",
current: collectionPolicy{identityFields: []string{"name"}, identityPresent: true},
replaceValue: true,
wantIdentity: []string{"name"},
wantIdentitySet: true,
wantMode: sequenceModeAppend,
wantRecords: 0,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
lower := collectionPolicy{
identityFields: []string{"id"}, identityPresent: true,
explicitMode: sequenceModeAppend, explicitModePresent: true,
}
lower.setRecordPolicy(newCollectionRecordPolicy())
var got collectionPolicy
if test.replaceValue {
got = collectionPolicyForReplacement(&lower, test.current)
} else {
var diagnostics []Diagnostic
got, diagnostics = collectionPolicyWithoutValue(&lower, test.current, nil, Keys("prompts"))
if len(diagnostics) != 0 {
t.Fatalf("policy diagnostics = %#v", diagnostics)
}
}
if got.identityPresent != test.wantIdentitySet || !reflect.DeepEqual(got.identityFields, test.wantIdentity) || got.explicitMode != test.wantMode || len(got.recordOrder) != test.wantRecords {
t.Fatalf("transition result = %#v", got)
}
})
}
}
func TestCollectionPolicyValueReplacementResetsIdentityButRetainsExplicitMode(t *testing.T) {
lower := collectionPolicy{
identityFields: []string{"id"},
identityPresent: true,
explicitMode: sequenceModeAppend,
explicitModePresent: true,
}
lower.setRecordPolicy(newCollectionRecordPolicy())
current := collectionPolicy{identityFields: []string{"name"}, identityPresent: true}
replaced := collectionPolicyForReplacement(&lower, current)
if !replaced.identityPresent || !reflect.DeepEqual(replaced.identityFields, []string{"name"}) {
t.Fatalf("replacement identity was not reset: %#v", replaced)
}
if replaced.explicitMode != sequenceModeAppend || !replaced.explicitModePresent || len(replaced.recordOrder) != 0 {
t.Fatalf("replacement failed to retain explicit mode or retire lower record policy: %#v", replaced)
}
cleared := collectionPolicyForReplacement(&lower, collectionPolicy{})
if cleared.identityPresent || len(cleared.identityFields) != 0 || cleared.explicitMode != sequenceModeAppend || !cleared.explicitModePresent {
t.Fatalf("replacement without a new key did not clear YAML identity while retaining mode: %#v", cleared)
}
}
func TestCollectionRecordRetentionAndReplacementUseOpaqueOwners(t *testing.T) {
oldRecord := newCollectionRecordPolicy()
oldRecord.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
retained := retainedCollectionRecordPolicy(oldRecord)
if retained.owner != oldRecord.owner {
t.Fatal("retained record did not keep its owner")
}
if got, ok := retained.scope.at(Keys("steps")); !ok || !got.identityPresent {
t.Fatalf("retained record lost its local policy: %#v, %t", got, ok)
}
if retained.scope.collections["/steps"].policy.identityFields[0] = "changed"; oldRecord.scope.collections["/steps"].policy.identityFields[0] != "id" {
t.Fatal("retained record policy aliases its source")
}
// A whole-record replacement at the same list position, even with equal
// authored identity/source metadata, receives new ownership and no old rules.
replacement := replacedCollectionRecordPolicy()
if replacement.owner == oldRecord.owner || len(replacement.scope.collections) != 0 {
t.Fatal("replacement record inherited discarded owner policy")
}
}
func TestCollectionPolicyAncestorRetirementPreservesIndependentMode(t *testing.T) {
var scope collectionPolicyScope
owner := newCollectionRecordPolicy()
owner.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
scope.set(Keys("catalog", "prompts"), collectionPolicyWithRecords(collectionPolicy{
identityFields: []string{"id"},
identityPresent: true,
explicitMode: sequenceModeAppend,
explicitModePresent: true,
}, owner))
scope.retireCollectionPoliciesAtOrBelow(Keys("catalog"))
policy, ok := scope.at(Keys("catalog", "prompts"))
if !ok || policy.identityPresent || len(policy.recordOrder) != 0 || policy.explicitMode != sequenceModeAppend || !policy.explicitModePresent {
t.Fatalf("ancestor retirement = %#v, %t", policy, ok)
}
reintroduced, diagnostics := collectionPolicyWithoutValue(&policy, collectionPolicy{}, nil, Keys("catalog", "prompts"))
if len(diagnostics) != 0 || reintroduced.identityPresent || reintroduced.explicitMode != sequenceModeAppend {
t.Fatalf("retired identity was resurrected while mode persisted: %#v, %#v", reintroduced, diagnostics)
}
}
func TestEqualOuterIdentitiesHaveDistinctRecordOwners(t *testing.T) {
retained := newCollectionRecordPolicy()
replacement := replacedCollectionRecordPolicy()
if retained.owner == replacement.owner {
t.Fatal("replacement with the same outer identity reused the old owner")
}
if retained.owner == nil || replacement.owner == nil {
t.Fatal("record owner token is missing")
}
}
func TestCollectionPolicyScopesKeepRootAndItemRulesIndependent(t *testing.T) {
var root collectionPolicyScope
root.set(Keys("steps"), collectionPolicy{identityFields: []string{"root-id"}, identityPresent: true})
owner := newCollectionRecordPolicy()
owner.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"item-id"}, identityPresent: true})
root.set(Keys("items"), collectionPolicyWithRecords(collectionPolicy{}, owner))
root.retireCollectionPoliciesAtOrBelow(Keys("steps"))
rootPolicy, ok := root.at(Keys("steps"))
if !ok || rootPolicy.identityPresent {
t.Fatalf("root-local policy was not retired: %#v", rootPolicy)
}
itemPolicy, ok := root.at(Keys("items"))
if !ok || len(itemPolicy.recordOrder) != 1 {
t.Fatalf("unrelated item owner disappeared: %#v", itemPolicy)
}
itemRecord, _ := itemPolicy.recordPolicyAt(0)
nested, ok := itemRecord.scope.at(Keys("steps"))
if !ok || !nested.identityPresent || !reflect.DeepEqual(nested.identityFields, []string{"item-id"}) {
t.Fatalf("item-local policy was coupled to root destination: %#v", nested)
}
}
func TestCollectionPolicyProjectionUsesConcretePlacement(t *testing.T) {
first := newCollectionRecordPolicy()
first.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"step"}, identityPresent: true})
second := newCollectionRecordPolicy()
second.scope.set(Keys("steps"), collectionPolicy{explicitMode: sequenceModeAppend, explicitModePresent: true})
var scope collectionPolicyScope
scope.set(Keys("items"), collectionPolicyWithRecords(collectionPolicy{}, first, second))
projected := projectCollectionPolicyScope(scope, Keys("mounted"))
if len(projected) != 3 {
t.Fatalf("projected policies = %#v", projected)
}
want := []string{"/mounted/items", "/mounted/items/0/steps", "/mounted/items/1/steps"}
for index, policy := range projected {
if policy.path.String() != want[index] {
t.Fatalf("projected path[%d] = %q, want %q", index, policy.path.String(), want[index])
}
}
}
func TestCollectionPolicyInheritanceCopiesOwnersAndState(t *testing.T) {
owner := newCollectionRecordPolicy()
owner.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
original := collectionPolicyWithRecords(collectionPolicy{}, owner)
copy := cloneCollectionPolicyForInheritance(original)
copyOwner := copy.recordOrder[0]
if copyOwner == owner.owner {
t.Fatal("inheritance copy retained the source record owner")
}
copy.recordPolicies[copyOwner].collections["/steps"].policy.identityFields[0] = "changed"
if owner.scope.collections["/steps"].policy.identityFields[0] != "id" {
t.Fatal("inheritance copy aliases source policy")
}
}
func TestAcquisitionScopeCloneOwnsCollectionPolicyCopy(t *testing.T) {
owner := newCollectionRecordPolicy()
owner.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
var scope acquisitionScope
scope.collectionPolicies.set(Keys("items"), collectionPolicyWithRecords(collectionPolicy{}, owner))
cloned := cloneAcquisitionScope(scope)
clonedOwner := cloned.collectionPolicies.collections["/items"].policy.recordOrder[0]
cloned.collectionPolicies.collections["/items"].policy.recordPolicies[clonedOwner].collections["/steps"].policy.identityFields[0] = "changed"
sourceOwner := scope.collectionPolicies.collections["/items"].policy.recordOrder[0]
if scope.collectionPolicies.collections["/items"].policy.recordPolicies[sourceOwner].collections["/steps"].policy.identityFields[0] != "id" {
t.Fatal("acquisition scope clone aliases collection policy")
}
if clonedOwner != owner.owner {
t.Fatal("acquisition scope clone changed record ownership")
}
}
func collectionPolicyWithRecords(policy collectionPolicy, records ...collectionRecordPolicy) collectionPolicy {
for _, record := range records {
policy.setRecordPolicy(record)
}
return policy
}
func parsePolicyNode(t *testing.T, input string) *document.Node {
t.Helper()
parsed, diagnostics := parseSource(Bytes("collection-policy-state.yml", []byte(input)), "collection-policy-state", defaultParseLimits())
if len(diagnostics) != 0 {
t.Fatalf("parse policy value: %#v", diagnostics)
}
return parsed.document
}

View File

@@ -42,10 +42,14 @@ type acquisitionScope struct {
owner importNodeKey
destination Path
contributions []acquiredContribution
collectionPolicies collectionPolicyScope
}
func cloneAcquisitionScope(scope acquisitionScope) acquisitionScope {
cloned := acquisitionScope{owner: scope.owner, destination: scope.destination.clone()}
cloned := acquisitionScope{
owner: scope.owner, destination: scope.destination.clone(),
collectionPolicies: cloneCollectionPolicyScope(scope.collectionPolicies),
}
if scope.contributions != nil {
cloned.contributions = make([]acquiredContribution, len(scope.contributions))
for index, contribution := range scope.contributions {