package engine import ( "maps" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) // composedDocument is the private handoff between source assembly and later // precedence layers. Claims remain ordered by source and declaration traversal; // origins retain every source that contributes to a composite mapping. type composedDocument struct { root *document.Node acquisition []acquisitionScope importGraph importInclusionGraph collectionPolicies collectionPolicyScope claims []compositionClaim sequenceRanges []sequenceRange origins map[string]Origin overlays []overlayRecord applications []historyApplication inputBytes int64 inputNodes int } type sequenceHistoryMode uint8 const ( sequenceHistoryReplace sequenceHistoryMode = iota + 1 sequenceHistoryAppend sequenceHistoryMapped ) type sequenceHistoryContribution struct { path Path mode sequenceHistoryMode offset int length int items []sequenceItemProjection replacements []sequenceItemReplacement } // assemblyAccumulator owns one mutable working document. Acquired trees held by // history applications are separate from its root and remain unchanged. type assemblyAccumulator struct { current *composedDocument index mappingIndex } func newAssemblyAccumulator(owned *composedDocument) *assemblyAccumulator { return &assemblyAccumulator{current: owned} } // cloneComposedDocument detaches mutable composition and provenance state; // history application trees are immutable and retain their existing sharing. func cloneComposedDocument(base *composedDocument) *composedDocument { if base == nil { return nil } return &composedDocument{ root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims), acquisition: cloneAcquisitionScopes(base.acquisition), importGraph: base.importGraph.clone(), collectionPolicies: cloneCollectionPolicyScope(base.collectionPolicies), sequenceRanges: cloneSequenceRanges(base.sequenceRanges), origins: cloneOriginIndex(base.origins), overlays: cloneOverlayRecords(base.overlays), applications: cloneHistoryApplications(base.applications), inputBytes: base.inputBytes, inputNodes: base.inputNodes, } } type compositionClaim struct { node *document.Node // borrowed immutable contribution, not the assembly tree path Path kind document.Kind location Location sourceIndex int } type compositionClaims struct { ordered []compositionClaim byPath map[string][]compositionClaim } // sequenceRange records where one source's sequence items were assigned in the // composed sequence at path. End is exclusive. type sequenceRange struct { sourceIndex int path Path start int end int items []sequenceItemProjection projection sequenceItemProjectionIndex } func (assigned sequenceRange) outputForInput(inputIndex int) (int, bool) { if assigned.projection.outputByInput != nil { return assigned.projection.outputForInput(inputIndex) } if assigned.items != nil { output, found := 0, false for _, item := range assigned.items { if item.inputIndex == inputIndex && inputIndex >= 0 && item.outputIndex >= 0 { output, found = item.outputIndex, true } } return output, found } if inputIndex < 0 || inputIndex >= assigned.end-assigned.start { return 0, false } return assigned.start + inputIndex, true } func (assigned sequenceRange) inputForOutput(outputIndex int) (int, bool) { if assigned.projection.inputByOutput != nil { return assigned.projection.inputForOutput(outputIndex) } if assigned.items != nil { input, found := 0, false for _, item := range assigned.items { if item.outputIndex == outputIndex && outputIndex >= 0 && item.inputIndex >= 0 { input, found = item.inputIndex, true } } return input, found } if outputIndex < assigned.start || outputIndex >= assigned.end { return 0, false } return outputIndex - assigned.start, true } func indexSequenceRange(assigned sequenceRange) sequenceRange { if assigned.items != nil && assigned.projection.outputByInput == nil && assigned.projection.inputByOutput == nil { assigned.projection = newSequenceItemProjectionIndex(assigned.items) } return assigned } // composeDocuments recursively joins disjoint mapping entries. Source order // controls only output traversal and diagnostics: every non-mapping overlap is // rejected before a result is assembled. func composeDocuments(inputs ...*document.Node) (*composedDocument, []Diagnostic) { return composeDocumentsWithPolicy(sequencePolicy{}, inputs...) } // composeDocumentsWithPolicy composes one strict tier using the registered // sequence rules when policy contains registrations. The ordinary adapter above // deliberately preserves the existing unregistered behavior for current callers. func composeDocumentsWithPolicy(policy sequencePolicy, inputs ...*document.Node) (*composedDocument, []Diagnostic) { return composeDocumentsWithCollectionPolicy(policy, collectionPolicyScope{}, nil, inputs...) } // composeDocumentsWithCollectionPolicy resolves collection declarations and // eligible lower policy before strictly composing completed peer documents. func composeDocumentsWithCollectionPolicy( policy sequencePolicy, lower collectionPolicyScope, declarations []sequenceDeclarationSpec, inputs ...*document.Node, ) (*composedDocument, []Diagnostic) { if len(inputs) == 0 { return nil, []Diagnostic{compositionInvalidDiagnostic("strict composition requires at least one document", nil)} } collected := compositionClaims{byPath: make(map[string][]compositionClaim)} for sourceIndex, input := range inputs { if input == nil || input.Kind != document.Mapping { var location *Location if input != nil { converted := publicLocation(input.Location) location = &converted } return nil, []Diagnostic{compositionInvalidDiagnostic("strict composition requires mapping document roots", location)} } collected.append(input, Path{}, sourceIndex) } identityFieldsByPath, collectionPolicies, diagnostics := resolveStrictCollectionPolicies( collected.ordered, policy, lower, declarations, ) if len(diagnostics) != 0 { return nil, diagnostics } checked := make(map[string]struct{}, len(collected.byPath)) for _, claim := range collected.ordered { pointer := claim.path.String() if _, seen := checked[pointer]; seen { continue } checked[pointer] = struct{}{} claims := collected.byPath[pointer] if len(claims) < 2 || allMappingClaims(claims) || (allSequenceClaims(claims) && len(identityFieldsByPath[pointer]) != 0) { continue } return nil, []Diagnostic{compositionConflictDiagnostic(claim.path, claims)} } validated, ranges, diagnostics := validateRegisteredSequences(collected.ordered, identityFieldsByPath) if len(diagnostics) != 0 { return nil, diagnostics } root := document.Clone(inputs[0]) index := new(mappingIndex) for sourceIndex, input := range inputs[1:] { mergeComposedMappings(root, input, Path{}, index, sourceIndex+1, identityFieldsByPath, validated) } applications := make([]historyApplication, 0, len(inputs)) seenSequences := make(map[string]bool) rangesBySource := make([][]sequenceRange, len(inputs)) for _, assigned := range ranges { rangesBySource[assigned.sourceIndex] = append(rangesBySource[assigned.sourceIndex], assigned) } for sourceIndex, input := range inputs { application := historyApplication{layer: LayerBase, value: input} for _, assigned := range rangesBySource[sourceIndex] { pointer := assigned.path.String() mode := sequenceHistoryAppend if !seenSequences[pointer] { mode = sequenceHistoryReplace } seenSequences[pointer] = true application.sequences = append(application.sequences, sequenceHistoryContribution{ path: assigned.path.clone(), mode: mode, offset: assigned.start, length: assigned.end - assigned.start, }) } application.sequencesByPath = indexSequenceHistoryContributions(application.sequences) applications = append(applications, application) } origins := projectedCompositionOrigins(inputs, ranges) completeCompositionOrigins(root, Path{}, origins) return &composedDocument{ root: root, collectionPolicies: collectionPolicies, claims: cloneCompositionClaims(collected.ordered), sequenceRanges: cloneSequenceRanges(ranges), origins: origins, applications: applications, }, nil } // Conflict claims treat sequences atomically, but inspection needs origins for // every descendant. Preserve composite mapping claims while filling those gaps. func completeCompositionOrigins(node *document.Node, path Path, origins map[string]Origin) { pointer := path.String() if _, present := origins[pointer]; !present { origins[pointer] = Origin{Layer: LayerBase, Locations: []Location{publicLocation(node.Location)}} } switch node.Kind { case document.Mapping: for _, entry := range node.Mapping { completeCompositionOrigins(entry.Value, path.Key(entry.Key), origins) } case document.Sequence: for index, child := range node.Sequence { completeCompositionOrigins(child, path.Index(index), origins) } } } func (c *compositionClaims) append(node *document.Node, path Path, sourceIndex int) { claim := compositionClaim{ node: node, path: path.clone(), kind: node.Kind, location: publicLocation(node.Location), sourceIndex: sourceIndex, } c.ordered = append(c.ordered, claim) pointer := path.String() c.byPath[pointer] = append(c.byPath[pointer], claim) if node.Kind != document.Mapping { return } for _, entry := range node.Mapping { c.append(entry.Value, path.Key(entry.Key), sourceIndex) } } func allSequenceClaims(claims []compositionClaim) bool { for _, claim := range claims { if claim.kind != document.Sequence { return false } } return true } type validatedSequence struct { items []*document.Node } func resolveStrictCollectionPolicies( claims []compositionClaim, applicationPolicy sequencePolicy, lower collectionPolicyScope, declarations []sequenceDeclarationSpec, ) (map[string][]string, collectionPolicyScope, []Diagnostic) { paths := make([]Path, 0, len(claims)+len(declarations)) seen := make(map[string]bool, len(claims)+len(declarations)) valuePresent := make(map[string]bool) declarationsByPath := make(map[string][]sequenceDeclarationSpec, len(declarations)) for _, claim := range claims { if len(lower.collections) != 0 { valuePresent[claim.path.String()] = true } if claim.kind != document.Sequence { continue } key := claim.path.String() if !seen[key] { seen[key] = true paths = append(paths, claim.path.clone()) } } for _, declaration := range declarations { key := declaration.destination.String() declarationsByPath[key] = append(declarationsByPath[key], declaration) if !seen[key] { seen[key] = true paths = append(paths, declaration.destination.clone()) } } identityFieldsByPath := make(map[string][]string) effectiveLower := lower if len(lower.collections) != 0 { effectiveLower, _ = lowerCollectionPoliciesForClaims(lower, claims, declarationsByPath, applicationPolicy) } collectionPolicies := cloneCollectionPolicyScope(effectiveLower) var diagnostics []Diagnostic for _, path := range paths { key := path.String() currentDeclarations := declarationsByPath[key] lowerEntry, hasLower := effectiveLower.collections[key] var lowerPolicy *collectionPolicy if hasLower { copy := cloneCollectionPolicy(lowerEntry.policy) lowerPolicy = © } applicationIdentity, _ := applicationPolicy.identityFieldsAt(path) resolution, found := resolveCollectionPolicy(path, currentDeclarations, applicationIdentity, lowerPolicy) if len(found) != 0 { diagnostics = append(diagnostics, found...) continue } if len(resolution.effectiveIdentityFields) != 0 { identityFieldsByPath[key] = cloneStrings(resolution.effectiveIdentityFields) } policyForPath := resolution.policy if hasLower && !valuePresent[key] { // A declaration without a new value changes policy metadata but // leaves retained record ownership untouched. policyForPath = cloneCollectionPolicy(lowerEntry.policy) policyForPath.identityFields = cloneStrings(resolution.policy.identityFields) policyForPath.identityPresent = resolution.policy.identityPresent policyForPath.identityLocation = cloneLocation(resolution.policy.identityLocation) policyForPath.explicitMode = resolution.policy.explicitMode policyForPath.explicitModePresent = resolution.policy.explicitModePresent policyForPath.modeLocation = cloneLocation(resolution.policy.modeLocation) } if collectionPolicies.collections == nil { collectionPolicies.collections = make(map[string]collectionPolicyEntry) } collectionPolicies.collections[key] = collectionPolicyEntry{destination: path.clone(), policy: policyForPath} } return identityFieldsByPath, collectionPolicies, canonicalDiagnostics(diagnostics) } func validateRegisteredSequences(claims []compositionClaim, identityFieldsByPath map[string][]string) (map[string]map[int]validatedSequence, []sequenceRange, []Diagnostic) { itemsByPath := make(map[string]map[int]validatedSequence) identitiesByPath := make(map[string]map[string]document.Location) lengthsByPath := make(map[string]int) var ranges []sequenceRange for _, claim := range claims { if claim.kind != document.Sequence { continue } identityFields := identityFieldsByPath[claim.path.String()] if len(identityFields) == 0 { continue } node := claim.node pathKey := claim.path.String() identities := identitiesByPath[pathKey] if identities == nil { identities = make(map[string]document.Location, len(node.Sequence)) identitiesByPath[pathKey] = identities } start := lengthsByPath[pathKey] contribution := validatedSequence{items: node.Sequence} records, diagnostics := validateNormalizedSequenceIdentityFrom(node, claim.path, identityFields, sequenceIdentityDiagnosticCodes{ invalid: CodeComposeInvalid, conflict: CodeComposeConflict, }, start, sequenceStructuralRecord) if len(diagnostics) != 0 { return nil, nil, diagnostics } byIndex := make([]sequenceIdentityRecord, len(node.Sequence)) for _, record := range records { byIndex[record.index] = record } for _, record := range byIndex { if first, duplicate := identities[record.tuple]; duplicate { current := publicLocation(record.location) return nil, nil, []Diagnostic{{ Code: CodeComposeConflict, Severity: SeverityError, Path: claim.path.clone(), Message: "registered sequence contains duplicate identities", Primary: ptrLocation(publicLocation(first)), Related: []Location{current}, }} } identities[record.tuple] = record.location } if itemsByPath[pathKey] == nil { itemsByPath[pathKey] = make(map[int]validatedSequence) } itemsByPath[pathKey][claim.sourceIndex] = contribution lengthsByPath[pathKey] = start + len(node.Sequence) ranges = append(ranges, sequenceRange{ sourceIndex: claim.sourceIndex, path: claim.path.clone(), start: start, end: start + len(node.Sequence), }) } return itemsByPath, ranges, nil } func mergeComposedMappings(target, incoming *document.Node, path Path, index *mappingIndex, sourceIndex int, identityFieldsByPath map[string][]string, validated map[string]map[int]validatedSequence) { for _, incomingEntry := range incoming.Mapping { entryPath := path.Key(incomingEntry.Key) position := index.find(target, incomingEntry.Key) if position < 0 { cloned := incomingEntry cloned.Value = document.Clone(incomingEntry.Value) index.append(target, cloned) continue } current := target.Mapping[position].Value if current.Kind == document.Sequence && incomingEntry.Value.Kind == document.Sequence && len(identityFieldsByPath[entryPath.String()]) != 0 { contribution := validated[entryPath.String()][sourceIndex] if contribution.items != nil { for _, item := range contribution.items { current.Sequence = append(current.Sequence, document.Clone(item)) } } continue } mergeDisjointMappingsAtPath(current, incomingEntry.Value, entryPath, index, sourceIndex, identityFieldsByPath, validated) } } func mergeDisjointMappingsAtPath(target, incoming *document.Node, path Path, index *mappingIndex, sourceIndex int, identityFieldsByPath map[string][]string, validated map[string]map[int]validatedSequence) { if target.Kind != document.Mapping || incoming.Kind != document.Mapping { return } mergeComposedMappings(target, incoming, path, index, sourceIndex, identityFieldsByPath, validated) } func cloneSequenceRanges(ranges []sequenceRange) []sequenceRange { cloned := make([]sequenceRange, len(ranges)) for i, assigned := range ranges { cloned[i] = assigned cloned[i].path = assigned.path.clone() if assigned.items != nil { cloned[i].items = append([]sequenceItemProjection{}, assigned.items...) } cloned[i].projection.outputByInput = maps.Clone(assigned.projection.outputByInput) cloned[i].projection.inputByOutput = maps.Clone(assigned.projection.inputByOutput) } return cloned } func allMappingClaims(claims []compositionClaim) bool { for _, claim := range claims { if claim.kind != document.Mapping { return false } } return true } func mergeDisjointMappings(target, incoming *document.Node, index *mappingIndex) { for _, incomingEntry := range incoming.Mapping { position := index.find(target, incomingEntry.Key) if position < 0 { cloned := incomingEntry cloned.Value = document.Clone(incomingEntry.Value) index.append(target, cloned) continue } // Claim validation guarantees that the only remaining overlap is a // mapping pair. Empty mappings naturally contribute no child entries. mergeDisjointMappings(target.Mapping[position].Value, incomingEntry.Value, index) } } func documentEntryIndex(entries []document.Entry, key string) int { for index := range entries { if entries[index].Key == key { return index } } return -1 } func projectedCompositionOrigins(inputs []*document.Node, ranges []sequenceRange) map[string]Origin { origins := make(map[string]Origin) sequencesBySource := make([][]sequenceHistoryContribution, len(inputs)) for _, assigned := range ranges { contribution := sequenceHistoryContribution{ path: assigned.path.clone(), mode: sequenceHistoryAppend, offset: assigned.start, length: assigned.end - assigned.start, } if assigned.items != nil { contribution.mode = sequenceHistoryMapped contribution.items = assigned.items } sequencesBySource[assigned.sourceIndex] = append(sequencesBySource[assigned.sourceIndex], contribution) } for sourceIndex, input := range inputs { application := historyApplication{value: input, sequences: sequencesBySource[sourceIndex]} application.sequencesByPath = indexSequenceHistoryContributions(application.sequences) walkHistoryApplication(application, func(_, effective Path, node *document.Node) { pointer := effective.String() origin := origins[pointer] origin.Layer = LayerBase origin.Locations = append(origin.Locations, publicLocation(node.Location)) origins[pointer] = origin }) } return origins } type sequenceRangeKey struct { sourceIndex int path string } type sequenceRangeIndex map[sequenceRangeKey]sequenceRange func indexSequenceRanges(ranges []sequenceRange) sequenceRangeIndex { if len(ranges) == 0 { return nil } index := make(sequenceRangeIndex, len(ranges)) for _, assigned := range ranges { assigned = indexSequenceRange(assigned) index[sequenceRangeKey{sourceIndex: assigned.sourceIndex, path: assigned.path.String()}] = assigned } return index } func applyCompositionOrigins(decoded *decodedConfiguration, origins map[string]Origin) { for pointer, origin := range origins { if _, present := decoded.presence[pointer]; present { decoded.origins[pointer] = cloneOrigin(origin) } } } func cloneCompositionClaims(claims []compositionClaim) []compositionClaim { cloned := make([]compositionClaim, len(claims)) for index, claim := range claims { cloned[index] = claim cloned[index].path = claim.path.clone() } return cloned } func compositionInvalidDiagnostic(message string, location *Location) Diagnostic { return Diagnostic{ Code: CodeComposeInvalid, Severity: SeverityError, Message: message, Primary: cloneLocation(location), } } func compositionConflictDiagnostic(path Path, claims []compositionClaim) Diagnostic { primary := claims[0].location related := make([]Location, 0, len(claims)-1) for _, claim := range claims[1:] { related = append(related, claim.location) } return Diagnostic{ Code: CodeComposeConflict, Severity: SeverityError, Path: path.clone(), Message: "configuration path is defined by multiple sources", Primary: &primary, Related: related, } } func publicLocation(location document.Location) Location { return Location{SourceID: location.SourceID, Line: location.Line, Column: location.Column} }