package config import ( "fmt" "gopkg.in/yaml.v3" ) // EffectivePipelineRootPath reports the absolute root pipeline path retained // while resolving cfg. It is empty for a pipeline not loaded through the // production loader. func EffectivePipelineRootPath(cfg *PipelineConfig) string { if cfg == nil || cfg.resolution == nil { return "" } return cfg.resolution.rootPath } // MarshalEffectivePipeline renders the validated, normalized pipeline as one // deterministic YAML document. Composition declarations and runtime-only // resolution data are excluded. Artifact family declarations are also omitted // because a resolved pipeline exposes their concrete artifacts instead. func MarshalEffectivePipeline(cfg *PipelineConfig) ([]byte, error) { if cfg == nil { return nil, fmt.Errorf("pipeline config is required") } data, err := yaml.Marshal(cfg) if err != nil { return nil, fmt.Errorf("serialize effective pipeline: %w", err) } document, err := parseCompositionBytes("effective pipeline", data) if err != nil { return nil, err } removeResolutionOnlyPipelineFields(document) return document.canonicalYAML() } func removeResolutionOnlyPipelineFields(document *compositionDocument) { if document == nil || document.root == nil { return } scriptoriumIndex := compositionFieldIndex(document.root.fields, "scriptorium") if scriptoriumIndex < 0 { return } scriptorium := document.root.fields[scriptoriumIndex].value if scriptorium == nil || scriptorium.kind != yaml.MappingNode { return } familyIndex := compositionFieldIndex(scriptorium.fields, "artifact_families") if familyIndex < 0 { return } scriptorium.fields = append(scriptorium.fields[:familyIndex], scriptorium.fields[familyIndex+1:]...) for index := range scriptorium.fields { scriptorium.fields[index].order = index } }