2026-09-12 15:58:06 +00:00
package engine
2026-09-29 17:52:20 +00:00
import (
"strconv"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
2026-09-12 15:58:06 +00:00
// assembledInput is the source/document handoff across schema and version checks.
type assembledInput struct {
2026-10-15 15:33:40 +00:00
parsed parsedSource
document * document . Node
composition * composedDocument
2026-10-16 15:48:36 +00:00
acquisition [ ] acquisitionScope
2026-10-15 15:33:40 +00:00
inheritance * inheritanceScope
sources [ ] SourceInfo
contexts map [ string ] ResourceContext
versionState * sourceVersionState
2026-10-23 18:15:01 +00:00
accounting * importAccounting
2026-09-12 15:58:06 +00:00
}
2026-10-16 15:48:36 +00:00
// acquiredContribution keeps a source-local payload beside its placed document.
// The latter is used for composition; the former preserves the acquisition
// boundary for later scoped assembly and evidence.
type acquiredContribution struct {
2026-10-23 18:55:03 +00:00
source SourceInfo
identity importNodeKey
role importContributionRole
scopeOwner importNodeKey
payload * document . Node
document * document . Node
destination Path
relativeDestination Path
processing * sourceVersionProcessingResult
sequenceDeclarations [ ] sequenceDeclarationSpec
nested * composedDocument
pendingItem * acquisitionScope
placementCharged bool
2026-10-16 15:48:36 +00:00
}
// acquisitionScope is the ordered strict mapping scope produced by one input
// and its ordinary imports.
type acquisitionScope struct {
2026-10-23 14:16:22 +00:00
owner importNodeKey
destination Path
contributions [ ] acquiredContribution
collectionPolicies collectionPolicyScope
2026-10-16 15:48:36 +00:00
}
func cloneAcquisitionScope ( scope acquisitionScope ) acquisitionScope {
2026-10-23 14:16:22 +00:00
cloned := acquisitionScope {
owner : scope . owner , destination : scope . destination . clone ( ) ,
collectionPolicies : cloneCollectionPolicyScope ( scope . collectionPolicies ) ,
}
2026-10-16 15:48:36 +00:00
if scope . contributions != nil {
cloned . contributions = make ( [ ] acquiredContribution , len ( scope . contributions ) )
for index , contribution := range scope . contributions {
cloned . contributions [ index ] = acquiredContribution {
2026-10-19 17:34:21 +00:00
source : cloneSourceInfo ( contribution . source ) , identity : contribution . identity ,
role : contribution . role , scopeOwner : contribution . scopeOwner , payload : contribution . payload ,
2026-10-16 15:48:36 +00:00
document : contribution . document , destination : contribution . destination . clone ( ) ,
relativeDestination : contribution . relativeDestination . clone ( ) , processing : contribution . processing ,
2026-10-23 18:55:03 +00:00
sequenceDeclarations : cloneSequenceDeclarations ( contribution . sequenceDeclarations ) ,
nested : cloneComposedDocument ( contribution . nested ) , pendingItem : cloneAcquisitionScopePointer ( contribution . pendingItem ) ,
2026-10-19 19:53:01 +00:00
placementCharged : contribution . placementCharged ,
2026-10-16 15:48:36 +00:00
}
}
}
return cloned
}
2026-10-19 17:34:21 +00:00
func cloneAcquisitionScopePointer ( scope * acquisitionScope ) * acquisitionScope {
if scope == nil {
return nil
}
cloned := cloneAcquisitionScope ( * scope )
return & cloned
}
2026-10-16 15:48:36 +00:00
func cloneAcquisitionScopes ( scopes [ ] acquisitionScope ) [ ] acquisitionScope {
if scopes == nil {
return nil
}
cloned := make ( [ ] acquisitionScope , len ( scopes ) )
for index , scope := range scopes {
cloned [ index ] = cloneAcquisitionScope ( scope )
}
return cloned
}
2026-09-12 15:58:06 +00:00
func assembleBaseInput [ T any ] ( source Source , options Options [ T ] ) ( assembledInput , [ ] Diagnostic ) {
2026-10-14 13:14:06 +00:00
return assembleBaseInputTracked ( source , options , Path { } , & importAccounting { } , nil , nil )
2026-09-29 17:52:20 +00:00
}
2026-10-14 13:14:06 +00:00
func assembleBaseInputTracked [ T any ] ( source Source , options Options [ T ] , destination Path , accounting * importAccounting , visited map [ resourceIdentity ] importRoute , hostToken * namespaceToken ) ( assembledInput , [ ] Diagnostic ) {
2026-10-15 15:33:40 +00:00
return assembleBaseInputTrackedWithVersion ( source , options , destination , accounting , visited , hostToken , nil )
2026-10-15 15:24:53 +00:00
}
2026-10-15 15:33:40 +00:00
func assembleBaseInputTrackedWithVersion [ T any ] ( source Source , options Options [ T ] , destination Path , accounting * importAccounting , visited map [ resourceIdentity ] importRoute , hostToken * namespaceToken , versionState * sourceVersionState ) ( assembledInput , [ ] Diagnostic ) {
2026-10-15 15:40:11 +00:00
return assembleBaseInputTrackedForRole ( source , options , destination , accounting , visited , hostToken , versionState , sourceVersionMain )
}
func assembleBaseInputTrackedForRole [ T any ] ( source Source , options Options [ T ] , destination Path , accounting * importAccounting , visited map [ resourceIdentity ] importRoute , hostToken * namespaceToken , versionState * sourceVersionState , role sourceVersionRole ) ( assembledInput , [ ] Diagnostic ) {
2026-09-29 17:52:20 +00:00
limits := defaultImportLimits ( )
parseLimits := defaultParseLimits ( )
parseLimits . source . remaining = limits . bytes - accounting . bytes
parsed , diagnostics := parseSource ( source , "source-" + strconv . Itoa ( accounting . sources + 1 ) , parseLimits )
2026-10-14 17:21:07 +00:00
if parsed . source != nil {
parsed . source . Destination = destination . clone ( )
2026-10-16 15:55:29 +00:00
parsed . source . placementMode = options . placementMode
2026-10-14 17:21:07 +00:00
}
2026-10-23 18:15:01 +00:00
input := assembledInput { parsed : parsed , document : parsed . document , sources : sourceInfos ( parsed . source ) , versionState : versionState , accounting : accounting }
2026-09-12 15:58:06 +00:00
if len ( diagnostics ) != 0 {
input . sources = sourceInfos ( parsed . source )
2026-10-14 15:49:51 +00:00
// Mapping value issues already identify application content. Other early
// failures describe source syntax or access, not a placed value.
if source . kind == sourceMapping && source . mapIssue != nil && len ( destination . segments ) != 0 {
for index := range diagnostics {
relative := diagnostics [ index ] . Path
segments := clonePathSegments ( destination . segments , len ( relative . segments ) )
diagnostics [ index ] . Path = Path { segments : append ( segments , relative . segments ... ) }
}
}
2026-09-12 15:58:06 +00:00
return input , diagnostics
}
2026-10-15 22:18:57 +00:00
if ! options . Imports && parsed . document != nil {
if diagnostics := disabledDirectiveDiagnostics ( parsed . document , Path { } ) ; len ( diagnostics ) != 0 {
return input , diagnostics
}
}
2026-10-15 15:40:11 +00:00
var versionProcessing * sourceVersionProcessingResult
2026-10-15 15:33:40 +00:00
if versionState != nil && versionState . mode != sourceVersionModeDefault {
var sourceID string
if parsed . source != nil {
sourceID = parsed . source . ID
2026-10-15 15:24:53 +00:00
}
2026-10-16 19:11:08 +00:00
processed , failures := versionState . process ( parsed . document , role , destination , sourceID , options . placementMode )
2026-10-15 15:33:40 +00:00
if len ( failures ) != 0 {
return input , failures
}
2026-10-15 15:40:11 +00:00
versionProcessing = & processed
if role == sourceVersionMain {
versionState . mainProcessing = & processed
}
2026-10-15 15:24:53 +00:00
}
2026-09-12 15:58:06 +00:00
if options . Imports {
2026-10-19 19:53:01 +00:00
if options . deferAssembly {
input . composition , input . sources , diagnostics = loadImportedDocumentsTrackedWithProcessingDeferred ( source , options . Filesystem , parsed , destination , options . Definition . Version , limits , accounting , visited , hostToken , versionState , versionProcessing , options . sequencePolicy , true )
} else {
input . composition , input . sources , diagnostics = loadImportedDocumentsTrackedWithProcessing ( source , options . Filesystem , parsed , destination , options . Definition . Version , limits , accounting , visited , hostToken , versionState , versionProcessing , options . sequencePolicy )
}
2026-09-12 15:58:06 +00:00
if len ( diagnostics ) != 0 {
return input , diagnostics
}
2026-10-19 19:53:01 +00:00
if input . composition != nil {
input . acquisition = cloneAcquisitionScopes ( input . composition . acquisition )
input . document = input . composition . root
}
2026-09-12 15:58:06 +00:00
} else {
input . sources = sourceInfos ( parsed . source )
2026-10-15 15:40:11 +00:00
if versionProcessing != nil {
input . document = versionProcessing . payload
}
2026-09-23 18:27:29 +00:00
if options . Filesystem != nil && ! activeOverlay ( options . Overlay ) {
2026-09-12 15:58:06 +00:00
if diagnostic := validateFilesystemContext ( source , options . Filesystem , parsed ) ; diagnostic != nil {
return input , [ ] Diagnostic { * diagnostic }
}
}
}
if ! options . Imports && input . document != nil {
2026-10-16 15:55:29 +00:00
_ , placed , err := placeDocumentWithMode ( input . document , destination , options . placementMode , input . document . Location , parseLimits . document . Depth , parseLimits . document . Nodes )
2026-10-14 13:14:06 +00:00
if err != nil {
return input , [ ] Diagnostic { sourceDiagnostic ( CodeSourceLimitExceeded , parsed . source . ID , "configuration exceeds the source depth or node limit after destination placement" ) }
}
2026-10-16 15:48:36 +00:00
localPayload := input . document
2026-10-14 13:14:06 +00:00
input . document = placed
2026-10-19 17:34:21 +00:00
identity := importKeyForMainSource ( parsed . source )
2026-10-16 15:48:36 +00:00
input . acquisition = [ ] acquisitionScope { {
2026-10-19 17:34:21 +00:00
owner : identity ,
2026-10-16 15:48:36 +00:00
destination : destination . clone ( ) ,
contributions : [ ] acquiredContribution { {
source : cloneSourceInfo ( * parsed . source ) , payload : localPayload ,
2026-10-19 17:34:21 +00:00
identity : identity , role : importRoleMain , scopeOwner : identity ,
2026-10-16 15:48:36 +00:00
document : placed , destination : destination . clone ( ) , processing : versionProcessing ,
} } ,
} }
2026-10-14 13:31:32 +00:00
rawNodes := countInputNodes ( parsed . document )
2026-10-16 15:55:29 +00:00
placementNodes := destinationWrapperNodeCountForMode ( destination , options . placementMode )
2026-10-14 13:31:32 +00:00
if accounting . sources + 1 > limits . sources || accounting . bytes + parsed . bytes > limits . bytes || accounting . nodes + rawNodes + placementNodes > limits . nodes {
2026-09-29 17:52:20 +00:00
return input , [ ] Diagnostic { sourceDiagnostic ( CodeSourceLimitExceeded , parsed . source . ID , "configuration exceeds the aggregate input limit" ) }
}
if diagnostic := registerAcquiredSource ( source , options . Filesystem , parsed . source , visited , hostToken ) ; diagnostic != nil {
return input , [ ] Diagnostic { * diagnostic }
}
accounting . sources ++
accounting . bytes += parsed . bytes
2026-10-14 13:31:32 +00:00
accounting . nodes += rawNodes + placementNodes
2026-09-12 15:58:06 +00:00
}
2026-09-29 18:08:27 +00:00
if ! activeOverlay ( options . Overlay ) || options . Imports {
contexts , diagnostic := contextsForAcquiredSources ( source , options . Filesystem , parsed . source , input . sources )
if diagnostic != nil {
return input , [ ] Diagnostic { * diagnostic }
}
input . contexts = contexts
}
2026-09-12 15:58:06 +00:00
return input , nil
}
2026-09-29 17:52:20 +00:00
func registerAcquiredSource ( source Source , context * FilesystemContext , info * SourceInfo , visited map [ resourceIdentity ] importRoute , hostToken * namespaceToken ) * Diagnostic {
if visited == nil || info == nil || info . Path == "" {
return nil
}
identity := resourceIdentity { namespace : hostToken , path : info . Path }
if source . kind == sourceFilesystem {
identity . namespace = source . namespace . token
} else if context != nil && context . FS != nil {
identity . namespace = context . FS . namespace . token
}
if previous , repeated := visited [ identity ] ; repeated {
value := importRepeatedDiagnostic ( Location { SourceID : info . ID } , previous , importRoute { } )
return & value
}
visited [ identity ] = importRoute { canonical : info . Path }
return nil
}
2026-09-12 15:58:06 +00:00
func applyInputLayers [ T any ] ( source Source , options Options [ T ] , input assembledInput ) ( assembledInput , [ ] Diagnostic ) {
2026-09-23 18:27:29 +00:00
if ! activeOverlay ( options . Overlay ) && len ( options . Overrides ) == 0 {
2026-09-12 15:58:06 +00:00
return input , nil
}
2026-10-16 18:59:52 +00:00
composition , diagnostics := compositionForInputWithPolicy ( input , options . sequencePolicy )
2026-09-12 15:58:06 +00:00
if len ( diagnostics ) != 0 {
return input , diagnostics
}
2026-09-29 17:13:14 +00:00
assembly := newAssemblyAccumulator ( composition )
2026-09-23 18:27:29 +00:00
if activeOverlay ( options . Overlay ) {
2026-10-15 15:44:04 +00:00
input . sources , diagnostics = loadSelectedOverlayIntoWithVersion (
2026-09-29 17:13:14 +00:00
source , options . Filesystem , input . parsed , assembly , input . sources ,
2026-10-15 15:44:04 +00:00
options . Overlay , options . Definition . Version , defaultImportLimits ( ) , input . versionState ,
2026-09-12 15:58:06 +00:00
)
if len ( diagnostics ) != 0 {
return input , diagnostics
}
2026-09-29 18:08:27 +00:00
contexts , diagnostic := contextsForAcquiredSources ( source , options . Filesystem , input . parsed . source , input . sources [ len ( input . sources ) - 1 : ] )
if diagnostic != nil {
return input , [ ] Diagnostic { * diagnostic }
}
mergeResourceContexts ( & input . contexts , contexts )
2026-09-12 15:58:06 +00:00
}
if len ( options . Overrides ) != 0 {
2026-09-29 17:13:14 +00:00
input . sources , diagnostics = assembly . applyOverrides (
input . sources , options . Overrides , options . Definition . Version , defaultImportLimits ( ) ,
2026-09-12 15:58:06 +00:00
)
if len ( diagnostics ) != 0 {
return input , diagnostics
}
}
2026-09-29 17:13:14 +00:00
input . composition = assembly . current
input . document = assembly . current . root
2026-09-12 15:58:06 +00:00
return input , nil
}
2026-09-23 18:27:29 +00:00
// compositionForInput initializes one handoff for overlays and CLI, including
2026-09-12 15:58:06 +00:00
// defaults-only CLI input. Raw source bytes and mapping keys retain their budgets.
func compositionForInput ( input assembledInput ) ( * composedDocument , [ ] Diagnostic ) {
2026-10-16 18:59:52 +00:00
return compositionForInputWithPolicy ( input , sequencePolicy { } )
}
func compositionForInputWithPolicy ( input assembledInput , policy sequencePolicy ) ( * composedDocument , [ ] Diagnostic ) {
2026-09-12 15:58:06 +00:00
if input . composition != nil {
2026-10-23 18:15:01 +00:00
if input . composition . root == nil && len ( input . composition . acquisition ) != 0 {
return composeAcquiredInputWithPolicy ( input , policy )
}
2026-09-12 15:58:06 +00:00
return input . composition , nil
}
if input . document == nil {
return & composedDocument { root : & document . Node { Kind : document . Mapping } , origins : make ( map [ string ] Origin ) } , nil
}
2026-10-16 18:59:52 +00:00
composition , diagnostics := composeDocumentsWithPolicy ( policy , input . document )
2026-09-12 15:58:06 +00:00
if len ( diagnostics ) != 0 {
return nil , diagnostics
}
composition . inputBytes = input . parsed . bytes
composition . inputNodes = countInputNodes ( input . document )
2026-10-16 15:48:36 +00:00
composition . acquisition = cloneAcquisitionScopes ( input . acquisition )
2026-09-12 15:58:06 +00:00
return composition , nil
}