package app import ( "context" "fmt" "io" "strings" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) func buildHelperArtifactCatalog(cfg *config.Config, m *manifest.Manifest) (*artifacts.ArtifactCatalog, error) { configured := artifacts.ConfiguredArtifactDefinitions(nil) if cfg.Pipeline.Scriptorium != nil { configured = artifacts.ConfiguredArtifactDefinitions(cfg.Pipeline.Scriptorium.Artifacts) } extractionDefinitions := artifacts.ExtractionDefinitionsFromConfig(cfg.Pipeline.Notarius) effective, err := artifacts.ResolveEffectiveArtifactSet(configured, nil) if err != nil { return nil, err } catalog, err := artifacts.BootstrapRuntimeCatalog(configured, effective, extractionDefinitions) if err != nil { return nil, err } paths := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root).SessionPathsFor(cfg.Session.Campaign, cfg.Session.SessionID) if cfg.Pipeline.Notarius != nil && cfg.Pipeline.Notarius.Enabled { catalog.HydrateExtractionArtifacts(paths, m, extractionDefinitions) } catalog.HydrateAnalyzeArtifacts(paths, m, configured) return catalog, nil } func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.ArtifactCatalog, locks *effectiveLocks, publishedRemoteState map[string]string) { lockSet := lockSourceSet(locks.All) fmt.Fprintln(out, "Built-in:") for _, transcript := range artifacts.RuntimeTranscriptArtifacts() { writeArtifactLine(out, transcript.SourceID, lockSet) } writeArtifactLine(out, artifacts.ArtifactBoundsSession, lockSet) fmt.Fprintln(out, "Configured:") origins := config.ArtifactFamilies(cfg.Pipeline).Members for _, entry := range catalog.ListConfigured() { state := "unavailable" if entry.Available { state = "available" } writeConfiguredArtifactLine(out, entry.SourceID, state, entry.Provenance, lockSet, origins) } fmt.Fprintln(out, "Extraction:") for _, entry := range catalog.ListExtraction() { state := "unavailable" if entry.Available { state = "available" } writeExtractionArtifactLine(out, entry.SourceID, state, entry.Provenance, lockSet) } fmt.Fprintln(out, "Previous-session:") if effective, err := resolveEffectiveArtifacts(cfg, nil); err == nil { for _, req := range artifacts.CollectPreviousArtifactRequirements(configuredScriptoriumArtifacts(cfg), effective) { fmt.Fprintf(out, "- %s required=%t\n", artifactpolicy.PreviousSessionSourceID(req.Name), req.Required) } } fmt.Fprintln(out, "Published:") for _, rule := range cfg.Pipeline.Publish.Outputs { writePublishedOutputLine(out, rule, catalog, lockSet, publishedRemoteState) } } func writeConfiguredArtifactLine(out io.Writer, source, state, provenance string, lockSet map[string]config.PublishLockRule, origins map[string]config.ArtifactFamilyMemberOrigin) { parts := []string{source, "planned", state} if key, ok := artifactpolicy.ParseConfiguredSource(source); ok { if origin, family := origins[key]; family { parts = append(parts, "family="+origin.Family, "character_id="+origin.CharacterID) } } if strings.TrimSpace(provenance) != "" { parts = append(parts, "provenance="+strings.TrimSpace(provenance)) } if _, ok := lockSet[source]; ok { parts = append(parts, "locked") } fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) } func writeExtractionArtifactLine(out io.Writer, source, state, provenance string, lockSet map[string]config.PublishLockRule) { parts := []string{source, "planned", state} if strings.TrimSpace(provenance) != "" { parts = append(parts, "provenance="+strings.TrimSpace(provenance)) } if _, ok := lockSet[source]; ok { parts = append(parts, "locked") } fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) } func writeArtifactLine(out io.Writer, source string, lockSet map[string]config.PublishLockRule) { parts := []string{source} if _, ok := lockSet[source]; ok { parts = append(parts, "locked") } fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) } func writePublishedOutputLine(out io.Writer, rule config.PublishOutputRule, catalog *artifacts.ArtifactCatalog, lockSet map[string]config.PublishLockRule, remoteState map[string]string) { source := strings.TrimSpace(rule.Source) parts := []string{source} if _, ok := lockSet[source]; ok { parts = append(parts, "locked") } dest, showDest, err := helperPublishedOutputDest(rule, catalog) if err != nil { parts = append(parts, "remote=error") fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) return } if showDest { parts = append(parts, "dest="+dest) } if state := remoteState[publishedOutputRemoteStateKey(source, dest)]; state != "" { parts = append(parts, state) } fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) } func unresolvedRemotePublishedOutputAvailability(cfg *config.Config, catalog *artifacts.ArtifactCatalog, state string) map[string]string { out := map[string]string{} for _, rule := range cfg.Pipeline.Publish.Outputs { source := strings.TrimSpace(rule.Source) dest, _, err := helperPublishedOutputDest(rule, catalog) if err != nil { out[publishedOutputRemoteStateKey(source, "")] = "remote=error" continue } out[publishedOutputRemoteStateKey(source, dest)] = state } return out } func remotePublishedOutputAvailabilityForCurrent( ctx context.Context, cfg *config.Config, store storage.ObjectStore, catalog *artifacts.ArtifactCatalog, current *RemoteCurrentState, ) map[string]string { if current == nil || current.Commit == nil { return unresolvedRemotePublishedOutputAvailability(cfg, catalog, "remote=missing") } out := map[string]string{} for _, rule := range cfg.Pipeline.Publish.Outputs { source := strings.TrimSpace(rule.Source) dest, _, err := helperPublishedOutputDest(rule, catalog) if err != nil { out[publishedOutputRemoteStateKey(source, "")] = "remote=error" continue } declaration, ok := committedPublishedOutput(current.Commit, source) if !ok { out[publishedOutputRemoteStateKey(source, dest)] = "remote=missing" continue } identity, err := artifacts.ParseImmutableRunObjectKey(current.SessionPrefix, declaration.DestinationKey) if err != nil { out[publishedOutputRemoteStateKey(source, dest)] = "remote=error" continue } exists, err := store.Exists(ctx, declaration.DestinationKey) if err != nil { out[publishedOutputRemoteStateKey(source, dest)] = "remote=error" continue } if !exists { out[publishedOutputRemoteStateKey(source, dest)] = "remote=missing" continue } state := "remote=published" if identity.RunID != current.RunID { state += " origin_run=" + identity.RunID } out[publishedOutputRemoteStateKey(source, dest)] = state } return out } func committedPublishedOutput(commit *artifacts.RemoteCommitManifest, source string) (artifacts.RemoteArtifact, bool) { if commit == nil { return artifacts.RemoteArtifact{}, false } for _, artifact := range commit.Artifacts { if artifact.Type == artifacts.RemoteArtifactTypePublishedOutput && artifact.Source == source { return artifact, true } } return artifacts.RemoteArtifact{}, false } func helperPublishedOutputDest(rule config.PublishOutputRule, catalog *artifacts.ArtifactCatalog) (string, bool, error) { source := strings.TrimSpace(rule.Source) normalized, err := artifactpolicy.ResolvePublishedDestinationWithExtractions( source, rule.Dest, helperConfiguredOutputPathMap(catalog), helperExtractionOutputSet(catalog), ) if err != nil { return "", false, err } entry, ok := catalog.Lookup(source) showDest := !ok || strings.TrimSpace(entry.CanonicalRelPath) != normalized return normalized, showDest, nil } func helperExtractionOutputSet(catalog *artifacts.ArtifactCatalog) map[string]struct{} { out := map[string]struct{}{} if catalog == nil { return out } for _, entry := range catalog.ListExtraction() { if strings.TrimSpace(entry.ExtractionKey) != "" { out[entry.ExtractionKey] = struct{}{} } } return out } func helperConfiguredOutputPathMap(catalog *artifacts.ArtifactCatalog) map[string]string { out := map[string]string{} if catalog == nil { return out } for _, entry := range catalog.ListConfigured() { if strings.TrimSpace(entry.ConfiguredKey) == "" { continue } out[entry.ConfiguredKey] = strings.TrimSpace(entry.CanonicalRelPath) } return out } func publishedOutputRemoteStateKey(source, dest string) string { return strings.TrimSpace(source) + "\x00" + strings.TrimSpace(dest) }