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 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), 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 } type compositionClaims struct { ordered []compositionClaim byPath map[string][]compositionClaim } // 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) { if len(inputs) == 0 { return nil, []Diagnostic{compositionInvalidDiagnostic("strict composition requires at least one document", nil)} } collected := compositionClaims{byPath: make(map[string][]compositionClaim)} for _, 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{}) } 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) { continue } return nil, []Diagnostic{compositionConflictDiagnostic(claim.path, claims)} } root := document.Clone(inputs[0]) index := new(mappingIndex) for _, input := range inputs[1:] { mergeDisjointMappings(root, input, index) } applications := make([]historyApplication, 0, len(inputs)) for _, input := range inputs { applications = append(applications, historyApplication{layer: LayerBase, value: input}) } return &composedDocument{ root: root, claims: cloneCompositionClaims(collected.ordered), origins: compositionOrigins(collected.ordered), applications: applications, }, nil } func (c *compositionClaims) append(node *document.Node, path Path) { claim := compositionClaim{ path: path.clone(), kind: node.Kind, location: publicLocation(node.Location), } 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)) } } 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} }