package engine import "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 claims []compositionClaim sequenceRanges []sequenceRange origins map[string]Origin overlays []overlayRecord applications []historyApplication inputBytes int64 inputNodes int } // 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 { return &composedDocument{ root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims), 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 { 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 } // 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) { 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) } 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) && policy.registered(claim.path)) { continue } return nil, []Diagnostic{compositionConflictDiagnostic(claim.path, claims)} } validated, ranges, diagnostics := validateRegisteredSequences(inputs, collected.ordered, policy) 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, policy, validated) } applications := make([]historyApplication, 0, len(inputs)) for _, input := range inputs { applications = append(applications, historyApplication{layer: LayerBase, value: input}) } origins := compositionOrigins(collected.ordered) completeCompositionOrigins(root, Path{}, origins) return &composedDocument{ root: root, 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{ 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 validateRegisteredSequences(inputs []*document.Node, claims []compositionClaim, policy sequencePolicy) (map[string]map[int]validatedSequence, []sequenceRange, []Diagnostic) { itemsByPath := make(map[string]map[int]validatedSequence) identitiesByPath := make(map[string]map[string]Location) lengthsByPath := make(map[string]int) var ranges []sequenceRange for _, claim := range claims { if claim.kind != document.Sequence { continue } identityField, registered := policy.identityFields[claim.path.String()] if !registered { continue } node, ok := documentAtPath(inputs[claim.sourceIndex], claim.path) if !ok || node.Kind != document.Sequence { continue } pathKey := claim.path.String() identities := identitiesByPath[pathKey] if identities == nil { identities = make(map[string]Location, len(node.Sequence)) identitiesByPath[pathKey] = identities } start := lengthsByPath[pathKey] contribution := validatedSequence{items: node.Sequence} for itemIndex, item := range node.Sequence { itemPath := claim.path.Index(itemIndex) if item == nil || item.Kind != document.Mapping { location := (*Location)(nil) if item != nil { converted := publicLocation(item.Location) location = &converted } return nil, nil, []Diagnostic{{ Code: CodeComposeInvalid, Severity: SeverityError, Path: itemPath, Message: "registered sequence items must be mappings", Primary: location, }} } fieldIndex := documentEntryIndex(item.Mapping, identityField) if fieldIndex < 0 { location := publicLocation(item.Location) return nil, nil, []Diagnostic{{ Code: CodeComposeInvalid, Severity: SeverityError, Path: itemPath, Message: "registered sequence item is missing its identity field", Primary: &location, }} } field := item.Mapping[fieldIndex].Value if field == nil || field.Kind != document.String { location := publicLocation(item.Mapping[fieldIndex].ValueLocation) if field != nil { location = publicLocation(field.Location) } return nil, nil, []Diagnostic{{ Code: CodeComposeInvalid, Severity: SeverityError, Path: itemPath.Key(identityField), Message: "registered sequence identity must be a string", Primary: &location, }} } if first, duplicate := identities[field.Text]; duplicate { current := publicLocation(field.Location) return nil, nil, []Diagnostic{{ Code: CodeComposeConflict, Severity: SeverityError, Path: claim.path.clone(), Message: "registered sequence contains duplicate identities", Primary: &first, Related: []Location{current}, }} } identities[field.Text] = publicLocation(field.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, policy sequencePolicy, 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 && policy.registered(entryPath) { 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, policy, validated) } } func mergeDisjointMappingsAtPath(target, incoming *document.Node, path Path, index *mappingIndex, sourceIndex int, policy sequencePolicy, validated map[string]map[int]validatedSequence) { if target.Kind != document.Mapping || incoming.Kind != document.Mapping { return } mergeComposedMappings(target, incoming, path, index, sourceIndex, policy, validated) } func (p sequencePolicy) registered(path Path) bool { _, ok := p.identityFields[path.String()] return ok } func cloneSequenceRanges(ranges []sequenceRange) []sequenceRange { cloned := make([]sequenceRange, len(ranges)) for i, assigned := range ranges { cloned[i] = assigned cloned[i].path = assigned.path.clone() } 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 compositionOrigins(claims []compositionClaim) map[string]Origin { origins := make(map[string]Origin, len(claims)) for _, claim := range claims { pointer := claim.path.String() origin := origins[pointer] origin.Layer = LayerBase origin.Locations = append(origin.Locations, claim.location) origins[pointer] = origin } return origins } 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} }