package config import ( "crypto/sha256" "encoding/hex" "fmt" "path/filepath" "sort" "strings" ) const ( pipelineDefaultOwnershipSource = "default" effectivePipelineDigestVersion = "narratio-effective-pipeline-v3\n" ) // PipelineOverlayProvenance identifies the overlay selected while resolving a // pipeline. It contains only non-secret selection metadata. type PipelineOverlayProvenance struct { Name string Source string OverlayPath string } // SelectedPipelineOverlay reports the overlay used to resolve cfg, if any. func SelectedPipelineOverlay(cfg *PipelineConfig) (*PipelineOverlayProvenance, bool) { if cfg == nil || cfg.resolution == nil || cfg.resolution.selectedOverlay == nil { return nil, false } selection := cfg.resolution.selectedOverlay return &PipelineOverlayProvenance{Name: selection.name, Source: selection.source, OverlayPath: selection.overlayPath}, true } // EffectivePipelineDigest reports the deterministic secret-free digest for a // resolved pipeline. func EffectivePipelineDigest(cfg *PipelineConfig) string { if cfg == nil || cfg.resolution == nil { return "" } return cfg.resolution.effectiveDigest } func finalizePipelineResolution(cfg *PipelineConfig) error { if cfg == nil || cfg.resolution == nil { return fmt.Errorf("pipeline resolution metadata is required") } declared := make(map[string][]string, len(cfg.resolution.ownership)) for _, ownership := range cfg.resolution.ownership { declared[ownership.path] = append([]string(nil), ownership.sources...) } records, err := projectionValueRecords(projectEffectivePipeline(cfg, true)) if err != nil { return err } ownership := make([]pipelineFieldOwnership, 0, len(records)) for _, record := range records { sources := declared[record.Path] if len(sources) == 0 { sources = []string{pipelineDefaultOwnershipSource} } ownership = append(ownership, pipelineFieldOwnership{ path: record.Path, sources: append([]string(nil), sources...), }) } for path, sources := range declared { if strings.HasPrefix(path, "scriptorium.artifact_families.") { ownership = append(ownership, pipelineFieldOwnership{path: path, sources: append([]string(nil), sources...)}) } } sort.Slice(ownership, func(i, j int) bool { return ownership[i].path < ownership[j].path }) cfg.resolution.ownership = ownership return recomputePipelineEffectiveDigest(cfg) } // recomputePipelineEffectiveDigest is the single package-owned hook for // refreshing provenance after later resolution expands concrete pipeline // values. Loader directives and runtime provenance are not serialized. func recomputePipelineEffectiveDigest(cfg *PipelineConfig) error { if cfg == nil || cfg.resolution == nil { return fmt.Errorf("pipeline resolution metadata is required") } canonical, err := projectionJSON(projectEffectivePipeline(cfg, true)) if err != nil { return err } digestInput := append([]byte(effectivePipelineDigestVersion), canonical...) digest := sha256.Sum256(digestInput) cfg.resolution.effectiveDigest = hex.EncodeToString(digest[:]) return nil } // captureLogicalNotariusPaths retains normalized user-facing path semantics // before runtime resolution makes relative paths depend on the checkout or // installation directory. Runtime paths remain absolute; provenance does not. func captureLogicalNotariusPaths(cfg *PipelineConfig) { if cfg == nil || cfg.resolution == nil || cfg.Notarius == nil { return } configPath := normalizeLogicalFilesystemPath(cfg.Notarius.ConfigPath) workingDirectory := normalizeLogicalFilesystemPath(cfg.Notarius.WorkingDirectory) cfg.resolution.logicalNotariusConfig = configPath cfg.resolution.logicalNotariusWorking = workingDirectory cfg.resolution.logicalNotariusCaptured = true } func normalizeLogicalFilesystemPath(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } return filepath.ToSlash(filepath.Clean(value)) }