diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index a6b87dd..83f2c40 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Consolidation Implementation Plan -Status: in progress. Stages 1 through 3 are complete; follow Stage 4 through Stage 20 in order. +Status: in progress. Stages 1 through 4 are complete; follow Stage 5 through Stage 20 in order. The target executor is a Luna coding agent working through one end-to-end sprint. ## Authority, Baseline, And Execution @@ -237,7 +237,7 @@ load-wide defaults, source-version, and sequence-registration validation into shared settings preparation, and removed `selectOptions` plus its validation flag from grouped preflight. `GOTOOLCHAIN=local GOPROXY=off go test ./...` passed. -## Stage 4 — Express Input Acquisition With Non-Generic Requests +## Stage 4 — Express Input Acquisition With Non-Generic Requests (Complete) **Owners:** new engine `acquisition.go` and existing `load_input.go`. @@ -263,6 +263,13 @@ flag from grouped preflight. `GOTOOLCHAIN=local GOPROXY=off go test ./...` passe `Options[T]` or an application type. Call-site and import-worker migration is completed by Stages 5–6; temporary non-compilation is acceptable. +Progress notes (2026-10-25): introduced the non-generic acquisition request and +policy/state/tier records, moved the input-acquisition implementation into +`acquireInput`, and added the resolved-context handoff field. The focused engine +test command currently cannot compile because the old assembly callers in +`load_input.go` and `load_request.go` are scheduled to migrate in Stage 6; test +verification is deferred until that integration. + ## Stage 5 — Consolidate Import Requests And Default Ownership **Owners:** engine import entry points, `importTraversal`, `source_version.go`, diff --git a/internal/engine/acquisition.go b/internal/engine/acquisition.go new file mode 100644 index 0000000..a70d544 --- /dev/null +++ b/internal/engine/acquisition.go @@ -0,0 +1,238 @@ +package engine + +import "strconv" + +type acquisitionPolicy struct { + importDefaults importDefaults + sequencePolicy sequencePolicy + importLimits importLimits +} + +type acquisitionState struct { + accounting *importAccounting + versionState *sourceVersionState +} + +type tierAcquisitionState struct { + visited map[resourceIdentity]importRoute + hostToken *namespaceToken +} + +type inputAcquisitionRequest struct { + input loadInput + role sourceVersionRole + directiveContext rootDirectiveContext + protectionRule *VersionRule + deferAssembly bool + ordinaryOverlay string +} + +type importAcquisitionRequest struct { + input inputAcquisitionRequest + main parsedSource + processing *sourceVersionProcessingResult +} + +type resolvedFilesystemContext struct { + native *nativeNamespace + hostToken *namespaceToken + rootPath string + basePath string + mainPath string +} + +func acquireInput(request inputAcquisitionRequest, policy acquisitionPolicy, state *acquisitionState, tier *tierAcquisitionState) (assembledInput, []Diagnostic) { + source := request.input.source + destination := request.input.destination + accounting := state.accounting + versionState := state.versionState + visited := tier.visited + hostToken := tier.hostToken + role := request.role + sourceID := "source-" + strconv.Itoa(accounting.sources+1) + selectedSource := source + if source.kind == sourceSearch { + resolved, failedSource, diagnostics := resolveSearchInput(source, sourceID, defaultDiscoveryDirectoryHelpers(), defaultDiscoveryHostProbe()) + if len(diagnostics) != 0 { + if failedSource != nil { + failedSource.Destination = destination.clone() + failedSource.placementMode = request.input.placementMode + } + return assembledInput{source: resolved, sources: sourceInfos(failedSource), versionState: versionState, accounting: accounting}, diagnostics + } + selectedSource = resolved + if request.input.filesystem != nil { + if diagnostics := filesystemOptionDiagnostics(selectedSource, request.input.filesystem); len(diagnostics) != 0 { + return assembledInput{source: selectedSource, versionState: versionState, accounting: accounting}, diagnostics + } + } + if selectedSource.kind == sourceNone && activeOverlay(request.ordinaryOverlay) && request.input.filesystem == nil { + return assembledInput{source: selectedSource, versionState: versionState, accounting: accounting}, []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "NoSource overlays require explicit filesystem context")} + } + } + limits := policy.importLimits + parseLimits := defaultParseLimits() + parseLimits.source.remaining = limits.bytes - accounting.bytes + parsed, diagnostics := parseSource(selectedSource, sourceID, parseLimits) + if parsed.source != nil { + parsed.source.Destination = destination.clone() + parsed.source.placementMode = request.input.placementMode + } + input := assembledInput{source: selectedSource, parsed: parsed, document: parsed.document, sources: sourceInfos(parsed.source), versionState: versionState, accounting: accounting} + if len(diagnostics) != 0 { + input.sources = sourceInfos(parsed.source) + // Mapping value issues already identify application content. Other early + // failures describe source syntax or access, not a placed value. + if selectedSource.kind == sourceMapping && selectedSource.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...)} + } + } + return input, diagnostics + } + directiveContext := request.directiveContext + if directiveContext == 0 { + directiveContext = rootDirectiveContextImportsOnly + } + if !request.input.imports && parsed.document != nil { + var directiveDiagnostics []Diagnostic + if directiveContext == rootDirectiveContextSource || directiveContext == rootDirectiveContextLayered { + directiveDiagnostics = disabledRootImportDiagnostics(parsed.document) + } else { + directiveDiagnostics = disabledDirectiveDiagnostics(parsed.document, Path{}) + } + if len(directiveDiagnostics) != 0 { + return input, directiveDiagnostics + } + } + var versionProcessing *sourceVersionProcessingResult + if directiveContext == rootDirectiveContextSource { + var sourceID string + if parsed.source != nil { + sourceID = parsed.source.ID + } + var processed sourceVersionProcessingResult + var failures []Diagnostic + if versionState != nil { + processed, failures = versionState.processWithDirectiveContext(parsed.document, role, destination, sourceID, request.input.placementMode, directiveContext) + } else { + processed = processSourceVersionWithDirectiveContext(parsed.document, request.protectionRule, role, destination, sourceVersionModeDefault, directiveContext) + failures = processed.diagnostics + } + if len(failures) != 0 { + return input, failures + } + versionProcessing = &processed + if role == sourceVersionMain && versionState != nil { + versionState.mainProcessing = &processed + } + } else if directiveContext == rootDirectiveContextLayered { + extracted, failures := extractRootDirectives(parsed.document, rootDirectiveContextLayered) + if len(failures) != 0 { + return input, failures + } + processed := sourceVersionProcessingResult{ + payload: extracted.body, imports: extracted.imports, sequences: extracted.sequences, + directiveContext: rootDirectiveContextLayered, + } + if versionState != nil && versionState.mode != sourceVersionModeDefault { + var sourceID string + if parsed.source != nil { + sourceID = parsed.source.ID + } + versionProcessed, versionFailures := versionState.process(extracted.body, role, destination, sourceID, request.input.placementMode) + if len(versionFailures) != 0 { + return input, versionFailures + } + versionProcessed.imports = extracted.imports + versionProcessed.sequences = append(cloneSequenceDeclarations(extracted.sequences), versionProcessed.sequences...) + versionProcessed.directiveContext = rootDirectiveContextLayered + processed = versionProcessed + if role == sourceVersionMain { + versionState.mainProcessing = &processed + } + } + versionProcessing = &processed + } else if versionState != nil && versionState.mode != sourceVersionModeDefault { + var sourceID string + if parsed.source != nil { + sourceID = parsed.source.ID + } + processed, failures := versionState.process(parsed.document, role, destination, sourceID, request.input.placementMode) + if len(failures) != 0 { + return input, failures + } + versionProcessing = &processed + if role == sourceVersionMain { + versionState.mainProcessing = &processed + } + } + if request.input.imports && selectedSource.kind != sourceNone { + if request.deferAssembly { + input.composition, input.sources, diagnostics = loadImportedDocumentsTrackedWithProcessingDeferred(selectedSource, request.input.filesystem, parsed, destination, request.protectionRule, limits, accounting, visited, hostToken, versionState, versionProcessing, policy.sequencePolicy, true) + } else { + input.composition, input.sources, diagnostics = loadImportedDocumentsTrackedWithProcessing(selectedSource, request.input.filesystem, parsed, destination, request.protectionRule, limits, accounting, visited, hostToken, versionState, versionProcessing, policy.sequencePolicy) + } + if len(diagnostics) != 0 { + return input, diagnostics + } + if input.composition != nil { + input.acquisition = cloneAcquisitionScopes(input.composition.acquisition) + input.document = input.composition.root + } + } else { + input.sources = sourceInfos(parsed.source) + if versionProcessing != nil { + input.document = versionProcessing.payload + } + if request.input.filesystem != nil && (selectedSource.kind == sourceNone || !activeOverlay(request.ordinaryOverlay)) { + if diagnostic := validateFilesystemContext(selectedSource, request.input.filesystem, parsed); diagnostic != nil { + return input, []Diagnostic{*diagnostic} + } + } + } + if !request.input.imports && input.document != nil { + _, placed, err := placeDocumentWithMode(input.document, destination, request.input.placementMode, input.document.Location, parseLimits.document.Depth, parseLimits.document.Nodes) + if err != nil { + return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the source depth or node limit after destination placement")} + } + localPayload := input.document + input.document = placed + identity := importKeyForMainSource(parsed.source) + var sequenceDeclarations []sequenceDeclarationSpec + if versionProcessing != nil { + sequenceDeclarations = cloneSequenceDeclarations(versionProcessing.sequences) + } + input.acquisition = []acquisitionScope{{ + owner: identity, + destination: destination.clone(), + contributions: []acquiredContribution{{ + source: cloneSourceInfo(*parsed.source), payload: localPayload, + identity: identity, role: importRoleMain, scopeOwner: identity, + document: placed, destination: destination.clone(), processing: versionProcessing, + sequenceDeclarations: sequenceDeclarations, + }}, + }} + rawNodes := countInputNodes(parsed.document) + placementNodes := destinationWrapperNodeCountForMode(destination, request.input.placementMode) + if accounting.sources+1 > limits.sources || accounting.bytes+parsed.bytes > limits.bytes || accounting.nodes+rawNodes+placementNodes > limits.nodes { + return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the aggregate input limit")} + } + if diagnostic := registerAcquiredSource(selectedSource, request.input.filesystem, parsed.source, visited, hostToken); diagnostic != nil { + return input, []Diagnostic{*diagnostic} + } + accounting.sources++ + accounting.bytes += parsed.bytes + accounting.nodes += rawNodes + placementNodes + } + if !activeOverlay(request.ordinaryOverlay) || request.input.imports { + contexts, diagnostic := contextsForAcquiredSources(selectedSource, request.input.filesystem, parsed.source, input.sources) + if diagnostic != nil { + return input, []Diagnostic{*diagnostic} + } + input.contexts = contexts + } + return input, nil +} diff --git a/internal/engine/load_input.go b/internal/engine/load_input.go index e8c4004..c512ef7 100644 --- a/internal/engine/load_input.go +++ b/internal/engine/load_input.go @@ -1,23 +1,22 @@ package engine import ( - "strconv" - "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) // assembledInput is the source/document handoff across schema and version checks. type assembledInput struct { - source Source - parsed parsedSource - document *document.Node - composition *composedDocument - acquisition []acquisitionScope - inheritance *inheritanceScope - sources []SourceInfo - contexts map[string]ResourceContext - versionState *sourceVersionState - accounting *importAccounting + source Source + parsed parsedSource + document *document.Node + composition *composedDocument + acquisition []acquisitionScope + inheritance *inheritanceScope + sources []SourceInfo + contexts map[string]ResourceContext + versionState *sourceVersionState + accounting *importAccounting + resolvedFilesystem *resolvedFilesystemContext } // acquiredContribution keeps a source-local payload beside its placed document. @@ -101,195 +100,6 @@ func assembleBaseInputTrackedWithVersion[T any](source Source, options Options[T 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) { - sourceID := "source-" + strconv.Itoa(accounting.sources+1) - selectedSource := source - if source.kind == sourceSearch { - resolved, failedSource, diagnostics := resolveSearchInput(source, sourceID, defaultDiscoveryDirectoryHelpers(), defaultDiscoveryHostProbe()) - if len(diagnostics) != 0 { - if failedSource != nil { - failedSource.Destination = destination.clone() - failedSource.placementMode = options.placementMode - } - return assembledInput{source: resolved, sources: sourceInfos(failedSource), versionState: versionState, accounting: accounting}, diagnostics - } - selectedSource = resolved - if options.Filesystem != nil { - if diagnostics := filesystemOptionDiagnostics(selectedSource, options.Filesystem); len(diagnostics) != 0 { - return assembledInput{source: selectedSource, versionState: versionState, accounting: accounting}, diagnostics - } - } - if selectedSource.kind == sourceNone && activeOverlay(options.Overlay) && options.Filesystem == nil { - return assembledInput{source: selectedSource, versionState: versionState, accounting: accounting}, []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "NoSource overlays require explicit filesystem context")} - } - } - limits := defaultImportLimits() - parseLimits := defaultParseLimits() - parseLimits.source.remaining = limits.bytes - accounting.bytes - parsed, diagnostics := parseSource(selectedSource, sourceID, parseLimits) - if parsed.source != nil { - parsed.source.Destination = destination.clone() - parsed.source.placementMode = options.placementMode - } - input := assembledInput{source: selectedSource, parsed: parsed, document: parsed.document, sources: sourceInfos(parsed.source), versionState: versionState, accounting: accounting} - if len(diagnostics) != 0 { - input.sources = sourceInfos(parsed.source) - // Mapping value issues already identify application content. Other early - // failures describe source syntax or access, not a placed value. - if selectedSource.kind == sourceMapping && selectedSource.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...)} - } - } - return input, diagnostics - } - directiveContext := options.directiveContext - if directiveContext == 0 { - directiveContext = rootDirectiveContextImportsOnly - } - if !options.Imports && parsed.document != nil { - var directiveDiagnostics []Diagnostic - if directiveContext == rootDirectiveContextSource || directiveContext == rootDirectiveContextLayered { - directiveDiagnostics = disabledRootImportDiagnostics(parsed.document) - } else { - directiveDiagnostics = disabledDirectiveDiagnostics(parsed.document, Path{}) - } - if len(directiveDiagnostics) != 0 { - return input, directiveDiagnostics - } - } - var versionProcessing *sourceVersionProcessingResult - if directiveContext == rootDirectiveContextSource { - var sourceID string - if parsed.source != nil { - sourceID = parsed.source.ID - } - var processed sourceVersionProcessingResult - var failures []Diagnostic - if versionState != nil { - processed, failures = versionState.processWithDirectiveContext(parsed.document, role, destination, sourceID, options.placementMode, directiveContext) - } else { - processed = processSourceVersionWithDirectiveContext(parsed.document, options.Definition.Version, role, destination, sourceVersionModeDefault, directiveContext) - failures = processed.diagnostics - } - if len(failures) != 0 { - return input, failures - } - versionProcessing = &processed - if role == sourceVersionMain && versionState != nil { - versionState.mainProcessing = &processed - } - } else if directiveContext == rootDirectiveContextLayered { - extracted, failures := extractRootDirectives(parsed.document, rootDirectiveContextLayered) - if len(failures) != 0 { - return input, failures - } - processed := sourceVersionProcessingResult{ - payload: extracted.body, imports: extracted.imports, sequences: extracted.sequences, - directiveContext: rootDirectiveContextLayered, - } - if versionState != nil && versionState.mode != sourceVersionModeDefault { - var sourceID string - if parsed.source != nil { - sourceID = parsed.source.ID - } - versionProcessed, versionFailures := versionState.process(extracted.body, role, destination, sourceID, options.placementMode) - if len(versionFailures) != 0 { - return input, versionFailures - } - versionProcessed.imports = extracted.imports - versionProcessed.sequences = append(cloneSequenceDeclarations(extracted.sequences), versionProcessed.sequences...) - versionProcessed.directiveContext = rootDirectiveContextLayered - processed = versionProcessed - if role == sourceVersionMain { - versionState.mainProcessing = &processed - } - } - versionProcessing = &processed - } else if versionState != nil && versionState.mode != sourceVersionModeDefault { - var sourceID string - if parsed.source != nil { - sourceID = parsed.source.ID - } - processed, failures := versionState.process(parsed.document, role, destination, sourceID, options.placementMode) - if len(failures) != 0 { - return input, failures - } - versionProcessing = &processed - if role == sourceVersionMain { - versionState.mainProcessing = &processed - } - } - if options.Imports && selectedSource.kind != sourceNone { - if options.deferAssembly { - input.composition, input.sources, diagnostics = loadImportedDocumentsTrackedWithProcessingDeferred(selectedSource, options.Filesystem, parsed, destination, options.Definition.Version, limits, accounting, visited, hostToken, versionState, versionProcessing, options.sequencePolicy, true) - } else { - input.composition, input.sources, diagnostics = loadImportedDocumentsTrackedWithProcessing(selectedSource, options.Filesystem, parsed, destination, options.Definition.Version, limits, accounting, visited, hostToken, versionState, versionProcessing, options.sequencePolicy) - } - if len(diagnostics) != 0 { - return input, diagnostics - } - if input.composition != nil { - input.acquisition = cloneAcquisitionScopes(input.composition.acquisition) - input.document = input.composition.root - } - } else { - input.sources = sourceInfos(parsed.source) - if versionProcessing != nil { - input.document = versionProcessing.payload - } - if options.Filesystem != nil && (selectedSource.kind == sourceNone || !activeOverlay(options.Overlay)) { - if diagnostic := validateFilesystemContext(selectedSource, options.Filesystem, parsed); diagnostic != nil { - return input, []Diagnostic{*diagnostic} - } - } - } - if !options.Imports && input.document != nil { - _, placed, err := placeDocumentWithMode(input.document, destination, options.placementMode, input.document.Location, parseLimits.document.Depth, parseLimits.document.Nodes) - if err != nil { - return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the source depth or node limit after destination placement")} - } - localPayload := input.document - input.document = placed - identity := importKeyForMainSource(parsed.source) - var sequenceDeclarations []sequenceDeclarationSpec - if versionProcessing != nil { - sequenceDeclarations = cloneSequenceDeclarations(versionProcessing.sequences) - } - input.acquisition = []acquisitionScope{{ - owner: identity, - destination: destination.clone(), - contributions: []acquiredContribution{{ - source: cloneSourceInfo(*parsed.source), payload: localPayload, - identity: identity, role: importRoleMain, scopeOwner: identity, - document: placed, destination: destination.clone(), processing: versionProcessing, - sequenceDeclarations: sequenceDeclarations, - }}, - }} - rawNodes := countInputNodes(parsed.document) - placementNodes := destinationWrapperNodeCountForMode(destination, options.placementMode) - if accounting.sources+1 > limits.sources || accounting.bytes+parsed.bytes > limits.bytes || accounting.nodes+rawNodes+placementNodes > limits.nodes { - return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the aggregate input limit")} - } - if diagnostic := registerAcquiredSource(selectedSource, options.Filesystem, parsed.source, visited, hostToken); diagnostic != nil { - return input, []Diagnostic{*diagnostic} - } - accounting.sources++ - accounting.bytes += parsed.bytes - accounting.nodes += rawNodes + placementNodes - } - if !activeOverlay(options.Overlay) || options.Imports { - contexts, diagnostic := contextsForAcquiredSources(selectedSource, options.Filesystem, parsed.source, input.sources) - if diagnostic != nil { - return input, []Diagnostic{*diagnostic} - } - input.contexts = contexts - } - return input, nil -} - func registerAcquiredSource(source Source, context *FilesystemContext, info *SourceInfo, visited map[resourceIdentity]importRoute, hostToken *namespaceToken) *Diagnostic { if visited == nil || info == nil || info.Path == "" { return nil