package config import ( "path/filepath" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) func normalizeDocumentPaths(document *Document, context *configkit.Resolution, state *documentLoadState) { resolution := documentNormalizer{Resolution: context} if document == nil { return } initializeDocumentMainDirectory(resolution, state) if document.PromptKit != nil { normalizeDocumentPath(resolution, state, configkit.Keys("promptkit", "profile_dir"), &document.PromptKit.ProfileDir, false, "resolved PromptKit profile directory") normalizeDocumentPath(resolution, state, configkit.Keys("promptkit", "profile_file"), &document.PromptKit.ProfileFile, false, "resolved PromptKit profile file") } if document.Output != nil { normalizeDocumentPath(resolution, state, configkit.Keys("output", "directory"), &document.Output.Directory, false, "resolved output directory") } if document.Cache != nil { if document.Cache.ChunkPlans != nil { normalizeDocumentPath(resolution, state, configkit.Keys("cache", "chunk_plans", "directory"), &document.Cache.ChunkPlans.Directory, true, "resolved chunk-plan cache directory") } if document.Cache.Checkpoints != nil { normalizeDocumentPath(resolution, state, configkit.Keys("cache", "checkpoints", "directory"), &document.Cache.Checkpoints.Directory, true, "resolved checkpoint cache directory") } } if document.Debug != nil { normalizeDocumentPath(resolution, state, configkit.Keys("debug", "directory"), &document.Debug.Directory, false, "resolved debug directory") } for _, id := range sortedPipelineDocumentKeys(document.Pipelines) { profile := document.Pipelines[id] path := configkit.Keys("pipelines").Key(id) normalizeDocumentReferencePaths(profile.References, path.Key("references"), resolution, state) normalizeDocumentBindingReferencePaths(&profile.Input, path.Key("input"), resolution, state) if profile.Chunk != nil { normalizeDocumentBindingReferencePaths(profile.Chunk, path.Key("chunk"), resolution, state) } if profile.Output != nil { normalizeDocumentBindingReferencePaths(profile.Output, path.Key("output"), resolution, state) } for laneID, lane := range profile.Artifacts { normalizeDocumentLaneReferencePaths(&lane, path.Key("artifacts").Key(laneID), resolution, state) profile.Artifacts[laneID] = lane } stepResolution := resolution.inSequence(path.Key("steps")) for index := range profile.Steps { step := &profile.Steps[index] stepPath := path.Key("steps").Index(index) normalizeDocumentReferencePaths(step.References, stepPath.Key("references"), stepResolution, state) for laneID, lane := range step.Artifacts { normalizeDocumentLaneReferencePaths(&lane, stepPath.Key("artifacts").Key(laneID), stepResolution, state) step.Artifacts[laneID] = lane } } document.Pipelines[id] = profile } } func initializeDocumentMainDirectory(resolution documentNormalizer, state *documentLoadState) { if state == nil || state.mainDirectory != "" { return } info, ok := resolution.InputInfo(configkit.Keys("version")) if ok && len(info.Origin.Locations) == 1 { if source, sourceOK := resolution.Source(info.Origin.Locations[0].SourceID); sourceOK && source.Path != "" { state.mainDirectory = filepath.Dir(source.Path) return } } // Byte and reader sources are test-oriented private entry points. They have // no declaring file, so preserve their established invocation-directory // semantics without extending that fallback to file-backed production loads. state.mainDirectory = cleanInvocationDirectory(".") } func normalizeDocumentBindingReferencePaths(binding *BindingDocument, path configkit.Path, resolution documentNormalizer, state *documentLoadState) { if binding != nil { normalizeDocumentReferencePaths(binding.References, path.Key("references"), resolution, state) } } func normalizeDocumentLaneReferencePaths(lane *ArtifactLaneDocument, path configkit.Path, resolution documentNormalizer, state *documentLoadState) { if lane == nil { return } normalizeDocumentBindingReferencePaths(&lane.Extract, path.Key("extract"), resolution, state) normalizeDocumentBindingReferencePaths(lane.Merge, path.Key("merge"), resolution, state) normalizeDocumentBindingReferencePaths(lane.Normalize, path.Key("normalize"), resolution, state) } func normalizeDocumentReferencePaths(references map[string]ReferenceDocument, path configkit.Path, resolution documentNormalizer, state *documentLoadState) { for _, name := range sortedReferenceDocumentKeys(references) { source := references[name] sourcePath := path.Key(name).Key("path") if source.Path != nil && *source.Path != "" && !filepath.IsAbs(*source.Path) { base, ok := documentPathBase(resolution, state, sourcePath) if !ok { resolution.Error(sourcePath, "relative path has no unambiguous source base") } else { value := filepath.Clean(filepath.Join(base, *source.Path)) if value != *source.Path { resolution.Record(sourcePath, configkit.ChangeNormalization, "resolved external reference path") source.Path = documentValue(value) } } references[name] = source } } } func normalizeDocumentPath(resolution documentNormalizer, state *documentLoadState, path configkit.Path, target **string, optional bool, reason string) { if target == nil || *target == nil || **target == "" || filepath.IsAbs(**target) { return } base, ok := documentPathBase(resolution, state, path) if !ok { resolution.Error(path, "relative path has no unambiguous source base") return } value := filepath.Clean(filepath.Join(base, **target)) if optional && **target == "" { return } if *target != nil && **target != value { configkit.Normalize(resolution.Resolution, path, target, documentValue(value), reason) } } func documentPathBase(resolution documentNormalizer, state *documentLoadState, path configkit.Path) (string, bool) { original := path if state != nil { original = state.originalPath(path) } info, ok := resolution.InputInfo(original) if !ok { if state != nil && state.mainDirectory != "" { return state.mainDirectory, true } return "", false } switch info.Origin.Layer { case configkit.LayerCLI: if state != nil && state.invocationDirectory != "" { return state.invocationDirectory, true } case configkit.LayerDefault: if state != nil && state.mainDirectory != "" { return state.mainDirectory, true } default: if len(info.Origin.Locations) != 1 { return "", false } source, ok := resolution.Source(info.Origin.Locations[0].SourceID) if !ok || source.Path == "" { if state != nil && state.mainDirectory != "" { return state.mainDirectory, true } return "", false } return filepath.Dir(source.Path), true } return "", false }