Separate collection overlay responsibilities

This commit is contained in:
2026-10-25 22:42:21 +00:00
parent 883244936c
commit 549fca239d
2 changed files with 162 additions and 66 deletions

View File

@@ -816,6 +816,16 @@ combination diagnostics remain protected. Newly separated inputs/results make
lower versus incoming versus retained ownership explicit.
Focused check: `go test ./internal/engine -run 'Collection|Sequence|Overlay|Inheritance|History'`.
Status: **complete** (preparation/ownership part of E03). Prior completed commits:
`2736ddf`, `deb9aa5`, `ae3408a`, `ea27f17`, `08a8c25`, `877c497`, `8aba43d`,
`7bf2f0c`, `0f9ecf1`, `119b42c`, `247a12b`, `4fcff5f`, `12d350a`, `08f2a8f`,
`826062a`, `e4a5ecb`, `3122e2f`, `6f6130f`, `4fd3bee`, `4bf940c`, `d8bf6ab`,
`f185821`, `a8ae2e9`, and `8832449`. Validation passed:
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Collection|Sequence|Overlay|Inheritance|History' -count=1`,
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -count=1`,
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./... -count=1`,
and `git diff --check`.
## Stage 26 — Index Assembly Policy Retirement By Semantic Path
Finding: **E01**.

View File

@@ -83,7 +83,6 @@ func (a *assemblyAccumulator) applyOverlayWithCollectionProjection(
if a == nil || a.current == nil || a.current.root == nil || a.current.root.Kind != document.Mapping || overlay == nil || overlay.Kind != document.Mapping {
return nil, invalidOverlayDocument()
}
mergeOriginContribution(a.current.origins, Path{}, incomingOrigin(incomingOrigins, Path{}, overlay, layer))
sequences, diagnostics := mergeOverlayMappingsWithCollectionPolicy(a, overlay, incomingOrigins, incomingPolicies, layer, sequencePolicy, declarations)
if len(diagnostics) != 0 {
return nil, diagnostics
@@ -110,11 +109,39 @@ func mergeOverlayMappingsWithCollectionPolicy(
sequencePolicy sequencePolicy,
declarations []sequenceDeclarationSpec,
) ([]sequenceHistoryContribution, []Diagnostic) {
result := assembly.current
prepared, diagnostics := prepareOverlayCollection(
assembly.current.collectionPolicies, overlay, sequencePolicy, declarations,
)
if len(diagnostics) != 0 {
return nil, diagnostics
}
operation := newOverlayMergeOperation(assembly, overlay, incomingOrigins, incomingPolicies, layer, prepared)
operation.apply()
if len(operation.diagnostics) != 0 {
return nil, canonicalDiagnostics(operation.diagnostics)
}
return finalizeOverlayCollection(assembly, prepared, operation)
}
// preparedOverlayCollection contains decisions made against the read-only
// lower policy scope. Incoming declarations remain grouped by destination for
// declaration-only finalization after value application.
type preparedOverlayCollection struct {
declarationsByPath map[string][]sequenceDeclarationSpec
resolutions map[string]collectionPolicyResolution
sequencePolicy sequencePolicy
}
func prepareOverlayCollection(
lowerPolicies collectionPolicyScope,
overlay *document.Node,
sequencePolicy sequencePolicy,
declarations []sequenceDeclarationSpec,
) (*preparedOverlayCollection, []Diagnostic) {
claims := compositionClaims{byPath: make(map[string][]compositionClaim)}
claims.append(overlay, Path{}, 0)
identityFieldsByPath, resolvedPolicies, diagnostics := resolveStrictCollectionPolicies(
claims.ordered, sequencePolicy, result.collectionPolicies, declarations,
claims.ordered, sequencePolicy, lowerPolicies, declarations,
)
if len(diagnostics) != 0 {
return nil, diagnostics
@@ -139,13 +166,13 @@ func mergeOverlayMappingsWithCollectionPolicy(
registered[key] = true
}
var overlayLookup *documentLookup
if len(result.collectionPolicies.collections) != 0 {
if len(lowerPolicies.collections) != 0 {
overlayLookup = buildDocumentLookup(overlay)
}
for key := range result.collectionPolicies.collections {
if node, found := documentAtPathWithLookup(overlay, result.collectionPolicies.collections[key].destination, overlayLookup); found && node != nil {
for key := range lowerPolicies.collections {
if node, found := documentAtPathWithLookup(overlay, lowerPolicies.collections[key].destination, overlayLookup); found && node != nil {
registered[key] = true
registeredPaths[key] = result.collectionPolicies.collections[key].destination.clone()
registeredPaths[key] = lowerPolicies.collections[key].destination.clone()
}
}
@@ -167,34 +194,91 @@ func mergeOverlayMappingsWithCollectionPolicy(
effectiveIdentityFields: cloneStrings(identityFieldsByPath[key]),
}
}
return &preparedOverlayCollection{
declarationsByPath: declarationsByPath,
resolutions: resolutions,
sequencePolicy: sequencePolicy,
}, nil
}
sequenceApplications := make(map[string]sequenceHistoryContribution)
policyValues := make(map[string]bool)
var combinationDiagnostics []Diagnostic
mergeOverlayMappingsWithHooks(result.root, overlay, Path{}, &assembly.index,
recursiveMergeHooks{
added: func(path Path, incoming *document.Node) {
collectOverlayOriginsFrom(result.origins, incomingOrigins, incoming, path, layer)
},
combined: func(path Path, incoming *document.Node) {
mergeOriginContribution(result.origins, path, incomingOrigin(incomingOrigins, path, incoming, layer))
},
replaced: func(path Path, existing, incoming *document.Node) {
assembly.current.collectionPolicies.retireCollectionPoliciesAtOrBelow(path)
recordOverlayReplacement(result, path, existing, incoming, layer, true, incomingOrigins)
},
merge: func(path Path, existing, incoming *document.Node) (*document.Node, bool) {
resolution, active := resolutions[path.String()]
// overlayMergeOperation owns recursive value application and its associated
// origin, policy, diagnostic, and sequence-history evidence.
type overlayMergeOperation struct {
assembly *assemblyAccumulator
result *composedDocument
index *mappingIndex
overlay *document.Node
incomingOrigins map[string]Origin
incomingPolicies collectionPolicyScope
layer Layer
prepared *preparedOverlayCollection
diagnostics []Diagnostic
sequenceApplications map[string]sequenceHistoryContribution
valueDestinations map[string]bool
}
func newOverlayMergeOperation(
assembly *assemblyAccumulator,
overlay *document.Node,
incomingOrigins map[string]Origin,
incomingPolicies collectionPolicyScope,
layer Layer,
prepared *preparedOverlayCollection,
) *overlayMergeOperation {
return &overlayMergeOperation{
assembly: assembly, result: assembly.current, index: &assembly.index, overlay: overlay,
incomingOrigins: incomingOrigins, incomingPolicies: incomingPolicies,
layer: layer, prepared: prepared,
sequenceApplications: make(map[string]sequenceHistoryContribution),
valueDestinations: make(map[string]bool),
}
}
func (operation *overlayMergeOperation) apply() {
mergeOriginContribution(operation.result.origins, Path{}, incomingOrigin(operation.incomingOrigins, Path{}, operation.overlay, operation.layer))
mergeOverlayMappingsWithHooks(
operation.result.root, operation.overlay, Path{}, operation.index,
operation.callbacks(),
)
}
func (operation *overlayMergeOperation) callbacks() recursiveMergeHooks {
return recursiveMergeHooks{
added: operation.added,
combined: operation.combined,
replaced: operation.replaced,
merge: operation.merge,
}
}
func (operation *overlayMergeOperation) added(path Path, incoming *document.Node) {
collectOverlayOriginsFrom(operation.result.origins, operation.incomingOrigins, incoming, path, operation.layer)
}
func (operation *overlayMergeOperation) combined(path Path, incoming *document.Node) {
mergeOriginContribution(operation.result.origins, path, incomingOrigin(operation.incomingOrigins, path, incoming, operation.layer))
}
func (operation *overlayMergeOperation) replaced(path Path, existing, incoming *document.Node) {
operation.result.collectionPolicies.retireCollectionPoliciesAtOrBelow(path)
recordOverlayReplacement(operation.result, path, existing, incoming, operation.layer, true, operation.incomingOrigins)
}
func (operation *overlayMergeOperation) merge(path Path, existing, incoming *document.Node) (*document.Node, bool) {
resolution, active := operation.prepared.resolutions[path.String()]
if !active {
return nil, false
}
combined, issues := combineSequence(existing, incoming, resolution, sequenceCombinationContext{destination: path})
if len(issues) != 0 {
combinationDiagnostics = append(combinationDiagnostics, issues...)
operation.diagnostics = append(operation.diagnostics, issues...)
return existing, false
}
applyOverlaySequenceEvidence(result, path, existing, incoming, combined, resolution, incomingOrigins, incomingPolicies, layer)
policyValues[path.String()] = true
applyOverlaySequenceEvidence(
operation.result, path, existing, incoming, combined, resolution,
operation.incomingOrigins, operation.incomingPolicies, operation.layer,
)
operation.valueDestinations[path.String()] = true
history := sequenceHistoryContribution{path: path.clone(), mode: sequenceHistoryMapped}
if resolution.operation == sequenceModeReplace {
history.mode = sequenceHistoryReplace
@@ -205,27 +289,29 @@ func mergeOverlayMappingsWithCollectionPolicy(
history.items = append([]sequenceItemProjection(nil), combined.higherItems...)
history.replacements = append([]sequenceItemReplacement(nil), combined.replacements...)
}
sequenceApplications[path.String()] = history
operation.sequenceApplications[path.String()] = history
return combined.value, true
},
},
)
if len(combinationDiagnostics) != 0 {
return nil, canonicalDiagnostics(combinationDiagnostics)
}
// Values are now complete. Index lazily for declaration-only checks;
// this lookup is never retained across the value-changing merge above.
func finalizeOverlayCollection(
assembly *assemblyAccumulator,
prepared *preparedOverlayCollection,
operation *overlayMergeOperation,
) ([]sequenceHistoryContribution, []Diagnostic) {
var diagnostics []Diagnostic
// Values are complete. Build the retained lookup lazily only if a
// declaration-only destination needs to be checked.
var retainedLookup *documentLookup
for key, grouped := range declarationsByPath {
if policyValues[key] {
for key, grouped := range prepared.declarationsByPath {
if operation.valueDestinations[key] {
continue
}
path := grouped[0].destination
applicationIdentity, _ := sequencePolicy.identityFieldsAt(path)
current, diagnostics := resolveCollectionPolicy(path, grouped, applicationIdentity, nil)
if len(diagnostics) != 0 {
combinationDiagnostics = append(combinationDiagnostics, diagnostics...)
applicationIdentity, _ := prepared.sequencePolicy.identityFieldsAt(path)
current, issues := resolveCollectionPolicy(path, grouped, applicationIdentity, nil)
if len(issues) != 0 {
diagnostics = append(diagnostics, issues...)
continue
}
lower, found := assembly.current.collectionPolicies.at(path)
@@ -239,28 +325,28 @@ func mergeOverlayMappingsWithCollectionPolicy(
retained, _ := documentAtPathWithLookup(assembly.current.root, path, retainedLookup)
if retained != nil && retained.Kind == document.Sequence && len(current.effectiveIdentityFields) != 0 {
if _, issues := validateSequenceIdentity(retained, path, current.effectiveIdentityFields, sequenceIdentityDiagnosticCodes{invalid: CodeSequenceInvalid, conflict: CodeSequenceConflict}); len(issues) != 0 {
combinationDiagnostics = append(combinationDiagnostics, issues...)
diagnostics = append(diagnostics, issues...)
continue
}
}
policy, issues := collectionPolicyWithoutValue(lowerPolicy, current.policy, retained, path)
if len(issues) != 0 {
combinationDiagnostics = append(combinationDiagnostics, issues...)
diagnostics = append(diagnostics, issues...)
continue
}
assembly.current.collectionPolicies.set(path, policy)
}
if len(combinationDiagnostics) != 0 {
return nil, canonicalDiagnostics(combinationDiagnostics)
if len(diagnostics) != 0 {
return nil, canonicalDiagnostics(diagnostics)
}
keys := make([]string, 0, len(sequenceApplications))
for key := range sequenceApplications {
keys := make([]string, 0, len(operation.sequenceApplications))
for key := range operation.sequenceApplications {
keys = append(keys, key)
}
slices.Sort(keys)
sequences := make([]sequenceHistoryContribution, 0, len(keys))
for _, key := range keys {
sequences = append(sequences, sequenceApplications[key])
sequences = append(sequences, operation.sequenceApplications[key])
}
return sequences, nil
}