2026-09-12 15:58:06 +00:00
package engine
import (
"os"
"reflect"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
// Definition contains reusable application rules for a configuration type.
// Secret is a pure predicate that can add sensitivity to semantic paths.
// Definitions should be treated as read-only after being shared.
type Definition [ T any ] struct {
Defaults func ( ) T
Resolve func ( * T , * Resolution ) error
Validate func ( * T , * Validation )
Version * VersionRule
Secret func ( Path ) bool
2026-10-16 15:19:32 +00:00
2026-10-23 20:59:23 +00:00
sequenceIdentityRegistrations [ ] sequenceIdentityRegistration
2026-09-12 15:58:06 +00:00
}
// Options contains the application definition and per-load input selections.
2026-10-24 15:55:10 +00:00
// Imports enables traversal of declarations in a selected main document; an
// absent source has no main declarations to traverse. Overlay selects one fixed
2026-10-24 15:59:39 +00:00
// overlay and requires filesystem context for NoSource. Overrides contain only
// values explicitly supplied by an application CLI.
2026-09-12 15:58:06 +00:00
type Options [ T any ] struct {
2026-10-25 12:52:50 +00:00
Definition Definition [ T ]
Imports bool
Overlay string
Filesystem * FilesystemContext
Overrides [ ] Override
SourceVersionMode sourceVersionMode
2026-10-26 00:10:07 +00:00
ImportDefaults ImportDefaults
2026-10-25 12:52:50 +00:00
destination Path
placementMode placementMode
2026-09-12 15:58:06 +00:00
}
2026-10-24 15:55:10 +00:00
// FilesystemContext supplies filesystem roots for imports and overlays. For an
// absent host input, Root is required and BaseDir may be omitted to use Root as
// the import base. Native contexts still require both BaseDir and Root.
2026-09-12 15:58:06 +00:00
type FilesystemContext struct {
BaseDir string
Root string
2026-09-29 17:36:20 +00:00
FS * Filesystem
2026-09-12 15:58:06 +00:00
}
// Override describes one explicitly supplied application override. Value uses
// parser-neutral built-in scalars, nil, []any, or map[string]any; Flag is a safe
// display name and must not contain a raw argument or value.
type Override struct {
Path Path
Value any
Flag string
}
// Resolution exposes entry-state input provenance, surviving-input presence, and
// audited application resolution changes. A resolver must not retain or
// concurrently use the context.
type Resolution struct {
state * resolutionState
}
// VersionRule declares the input path and application checker for a schema version.
// At that path, string and decimal-integer input is supplied as Version.Text.
type VersionRule struct {
Path Path
Check func ( Version , * Validation ) error
}
// Version preserves the absent, null, and textual states of a schema-version input.
// Text is the exact spelling of accepted string or decimal-integer input.
type Version struct {
Present bool
Null bool
Text string
}
2026-09-12 16:12:01 +00:00
// Loaded is a short-lived generic carrier for a successful configuration load.
// Inspection is frozen independently of Config and does not retain application values.
type Loaded [ T any ] struct {
Config T
Inspection * Inspection
}
2026-09-12 15:58:06 +00:00
2026-09-12 16:12:01 +00:00
// Inspection owns frozen load snapshots and detached query state.
type Inspection struct {
2026-09-12 15:58:06 +00:00
metadata Metadata
sources [ ] SourceInfo
2026-09-29 18:08:27 +00:00
contexts map [ string ] ResourceContext
2026-09-12 15:58:06 +00:00
diagnostics [ ] Diagnostic
baseline * document . Node
input * document . Node
inputOrigins map [ string ] Origin
2026-10-23 21:20:19 +00:00
preCLI * document . Node
preCLIOrigins map [ string ] Origin
postDecodeApps [ ] historyApplication
2026-09-12 15:58:06 +00:00
final * document . Node
2026-10-09 03:54:14 +00:00
immutableLookup * documentLookup
2026-09-12 15:58:06 +00:00
resolution [ ] resolutionSnapshot
2026-10-09 03:49:43 +00:00
resolutionReplay [ ] resolutionSnapshot
2026-09-12 15:58:06 +00:00
applications [ ] historyApplication
2026-09-29 22:52:51 +00:00
inheritance [ ] inheritanceOperation
2026-09-12 15:58:06 +00:00
secrets map [ string ] bool
secretReferences map [ string ] string
}
// Metadata returns detached effective presence, origin, and history records.
2026-09-12 16:12:01 +00:00
func ( r * Inspection ) Metadata ( ) Metadata {
2026-09-12 15:58:06 +00:00
if r == nil {
return Metadata { }
}
return cloneMetadata ( r . metadata )
}
// Sources returns a detached list of sources used by the load.
2026-09-12 16:12:01 +00:00
func ( r * Inspection ) Sources ( ) [ ] SourceInfo {
2026-09-12 15:58:06 +00:00
if r == nil {
return nil
}
return cloneSourceInfos ( r . sources )
}
2026-09-29 18:08:27 +00:00
// SourceContext returns a passive filesystem view for an acquired source.
// Unknown IDs and sources without a filesystem context return false.
func ( r * Inspection ) SourceContext ( id string ) ( ResourceContext , bool ) {
if r == nil {
return ResourceContext { } , false
}
context , ok := r . contexts [ id ]
return context , ok
}
2026-09-12 15:58:06 +00:00
// Diagnostics returns detached nonfatal diagnostics from the load.
2026-09-12 16:12:01 +00:00
func ( r * Inspection ) Diagnostics ( ) [ ] Diagnostic {
2026-09-12 15:58:06 +00:00
if r == nil {
return nil
}
return cloneDiagnostics ( r . diagnostics )
}
// Load parses and strictly decodes zero or one selected source, explicitly enabled
2026-09-23 18:27:29 +00:00
// imports, and one selected overlay. It accepts at most one Options value and
2026-09-12 15:58:06 +00:00
// returns no result when any error is reported.
2026-09-12 16:12:01 +00:00
func Load [ T any ] ( source Source , secrets SecretBinding , supplied ... Options [ T ] ) ( * Loaded [ T ] , error ) {
2026-09-12 16:04:31 +00:00
return loadWithSecretLookup ( source , secrets , os . LookupEnv , supplied ... )
2026-09-12 15:58:06 +00:00
}
2026-09-12 16:12:01 +00:00
func loadWithSecretLookup [ T any ] ( source Source , secrets SecretBinding , lookup secretLookup , supplied ... Options [ T ] ) ( * Loaded [ T ] , error ) {
2026-10-25 22:04:54 +00:00
return loadWithSecretLookupLimit ( source , secrets , lookup , historyNodeLimit , supplied ... )
}
func loadWithSecretLookupLimit [ T any ] ( source Source , secrets SecretBinding , lookup secretLookup , historyLimit int , supplied ... Options [ T ] ) ( * Loaded [ T ] , error ) {
2026-10-25 12:52:50 +00:00
if len ( supplied ) > 1 {
return nil , ( & diagnosticAccumulator { diagnostics : [ ] Diagnostic { optionDiagnostic ( CodeOptionsInvalid , "Load accepts at most one options value" ) } } ) . loadError ( )
}
var options Options [ T ]
if len ( supplied ) == 1 {
options = supplied [ 0 ]
}
input , diagnostics := normalizeInput ( loadInput {
source : source , imports : options . Imports , filesystem : options . Filesystem ,
destination : options . destination . clone ( ) , placementMode : options . placementMode ,
} , options . Overlay )
2026-10-26 00:10:07 +00:00
defaults , policy , settingsDiagnostics := loadSettings ( options . ImportDefaults , options . Definition , options . SourceVersionMode )
2026-10-25 12:52:50 +00:00
diagnostics = append ( diagnostics , settingsDiagnostics ... )
diagnostics = append ( diagnostics , overlayOptionDiagnostics ( options . Overlay ) ... )
2026-09-12 15:58:06 +00:00
if len ( diagnostics ) != 0 {
2026-09-29 17:52:20 +00:00
return nil , ( & diagnosticAccumulator { diagnostics : diagnostics } ) . loadError ( )
2026-09-12 15:58:06 +00:00
}
2026-10-16 15:19:32 +00:00
definition := options . Definition
2026-10-23 20:59:23 +00:00
definition . sequenceIdentityRegistrations = nil
2026-09-29 17:52:20 +00:00
request := loadRequest [ T ] {
2026-10-25 12:52:50 +00:00
main : input ,
definition : definition , importDefaults : defaults , sequencePolicy : policy , overlay : options . Overlay ,
2026-09-29 17:52:20 +00:00
overrides : options . Overrides , ordinary : true ,
2026-10-15 15:24:53 +00:00
sourceVersionMode : options . SourceVersionMode ,
2026-10-25 12:52:50 +00:00
effectiveVersion : effectiveSourceVersionRuleForPlacement ( options . Definition . Version , input . destination , options . SourceVersionMode , input . placementMode ) ,
2026-09-29 17:52:20 +00:00
}
2026-10-25 22:04:54 +00:00
return executeLoadRequest ( request , secrets , lookup , historyLimit )
2026-09-29 17:52:20 +00:00
}
2026-09-12 15:58:06 +00:00
2026-10-25 22:04:54 +00:00
func executeLoadRequest [ T any ] ( request loadRequest [ T ] , secrets SecretBinding , lookup secretLookup , historyLimit int ) ( * Loaded [ T ] , error ) {
2026-09-29 17:52:20 +00:00
accumulator := diagnosticAccumulator { }
accounting := & importAccounting { }
2026-10-25 13:07:16 +00:00
policy := acquisitionPolicy {
importDefaults : snapshotImportDefaults ( request . importDefaults ) ,
sequencePolicy : request . sequencePolicy ,
importLimits : defaultImportLimits ( ) ,
2026-09-29 17:52:20 +00:00
}
2026-10-15 15:24:53 +00:00
var schema * schemaDescriptor
if request . sourceVersionMode != sourceVersionModeDefault {
var schemaIssue * schemaIssue
schema , schemaIssue = buildSchema ( reflect . TypeFor [ T ] ( ) , secrets )
if schemaIssue != nil {
accumulator . add ( diagnosticFromSchemaIssue ( schemaIssue ) )
return nil , accumulator . loadError ( )
}
}
2026-10-15 15:33:40 +00:00
versionState := & sourceVersionState {
localRule : request . definition . Version , effectiveRule : request . protectionVersion ( ) , mode : request . sourceVersionMode ,
}
2026-10-25 13:07:16 +00:00
state := & acquisitionState { accounting : accounting , versionState : versionState }
2026-10-15 16:12:21 +00:00
accumulator . versionState = versionState
2026-10-25 13:07:16 +00:00
directiveContext := rootDirectiveContextLayered
ordinaryOverlay := ""
if request . ordinary {
directiveContext = rootDirectiveContextSource
ordinaryOverlay = request . overlay
}
input , diagnostics := acquireInput ( inputAcquisitionRequest {
input : request . main , role : sourceVersionMain , directiveContext : directiveContext ,
2026-10-25 22:28:35 +00:00
protectionRule : request . protectionVersion ( ) ,
2026-10-25 13:07:16 +00:00
ordinaryOverlay : ordinaryOverlay ,
} , policy , state , & tierAcquisitionState { visited : make ( map [ resourceIdentity ] importRoute ) } )
2026-10-24 16:11:03 +00:00
request . main . source = input . source
2026-09-12 15:58:06 +00:00
accumulator . sources = input . sources
2026-10-15 15:33:40 +00:00
accumulator . add ( versionState . diagnostics ... )
2026-10-15 15:40:11 +00:00
reportedVersionDiagnostics := len ( versionState . diagnostics )
2026-09-12 15:58:06 +00:00
accumulator . add ( diagnostics ... )
2026-10-15 15:24:53 +00:00
if hasErrors ( diagnostics ) {
2026-09-12 15:58:06 +00:00
return nil , accumulator . loadError ( )
}
2026-10-15 15:24:53 +00:00
if schema == nil {
var schemaIssue * schemaIssue
schema , schemaIssue = buildSchema ( reflect . TypeFor [ T ] ( ) , secrets )
if schemaIssue != nil {
accumulator . add ( diagnosticFromSchemaIssue ( schemaIssue ) )
return nil , accumulator . loadError ( )
}
2026-09-12 15:58:06 +00:00
}
2026-10-23 18:15:01 +00:00
if request . ordinary && ( input . document != nil || input . composition != nil ) {
2026-10-25 13:07:16 +00:00
input . composition , diagnostics = compositionForInputWithPolicy ( input , policy . sequencePolicy )
2026-10-16 18:59:52 +00:00
if len ( diagnostics ) != 0 {
accumulator . add ( diagnostics ... )
return nil , accumulator . loadError ( )
}
input . document = input . composition . root
}
2026-10-23 19:49:12 +00:00
if request . sourceVersionMode == sourceVersionModeDefault && request . ordinary {
2026-10-23 18:15:01 +00:00
versionDiagnostics := checkVersion ( request . definition . Version , input . document )
accumulator . add ( versionDiagnostics ... )
if hasErrors ( versionDiagnostics ) {
return nil , accumulator . loadError ( )
}
}
2026-09-29 17:52:20 +00:00
if request . ordinary {
2026-10-24 01:11:28 +00:00
versionState . locateMainVersion ( input . document )
2026-10-25 13:07:16 +00:00
input , diagnostics = applyInputLayers ( request . main . source , request . main . filesystem , request . overlay , versionState . effectiveRule , policy . sequencePolicy , input )
2026-09-29 17:52:20 +00:00
} else {
2026-10-25 13:07:16 +00:00
input , diagnostics = assembleLayeredInput ( request , input , policy , state )
2026-09-29 17:52:20 +00:00
}
2026-10-15 15:40:11 +00:00
accumulator . add ( versionState . diagnostics [ reportedVersionDiagnostics : ] ... )
2026-09-12 15:58:06 +00:00
accumulator . sources = input . sources
accumulator . add ( diagnostics ... )
2026-10-24 00:13:54 +00:00
if hasErrors ( diagnostics ) {
2026-09-12 15:58:06 +00:00
return nil , accumulator . loadError ( )
}
2026-10-24 01:11:28 +00:00
request . effectiveVersion = versionState . effectiveRule
2026-09-29 18:08:27 +00:00
if request . ordinary && input . parsed . source != nil {
2026-10-25 22:34:52 +00:00
input , diagnostics = completeOrdinaryResourceContexts ( input , request . main )
if len ( diagnostics ) != 0 {
accumulator . add ( diagnostics ... )
2026-09-29 23:04:13 +00:00
return nil , accumulator . loadError ( )
}
2026-10-25 22:34:52 +00:00
}
var expanded bool
var effectiveVersion * VersionRule
input , effectiveVersion , expanded , diagnostics = expandStructuralInheritance ( input , request . protectionVersion ( ) , policy . sequencePolicy )
if len ( diagnostics ) != 0 {
accumulator . add ( diagnostics ... )
return nil , accumulator . loadError ( )
}
if expanded {
2026-10-24 01:11:28 +00:00
request . effectiveVersion = effectiveVersion
versionState . effectiveRule = effectiveVersion
2026-09-29 23:04:13 +00:00
}
2026-09-12 15:58:06 +00:00
2026-10-23 21:37:13 +00:00
base := overridePreparationInput { root : input . document }
if input . composition != nil {
base . inputBytes = input . composition . inputBytes
base . inputNodes = input . composition . inputNodes
}
preparedOverrides , diagnostics := preparePostStructuralOverrides ( base , input . sources , request . overrides , request . protectionVersion ( ) , defaultImportLimits ( ) )
accumulator . add ( diagnostics ... )
if hasErrors ( diagnostics ) {
return nil , accumulator . loadError ( )
}
input . sources = preparedOverrides . sources
accumulator . sources = input . sources
2026-10-15 15:24:53 +00:00
decodeDefinition := request . definition
decodeDefinition . Version = request . protectionVersion ( )
decoded , diagnostics := decodeLoadInput ( schema , decodeDefinition , input )
2026-09-12 15:58:06 +00:00
accumulator . add ( diagnostics ... )
if len ( diagnostics ) != 0 {
return nil , accumulator . loadError ( )
}
2026-10-23 21:37:13 +00:00
decodeContext := decodeContext { }
if decodeDefinition . Version != nil {
decodeContext . versionPath = & decodeDefinition . Version . Path
}
assigned , issues := applyDecodedOverrides ( schema , decoded , preparedOverrides , decodeContext )
if len ( issues ) != 0 {
for _ , issue := range issues {
accumulator . add ( diagnosticFromDecodeIssue ( issue ) )
}
return nil , accumulator . loadError ( )
}
postDecode := preparePostDecodeOverrideContext ( input , decoded , assigned )
resolved , diagnostics := resolveLoadInput ( schema , request . definition , input , postDecode )
2026-09-12 15:58:06 +00:00
accumulator . add ( diagnostics ... )
if hasErrors ( diagnostics ) {
return nil , accumulator . loadError ( )
}
2026-10-25 22:04:54 +00:00
counter := newRetainedNodeCounter ( historyLimit )
inspection , admitted := prepareInspectionHistory ( resolved , counter )
if ! admitted {
accumulator . add ( Diagnostic { Code : CodeExplainLimitExceeded , Severity : SeverityError , Message : "retained configuration history exceeds the node limit" } )
return nil , accumulator . loadError ( )
}
2026-10-25 21:53:24 +00:00
historyApplications := make ( [ ] historyApplication , 0 , len ( inspection . applications ) + len ( inspection . postDecodeApps ) )
historyApplications = append ( historyApplications , inspection . applications ... )
historyApplications = append ( historyApplications , inspection . postDecodeApps ... )
2026-10-25 22:04:54 +00:00
if ! addInspectionHistory ( counter , inspection ) {
2026-09-12 15:58:06 +00:00
accumulator . add ( Diagnostic { Code : CodeExplainLimitExceeded , Severity : SeverityError , Message : "retained configuration history exceeds the node limit" } )
return nil , accumulator . loadError ( )
}
// Detach resolver-borrowed values before reference mutation or validation.
configuration , ownershipIssue := detachLoadConfiguration ( schema , resolved . configuration , cloneUnresolvedSecrets )
if ownershipIssue != nil {
accumulator . add ( * ownershipIssue )
return nil , accumulator . loadError ( )
}
2026-10-23 21:37:13 +00:00
secretResolution := resolveSecretReferences ( reflect . ValueOf ( & configuration ) . Elem ( ) , schema , resolved . referenceInput , lookup )
2026-09-12 15:58:06 +00:00
accumulator . add ( secretResolution . diagnostics ... )
if hasErrors ( secretResolution . diagnostics ) {
return nil , accumulator . loadError ( )
}
2026-10-25 13:07:16 +00:00
identityDiagnostics := validateFinalSequenceIdentities ( resolved . referenceInput , policy . sequencePolicy )
2026-10-23 21:43:54 +00:00
accumulator . add ( identityDiagnostics ... )
if hasErrors ( identityDiagnostics ) {
return nil , accumulator . loadError ( )
}
2026-09-29 17:52:20 +00:00
if request . definition . Validate != nil {
2026-10-23 21:37:13 +00:00
diagnostics = runValidation ( & configuration , request . definition . Validate , schema , resolved . final , resolved . referenceInput , secretResolution . lookups )
2026-09-12 15:58:06 +00:00
accumulator . add ( diagnostics ... )
if hasErrors ( diagnostics ) {
return nil , accumulator . loadError ( )
}
}
resolved . configuration = configuration
// Publication detaches equal-valued validator replacements, preserving the
// validated secret state, then finalizes metadata and sensitivity.
2026-10-25 21:53:24 +00:00
return publishLoadResult ( schema , request . definition , resolved , inspection , historyApplications , secretResolution . references , & accumulator )
2026-09-12 15:58:06 +00:00
}