Compose registered sequences by identity

This commit is contained in:
2026-10-16 15:25:06 +00:00
parent ff212885c4
commit dc7a70811a
3 changed files with 365 additions and 25 deletions

View File

@@ -6,13 +6,14 @@ import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
// 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
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
@@ -31,16 +32,18 @@ func newAssemblyAccumulator(owned *composedDocument) *assemblyAccumulator {
func cloneComposedDocument(base *composedDocument) *composedDocument {
return &composedDocument{
root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims),
origins: cloneOriginIndex(base.origins), overlays: cloneOverlayRecords(base.overlays),
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
path Path
kind document.Kind
location Location
sourceIndex int
}
type compositionClaims struct {
@@ -48,16 +51,32 @@ type compositionClaims struct {
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 _, input := range inputs {
for sourceIndex, input := range inputs {
if input == nil || input.Kind != document.Mapping {
var location *Location
if input != nil {
@@ -66,7 +85,7 @@ func composeDocuments(inputs ...*document.Node) (*composedDocument, []Diagnostic
}
return nil, []Diagnostic{compositionInvalidDiagnostic("strict composition requires mapping document roots", location)}
}
collected.append(input, Path{})
collected.append(input, Path{}, sourceIndex)
}
checked := make(map[string]struct{}, len(collected.byPath))
@@ -77,16 +96,21 @@ func composeDocuments(inputs ...*document.Node) (*composedDocument, []Diagnostic
}
checked[pointer] = struct{}{}
claims := collected.byPath[pointer]
if len(claims) < 2 || allMappingClaims(claims) {
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 _, input := range inputs[1:] {
mergeDisjointMappings(root, input, index)
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 {
@@ -95,10 +119,11 @@ func composeDocuments(inputs ...*document.Node) (*composedDocument, []Diagnostic
origins := compositionOrigins(collected.ordered)
completeCompositionOrigins(root, Path{}, origins)
return &composedDocument{
root: root,
claims: cloneCompositionClaims(collected.ordered),
origins: origins,
applications: applications,
root: root,
claims: cloneCompositionClaims(collected.ordered),
sequenceRanges: cloneSequenceRanges(ranges),
origins: origins,
applications: applications,
}, nil
}
@@ -121,11 +146,12 @@ func completeCompositionOrigins(node *document.Node, path Path, origins map[stri
}
}
func (c *compositionClaims) append(node *document.Node, path Path) {
func (c *compositionClaims) append(node *document.Node, path Path, sourceIndex int) {
claim := compositionClaim{
path: path.clone(),
kind: node.Kind,
location: publicLocation(node.Location),
path: path.clone(),
kind: node.Kind,
location: publicLocation(node.Location),
sourceIndex: sourceIndex,
}
c.ordered = append(c.ordered, claim)
pointer := path.String()
@@ -135,10 +161,149 @@ func (c *compositionClaims) append(node *document.Node, path Path) {
return
}
for _, entry := range node.Mapping {
c.append(entry.Value, path.Key(entry.Key))
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 {