2026-08-30 14:43:25 +00:00
package app
import (
"context"
"errors"
"flag"
"fmt"
"io"
2026-08-30 14:56:22 +00:00
"sort"
2026-08-30 14:43:25 +00:00
"strings"
"gitea.maximumdirect.net/eric/narratio/internal/config"
)
type inspectionFlags struct {
pipelinePath string
campaignPath string
campaignFilePath string
profile singletonStringFlag
}
type inspectionConfig struct {
PipelinePath string
Pipeline * config . PipelineConfig
2026-08-30 14:56:22 +00:00
CampaignPath string
Campaign * config . CampaignConfig
Party config . ResolvedParty
2026-08-30 14:43:25 +00:00
}
// Config dispatches read-only pipeline configuration commands.
func Config ( ctx context . Context , args [ ] string , out io . Writer ) error {
if len ( args ) == 0 {
2026-08-30 15:10:38 +00:00
return fmt . Errorf ( "config: expected subcommand: validate|show|sources|diff" )
2026-08-30 14:43:25 +00:00
}
if args [ 0 ] == "--help" || args [ 0 ] == "-h" {
writeConfigCommandUsage ( out )
return nil
}
switch args [ 0 ] {
case "validate" :
return ConfigValidate ( ctx , args [ 1 : ] , out )
case "show" :
return ConfigShow ( ctx , args [ 1 : ] , out )
2026-08-30 14:56:22 +00:00
case "sources" :
return ConfigSources ( ctx , args [ 1 : ] , out )
2026-08-30 15:10:38 +00:00
case "diff" :
return ConfigDiff ( ctx , args [ 1 : ] , out )
2026-08-30 14:43:25 +00:00
default :
return fmt . Errorf ( "config: unknown subcommand %q" , args [ 0 ] )
}
}
// ConfigValidate validates one fully resolved effective pipeline without
// loading session state or constructing runtime collaborators.
func ConfigValidate ( _ context . Context , args [ ] string , out io . Writer ) error {
flags , err := parseInspectionFlags ( "config validate" , args , out )
if err != nil {
if errors . Is ( err , flag . ErrHelp ) {
return nil
}
return err
}
resolved , err := resolveInspectionConfig ( flags )
if err != nil {
return fmt . Errorf ( "config validate: %w" , err )
}
_ , err = fmt . Fprintf ( out , "Configuration valid: root=%s; profile=%s; digest=%s\n" ,
inspectionRootPath ( resolved ) , inspectionProfile ( resolved . Pipeline ) , config . EffectivePipelineDigest ( resolved . Pipeline ) )
return err
}
// ConfigShow validates and renders one fully resolved effective pipeline
// without loading session state or constructing runtime collaborators.
func ConfigShow ( _ context . Context , args [ ] string , out io . Writer ) error {
flags , err := parseInspectionFlags ( "config show" , args , out )
if err != nil {
if errors . Is ( err , flag . ErrHelp ) {
return nil
}
return err
}
resolved , err := resolveInspectionConfig ( flags )
if err != nil {
return fmt . Errorf ( "config show: %w" , err )
}
data , err := config . MarshalEffectivePipeline ( resolved . Pipeline )
if err != nil {
return fmt . Errorf ( "config show: %w" , err )
}
_ , err = out . Write ( data )
return err
}
2026-08-30 14:56:22 +00:00
// ConfigSources validates and reports deterministic effective configuration
// ownership without loading session state or runtime collaborators.
func ConfigSources ( _ context . Context , args [ ] string , out io . Writer ) error {
flags , err := parseInspectionFlags ( "config sources" , args , out )
if err != nil {
if errors . Is ( err , flag . ErrHelp ) {
return nil
}
return err
}
resolved , err := resolveInspectionConfig ( flags )
if err != nil {
return fmt . Errorf ( "config sources: %w" , err )
}
records , err := config . EffectivePipelineSources ( resolved . Pipeline , resolved . Party )
if err != nil {
return fmt . Errorf ( "config sources: %w" , err )
}
records = append ( records , config . EffectiveCampaignSources ( resolved . CampaignPath , resolved . Campaign , resolved . Party ) ... )
sortInspectionSourceRecords ( records )
if err := writeInspectionSourceHeader ( out , resolved ) ; err != nil {
return err
}
for _ , record := range records {
if _ , err := fmt . Fprintf ( out , "%s\t%s\t%s\n" , record . Path , record . Role , record . Source ) ; err != nil {
return err
}
}
return nil
}
2026-08-30 14:43:25 +00:00
func parseInspectionFlags ( command string , args [ ] string , out io . Writer ) ( inspectionFlags , error ) {
fs := flag . NewFlagSet ( command , flag . ContinueOnError )
fs . SetOutput ( out )
var flags inspectionFlags
fs . StringVar ( & flags . pipelinePath , "config" , "" , "path to pipeline.yml (optional; defaults searched)" )
fs . StringVar ( & flags . campaignPath , "campaign" , "" , "campaign ID" )
fs . StringVar ( & flags . campaignFilePath , "campaign-file" , "" , "path to campaign.yml" )
flags . profile . name = "profile"
fs . Var ( & flags . profile , "profile" , "named pipeline profile" )
fs . Usage = func ( ) {
fmt . Fprintf ( out , "Usage: narratio %s [--config <pipeline.yml>] [--campaign <id> | --campaign-file <campaign.yml>] [--profile <name>]\n\n" , command )
fmt . Fprintln ( out , "Resolves configuration without a session, workspace, manifest, adapters, or external services." )
fmt . Fprintln ( out )
fmt . Fprintln ( out , "Flags:" )
fs . PrintDefaults ( )
}
if err := fs . Parse ( args ) ; err != nil {
return inspectionFlags { } , fmt . Errorf ( "%s: invalid flags: %w" , command , err )
}
if fs . NArg ( ) != 0 {
return inspectionFlags { } , fmt . Errorf ( "%s: unexpected positional arguments" , command )
}
if strings . TrimSpace ( flags . campaignPath ) != "" && strings . TrimSpace ( flags . campaignFilePath ) != "" {
return inspectionFlags { } , fmt . Errorf ( "%s: --campaign and --campaign-file are mutually exclusive" , command )
}
return flags , nil
}
func resolveInspectionConfig ( flags inspectionFlags ) ( * inspectionConfig , error ) {
pipelinePath , pipeline , err := loadPipelineConfig ( flags . pipelinePath , config . PipelineLoadOptions { Profile : flags . profile . pointer ( ) } )
if err != nil {
return nil , err
}
needsCampaign := pipelineHasArtifactFamilies ( pipeline )
hasCampaignSelection := strings . TrimSpace ( flags . campaignPath ) != "" || strings . TrimSpace ( flags . campaignFilePath ) != ""
if ! needsCampaign && ! hasCampaignSelection {
if err := validateInspectionPipeline ( pipelinePath , pipeline ) ; err != nil {
return nil , err
}
return & inspectionConfig { PipelinePath : pipelinePath , Pipeline : pipeline } , nil
}
campaignPath , err := resolveCampaignConfigPath ( pipeline , flags . campaignPath , flags . campaignFilePath )
if err != nil {
if needsCampaign {
return nil , fmt . Errorf ( "pipeline.scriptorium.artifact_families requires a campaign: %w" , err )
}
return nil , err
}
campaign , err := config . LoadCampaign ( campaignPath )
if err != nil {
return nil , err
}
if selectedID := strings . TrimSpace ( flags . campaignPath ) ; selectedID != "" && strings . TrimSpace ( flags . campaignFilePath ) == "" {
if got := config . CampaignID ( campaign ) ; got != selectedID {
return nil , fmt . Errorf ( "campaign config %q invalid: campaign_id %q does not match selected campaign %q" , campaignPath , got , selectedID )
}
}
loaded , err := config . LoadPipelineCampaign ( pipelinePath , pipeline , campaignPath , campaign )
if err != nil {
return nil , err
}
if err := validateInspectionPipeline ( loaded . PipelinePath , loaded . Pipeline ) ; err != nil {
return nil , err
}
if err := validateInspectionCampaign ( loaded . CampaignPath , loaded . Campaign ) ; err != nil {
return nil , err
}
return & inspectionConfig {
PipelinePath : loaded . PipelinePath ,
Pipeline : loaded . Pipeline ,
2026-08-30 14:56:22 +00:00
CampaignPath : loaded . CampaignPath ,
Campaign : loaded . Campaign ,
Party : loaded . Party ,
2026-08-30 14:43:25 +00:00
} , nil
}
func pipelineHasArtifactFamilies ( pipeline * config . PipelineConfig ) bool {
return pipeline != nil && pipeline . Scriptorium != nil && len ( pipeline . Scriptorium . ArtifactFamilies ) != 0
}
func validateInspectionPipeline ( path string , pipeline * config . PipelineConfig ) error {
if err := config . ValidatePipelineConfig ( pipeline ) ; err != nil {
return fmt . Errorf ( "pipeline config %q invalid: %w" , path , err )
}
return nil
}
func validateInspectionCampaign ( path string , campaign * config . CampaignConfig ) error {
if err := config . ValidateCampaignConfig ( campaign ) ; err != nil {
return fmt . Errorf ( "campaign config %q invalid: %w" , path , err )
}
return nil
}
func inspectionRootPath ( resolved * inspectionConfig ) string {
if resolved == nil {
return ""
}
if root := config . EffectivePipelineRootPath ( resolved . Pipeline ) ; root != "" {
return root
}
return resolved . PipelinePath
}
func inspectionProfile ( pipeline * config . PipelineConfig ) string {
if profile , ok := config . SelectedPipelineProfile ( pipeline ) ; ok {
return profile . Name
}
return "none"
}
func writeConfigCommandUsage ( out io . Writer ) {
2026-08-30 15:10:38 +00:00
fmt . Fprintln ( out , "Usage: narratio config <validate|show|sources|diff>" )
2026-08-30 14:43:25 +00:00
fmt . Fprintln ( out )
2026-08-30 15:10:38 +00:00
fmt . Fprintln ( out , "Use config validate to check an effective pipeline, config show to print normalized YAML, config sources to report ownership, or config diff to compare two profiles." )
2026-08-30 14:56:22 +00:00
}
func writeInspectionSourceHeader ( out io . Writer , resolved * inspectionConfig ) error {
if _ , err := fmt . Fprintf ( out , "root: %s\n" , inspectionRootPath ( resolved ) ) ; err != nil {
return err
}
imports := config . EffectivePipelineImports ( resolved . Pipeline )
if len ( imports ) == 0 {
if _ , err := fmt . Fprintln ( out , "imports: none" ) ; err != nil {
return err
}
}
for _ , imported := range imports {
if _ , err := fmt . Fprintf ( out , "import: %s\n" , config . NormalizedConfigurationPath ( imported ) ) ; err != nil {
return err
}
}
if profile , ok := config . SelectedPipelineProfile ( resolved . Pipeline ) ; ok {
if _ , err := fmt . Fprintf ( out , "profile: name=%s selection=%s overlay=%s\n" , profile . Name , profile . Source , config . NormalizedConfigurationPath ( profile . OverlayPath ) ) ; err != nil {
return err
}
} else if _ , err := fmt . Fprintln ( out , "profile: none" ) ; err != nil {
return err
}
if resolved . Campaign == nil {
if _ , err := fmt . Fprintln ( out , "campaign: none" ) ; err != nil {
return err
}
if _ , err := fmt . Fprintln ( out , "party: none" ) ; err != nil {
return err
}
} else {
if _ , err := fmt . Fprintf ( out , "campaign: id=%s source=%s\n" , config . CampaignID ( resolved . Campaign ) , config . NormalizedConfigurationPath ( resolved . CampaignPath ) ) ; err != nil {
return err
}
2026-09-02 12:04:35 +00:00
if _ , err := fmt . Fprintf ( out , "party: source=%s\n" , config . NormalizedConfigurationPath ( resolved . Party . Source . Path ) ) ; err != nil {
2026-08-30 14:56:22 +00:00
return err
}
}
if _ , err := fmt . Fprintf ( out , "digest: %s\n" , config . EffectivePipelineDigest ( resolved . Pipeline ) ) ; err != nil {
return err
}
_ , err := fmt . Fprintln ( out , "path\trole\tsource" )
return err
}
func sortInspectionSourceRecords ( records [ ] config . EffectivePipelineSourceRecord ) {
sort . Slice ( records , func ( left , right int ) bool {
if records [ left ] . Path != records [ right ] . Path {
return records [ left ] . Path < records [ right ] . Path
}
if records [ left ] . Role != records [ right ] . Role {
return records [ left ] . Role < records [ right ] . Role
}
return records [ left ] . Source < records [ right ] . Source
} )
2026-08-30 14:43:25 +00:00
}