From 8cc298d0924e0b25a364988298097ceb843a2ab7 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 10 Nov 2026 16:55:56 +0000 Subject: [PATCH] Adopt native command routing and inspection --- docs/roadmap/implementation.md | 5 + internal/cli/app_wiring.go | 74 ++++++++++ internal/cli/app_wiring_test.go | 77 ++++++++++ internal/cli/config_input.go | 193 +------------------------ internal/cli/process.go | 85 ----------- internal/cli/process_command.go | 85 +++++++++++ internal/cli/run.go | 241 +------------------------------- 7 files changed, 248 insertions(+), 512 deletions(-) create mode 100644 internal/cli/app_wiring.go create mode 100644 internal/cli/app_wiring_test.go create mode 100644 internal/cli/process_command.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 3e0b1dd..ab21d83 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -237,6 +237,11 @@ secret-safe failure evidence. Leave stage client selection to Stage 12. ## Stage 5: Install CommandKit routing and native configuration/resource scopes +**Completed.** CommandKit owns routing and native inspection. The typed process +declaration awaits its processing handler, preflight, and later selection flags +as assigned below. Existing process code and earlier parser tests remain to be +reconciled; focused declaration and inspection checks passed independently. + Read [CommandKit operator experience](commandkit.md#agreed-operator-experience) and the released consumer guide linked there. Replace the routing/parser/inspection parts of `internal/cli`; keep `cmd/audita` responsible for process lifetime and diff --git a/internal/cli/app_wiring.go b/internal/cli/app_wiring.go new file mode 100644 index 0000000..6df82ee --- /dev/null +++ b/internal/cli/app_wiring.go @@ -0,0 +1,74 @@ +package cli + +import ( + "context" + "fmt" + "reflect" + "strings" + + "gitea.maximumdirect.net/eric/audita/v3/internal/core/config" + "gitea.maximumdirect.net/eric/audita/v3/internal/framework/llm" + "go.maximumdirect.net/ckconfig" + "go.maximumdirect.net/ckprompt" + commandkit "go.maximumdirect.net/commandkit" + ck "go.maximumdirect.net/configkit" + pk "go.maximumdirect.net/promptkit" + "go.maximumdirect.net/releasekit/buildinfo" +) + +type invocationState struct{ engines []engineEntry } +type engineEntry struct { + selection ckconfig.Selection + engine *pk.Engine +} + +func loadApplication(ctx context.Context, selection ckconfig.Selection, _ struct{}, _ *invocationState) (*ck.Result[config.Config], error) { + if err := ctx.Err(); err != nil { + return nil, err + } + var source ck.Source + if selection.Config.Set { + if strings.TrimSpace(selection.Config.Value) == "" { + return nil, fmt.Errorf("--config requires a nonempty path") + } + source = ck.File(selection.Config.Value) + } else { + source = ck.SearchConfig("audita", ck.Searches(ck.WorkingDirectory(), ck.UserConfigDirectory(), ck.UnixSystemDirectories())) + } + return config.Load(ck.Input{Source: source}, selection.Overlay.Value, selection.Overrides...) +} + +func loadPromptEngine(ctx context.Context, selection ckconfig.Selection, input struct{}, state *invocationState) (*pk.Engine, error) { + if state != nil { + for _, entry := range state.engines { + if reflect.DeepEqual(entry.selection, selection) { + return entry.engine, nil + } + } + } + loaded, err := loadApplication(ctx, selection, input, state) + if err != nil { + return nil, err + } + runtime, err := llm.NewRuntime(ctx, loaded, nil) + if err != nil { + return nil, err + } + if state != nil { + state.engines = append(state.engines, engineEntry{selection: selection, engine: runtime.Engine}) + } + return runtime.Engine, nil +} + +func buildApplication(extra ...commandkit.Declaration) (*commandkit.App, error) { + appScope := ckconfig.FromConfigKit("app", "Audita application settings.", loadApplication) + appScope.Features.Overrides = true + promptScope := ckprompt.ConfigScope(loadPromptEngine) + configCommand := ckconfig.Command(ckconfig.Suite[struct{}, *invocationState]{NewState: func() *invocationState { return &invocationState{} }, Scopes: []ckconfig.Scope[struct{}, *invocationState]{appScope, promptScope}}) + definitions := ckprompt.WithNamespaces(ckprompt.FromEngine(loadPromptEngine), pk.Namespaces{}) + definitions.NewState = func() *invocationState { return &invocationState{} } + declarations := []commandkit.Declaration{configCommand, commandkit.Completion("audita"), processCommand()} + declarations = append(declarations, ckprompt.Declarations(definitions)...) + declarations = append(declarations, extra...) + return commandkit.New(commandkit.Identity{Name: "audita", Version: buildinfo.Version()}, declarations...) +} diff --git a/internal/cli/app_wiring_test.go b/internal/cli/app_wiring_test.go new file mode 100644 index 0000000..17b55f7 --- /dev/null +++ b/internal/cli/app_wiring_test.go @@ -0,0 +1,77 @@ +package cli + +import ( + "bytes" + "context" + "os" + "path/filepath" + "strings" + "testing" + + commandkit "go.maximumdirect.net/commandkit" +) + +func TestNativeConfigurationAndResourceCommands(t *testing.T) { + dir := t.TempDir() + file := filepath.Join(dir, "config.yml") + if err := os.WriteFile(file, []byte("version: 3\ncontext:\n description: example\n"), 0644); err != nil { + t.Fatal(err) + } + app, err := buildApplication() + if err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + args []string + contains string + }{ + {[]string{"--help"}, "audita"}, + {[]string{"version"}, "audita"}, + {[]string{"process", "--help"}, "transcript"}, + {[]string{"config", "show", "--scope", "app", "--config", file}, "example"}, + {[]string{"promptkit", "list", "prompts", "--config", file}, "audita/modules.grammar.proposal"}, + } { + var out, stderr bytes.Buffer + result := app.Execute(context.Background(), tc.args, commandkit.Streams{Out: &out, Err: &stderr}) + if result.Status != 0 || !strings.Contains(out.String(), tc.contains) { + t.Errorf("%v: status=%d out=%q err=%q", tc.args, result.Status, out.String(), stderr.String()) + } + } +} + +func TestNativeConfigurationSelectionsAndComparison(t *testing.T) { + dir := t.TempDir() + first, second := filepath.Join(dir, "first.yml"), filepath.Join(dir, "second.yml") + for name, data := range map[string]string{first: "context:\n description: before\n", second: "context:\n description: after\n"} { + if err := os.WriteFile(name, []byte(data), 0644); err != nil { + t.Fatal(err) + } + } + app, err := buildApplication() + if err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + outcome := app.Execute(context.Background(), []string{"config", "diff", "--details", "--check", "--scope=app", "--config", first, "--against-config", second, "--set=/context/description=\"left\"", "--against-set=/context/description=\"right\""}, commandkit.Streams{Out: &out, Err: &stderr}) + if outcome.Status != 3 || !strings.Contains(out.String(), "left") || !strings.Contains(out.String(), "right") { + t.Fatalf("status=%d output=%q err=%q", outcome.Status, out.String(), stderr.String()) + } + out.Reset() + stderr.Reset() + outcome = app.Execute(context.Background(), []string{"config", "show", "--scope=app", "--config", filepath.Join(dir, "missing.yml")}, commandkit.Streams{Out: &out, Err: &stderr}) + if outcome.Status == 0 { + t.Fatalf("missing file accepted: %s", out.String()) + } +} + +func TestExplicitEmptyConfigurationPathFails(t *testing.T) { + app, err := buildApplication() + if err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + result := app.Execute(context.Background(), []string{"config", "show", "--scope=app", "--config="}, commandkit.Streams{Out: &out, Err: &stderr}) + if result.Status == 0 { + t.Fatal("explicit empty path accepted") + } +} diff --git a/internal/cli/config_input.go b/internal/cli/config_input.go index bb34897..e9a5f4e 100644 --- a/internal/cli/config_input.go +++ b/internal/cli/config_input.go @@ -1,208 +1,19 @@ package cli import ( - "errors" - "flag" "fmt" - "io" - "path/filepath" - "strconv" - "strings" - "gitea.maximumdirect.net/eric/audita/v3/internal/core/config" ck "go.maximumdirect.net/configkit" + "path/filepath" + "strings" ) -type configInput struct { - ConfigPath string - ConfigPathSet bool - Overlay string - View string - TranscriptPath string - GlossaryPath string - OutputPath string - ReportDirSet bool - Overrides []ck.Override -} - type selectedConfig struct { Path string Source string Result *ck.Result[config.Config] } -// parseConfigInput owns the common setting flags and process-only path flags. -// A transcript before flags and one after flags are both accepted. -func parseConfigInput(command string, args []string, output io.Writer) (configInput, bool, error) { - var input configInput - fs := flag.NewFlagSet(command, flag.ContinueOnError) - fs.SetOutput(output) - configPath := fs.String("config", "", "Main configuration YAML file") - overlay := fs.String("overlay", "", "Selected configuration overlay") - if command == "print-effective" { - fs.String("view", "settings", "Effective view: settings, profiles, or explain") - } - settingPaths := map[string]ck.Path{ - "proposal-profile": ck.Keys("llm", "proposal_profile"), - "validation-profile": ck.Keys("llm", "validation_profile"), - "output-schema": ck.Keys("output", "schema"), - "validation-max-prompt-tokens": ck.Keys("validation", "max_prompt_tokens"), - "target-sections": ck.Keys("chunking", "target_sections"), - "max-section-tokens": ck.Keys("chunking", "max_section_tokens"), - "min-section-tokens": ck.Keys("chunking", "min_section_tokens"), - "normalize-max-segment-gap": ck.Keys("normalization", "max_segment_gap"), - "normalize-ellipsis-gap": ck.Keys("normalization", "ellipsis_gap"), - "normalize-max-segment-duration": ck.Keys("normalization", "max_segment_duration"), - "normalize-max-segment-tokens": ck.Keys("normalization", "max_segment_tokens"), - "glossary-confidence-threshold": ck.Keys("thresholds", "glossary"), - "homophones-confidence-threshold": ck.Keys("thresholds", "homophones"), - "spoken-word-confidence-threshold": ck.Keys("thresholds", "spoken_word"), - "grammar-confidence-threshold": ck.Keys("thresholds", "grammar"), - "transcript-description": ck.Keys("context", "description"), - "reporting": ck.Keys("reporting", "enabled"), - "report-dir": ck.Keys("reporting", "stdout_dir"), - "debug": ck.Keys("diagnostics", "debug"), - "work-dir": ck.Keys("diagnostics", "work_dir"), - "modules": ck.Keys("pipeline", "modules"), - } - for name := range settingPaths { - switch name { - case "reporting", "debug": - fs.Bool(name, false, "Setting override") - case "validation-max-prompt-tokens", "target-sections", "max-section-tokens", "min-section-tokens", "normalize-max-segment-tokens": - fs.Int(name, 0, "Setting override") - case "normalize-max-segment-gap", "normalize-ellipsis-gap", "normalize-max-segment-duration", "glossary-confidence-threshold", "homophones-confidence-threshold", "spoken-word-confidence-threshold", "grammar-confidence-threshold": - fs.Float64(name, 0, "Setting override") - default: - fs.String(name, "", "Setting override") - } - } - if command == "process" { - fs.String("glossary", "", "Glossary YAML file") - fs.String("output", "", "Corrected transcript output file") - } - parseArgs := args - front := "" - if command == "process" && len(args) > 0 && !strings.HasPrefix(args[0], "-") { - front, parseArgs = args[0], args[1:] - } - if err := fs.Parse(parseArgs); err != nil { - if errors.Is(err, flag.ErrHelp) { - return configInput{}, true, nil - } - return configInput{}, false, err - } - positions := fs.Args() - if front != "" { - positions = append([]string{front}, positions...) - } - if command == "process" { - if len(positions) != 1 { - return configInput{}, false, fmt.Errorf("expected exactly one transcript JSON path") - } - input.TranscriptPath = positions[0] - input.GlossaryPath = fs.Lookup("glossary").Value.String() - input.OutputPath = fs.Lookup("output").Value.String() - if strings.TrimSpace(input.GlossaryPath) == "" { - return configInput{}, false, fmt.Errorf("--glossary is required") - } - } else if len(positions) != 0 { - return configInput{}, false, fmt.Errorf("unexpected positional arguments") - } - input.ConfigPath, input.Overlay = *configPath, *overlay - if command == "print-effective" { - input.View = fs.Lookup("view").Value.String() - switch input.View { - case "settings", "profiles", "explain": - default: - return configInput{}, false, fmt.Errorf("unknown effective view %q", input.View) - } - } - fs.Visit(func(f *flag.Flag) { - if f.Name == "config" { - input.ConfigPathSet = true - } - if f.Name == "report-dir" { - input.ReportDirSet = true - } - path, ok := settingPaths[f.Name] - if !ok { - return - } - var value any = f.Value.String() - switch f.Name { - case "reporting", "debug": - value, _ = strconv.ParseBool(f.Value.String()) - case "modules": - modules, err := config.ParseModulesCSV(f.Value.String()) - if err != nil { - value = err - } else { - values := make([]any, len(modules)) - for i, module := range modules { - values[i] = module - } - value = values - } - case "validation-max-prompt-tokens", "target-sections", "max-section-tokens", "min-section-tokens", "normalize-max-segment-tokens": - value, _ = strconv.Atoi(f.Value.String()) - case "normalize-max-segment-gap", "normalize-ellipsis-gap", "normalize-max-segment-duration", "glossary-confidence-threshold", "homophones-confidence-threshold", "spoken-word-confidence-threshold", "grammar-confidence-threshold": - value, _ = strconv.ParseFloat(f.Value.String(), 64) - } - input.Overrides = append(input.Overrides, ck.Override{Path: path, Value: value, Flag: "--" + f.Name}) - }) - for _, override := range input.Overrides { - if err, ok := override.Value.(error); ok { - return configInput{}, false, fmt.Errorf("%s: %w", override.Flag, err) - } - } - return input, false, nil -} - -func loadConfigInput(input configInput, command string) (selectedConfig, error) { - var source ck.Source - if input.ConfigPathSet { - if strings.TrimSpace(input.ConfigPath) == "" { - return selectedConfig{}, fmt.Errorf("--config requires a nonempty path") - } - source = ck.File(input.ConfigPath) - } else { - source = ck.SearchConfig("audita", ck.Searches(ck.WorkingDirectory(), ck.UserConfigDirectory(), ck.UnixSystemDirectories())) - } - loaded, err := config.Load(ck.Input{Source: source}, input.Overlay, input.Overrides...) - if err != nil { - return selectedConfig{}, err - } - selected := selectedConfig{Result: loaded} - for _, info := range loaded.Sources() { - if info.Layer == ck.LayerMain && info.ImportedFrom == nil { - selected.Path = info.Path - if input.ConfigPathSet { - selected.Source = "flag" - } else { - selected.Source = "default" - } - break - } - } - return selected, nil -} - -func configInputError(err error) string { - var loadErr *ck.LoadError - if !errors.As(err, &loadErr) { - return err.Error() - } - var messages []string - for _, diagnostic := range loadErr.Diagnostics() { - messages = append(messages, diagnostic.Message) - } - if len(messages) == 0 { - return err.Error() - } - return strings.Join(messages, "; ") -} - func selectedReportDir(cfg config.Config, outputPath string, reportDirSet bool) (string, error) { if !cfg.Reporting.Enabled { return "", nil diff --git a/internal/cli/process.go b/internal/cli/process.go index 914abc8..fcabf3f 100644 --- a/internal/cli/process.go +++ b/internal/cli/process.go @@ -30,91 +30,6 @@ import ( "gitea.maximumdirect.net/eric/audita/v3/internal/framework/structuredoutput" ) -func runProcess(ctx context.Context, args []string, stdout, stderr io.Writer) int { - input, help, err := parseConfigInput("process", args, stderr) - if err != nil { - fmt.Fprintf(stderr, "audita process: %s\n", configInputError(err)) - return 2 - } - if help { - writeProcessUsage(stdout) - return 0 - } - selected, err := loadConfigInput(input, "process") - if err != nil { - fmt.Fprintf(stderr, "audita process: %s\n", configInputError(err)) - return 2 - } - cfg := selected.Result.Config - reportDir, err := selectedReportDir(cfg, input.OutputPath, input.ReportDirSet) - if err != nil { - fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(err, cfg)) - return 2 - } - if cfg.Diagnostics.Debug && strings.TrimSpace(cfg.Diagnostics.WorkDir) == "" { - fmt.Fprintln(stderr, "audita process: debug work directory is required") - return 2 - } - if err := ctx.Err(); err != nil { - fmt.Fprintf(stderr, "audita process: %s\n", err) - return 1 - } - runtime, err := llm.NewRuntime(ctx, cfg, selected.Path, nil) - if err != nil { - fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(err, cfg)) - if ctx.Err() != nil || llm.IsAssetError(err) { - return 1 - } - return 2 - } - specs, err := contracts.ResolveModuleRunSpecs(cfg.Pipeline.Modules) - if err != nil { - fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(err, cfg)) - return 2 - } - runID, err := generateRunID() - if err != nil { - fmt.Fprintf(stderr, "audita process: run ID: %s\n", llm.RedactError(err, cfg)) - return 1 - } - started := time.Now().UTC() - state := &processState{cfg: cfg, runtime: runtime, specs: specs, runID: runID, started: started, - transcriptPath: input.TranscriptPath, glossaryPath: input.GlossaryPath, outputPath: input.OutputPath, - selected: selected, overlay: input.Overlay, stderr: stderr, stdout: stdout, ctx: ctx} - protected := []string{input.GlossaryPath} - for _, source := range selected.Result.Sources() { - if source.Path != "" { - protected = append(protected, source.Path) - } - } - protected = append(protected, runtime.SourcePaths...) - session, err := diagnostics.PrepareOutput(diagnostics.OutputSelection{InputTranscriptPath: input.TranscriptPath, TranscriptPath: input.OutputPath, - ReportDir: reportDir, ReportingEnabled: cfg.Reporting.Enabled, ProtectedPaths: protected}) - if err != nil { - fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(err, cfg)) - return 2 - } - state.session = session - initial := state.snapshot(reporting.StatusInProgress, nil, nil) - initialBundle, err := state.bundle(initial) - if err != nil { - fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(err, cfg)) - return 1 - } - initFailure := []processreport.RunFailure{{Phase: "initialization", Reason: outcomes.ReasonInternalFailure}} - failedInit, err := state.bundle(state.snapshot(reporting.StatusFailed, nil, initFailure)) - if err != nil { - fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(err, cfg)) - return 1 - } - if err := session.Initialize(initialBundle, failedInit); err != nil { - fmt.Fprintf(stderr, "audita process: %s\n", llm.RedactError(err, cfg)) - return 1 - } - state.startDebug() - return state.execute() -} - type processState struct { cfg config.Config runtime *llm.Runtime diff --git a/internal/cli/process_command.go b/internal/cli/process_command.go new file mode 100644 index 0000000..d0efa18 --- /dev/null +++ b/internal/cli/process_command.go @@ -0,0 +1,85 @@ +package cli + +import ( + "strconv" + + commandkit "go.maximumdirect.net/commandkit" + ck "go.maximumdirect.net/configkit" +) + +type processInput struct { + Config commandkit.Supplied[string] + Overlay commandkit.Supplied[string] + Transcript string + Output commandkit.Supplied[string] + Debug commandkit.Supplied[bool] + Overrides []ck.Override +} + +func processCommand() commandkit.Command { + stringValue := func(s string) (string, error) { return s, nil } + flags := []commandkit.Flag[processInput]{ + commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Application configuration file."}, (*string)(nil), stringValue, func(in *processInput, v commandkit.Supplied[string]) { in.Config = v }), + commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Configuration overlay."}, (*string)(nil), stringValue, func(in *processInput, v commandkit.Supplied[string]) { in.Overlay = v }), + commandkit.ValueFlag(commandkit.FlagMeta{Name: "output", Usage: "Transcript output file."}, (*string)(nil), stringValue, func(in *processInput, v commandkit.Supplied[string]) { in.Output = v }), + commandkit.BoolFlag(commandkit.FlagMeta{Name: "debug", Usage: "Capture diagnostic artifacts."}, false, func(in *processInput, v commandkit.Supplied[bool]) { in.Debug = v }), + } + for _, item := range []struct { + name string + path ck.Path + }{ + {"proposal-profile", ck.Keys("llm", "proposal_profile")}, {"validation-profile", ck.Keys("llm", "validation_profile")}, + {"output-schema", ck.Keys("output", "schema")}, {"transcript-description", ck.Keys("context", "description")}, + {"report-dir", ck.Keys("reporting", "stdout_dir")}, {"work-dir", ck.Keys("diagnostics", "work_dir")}, + } { + name, path := item.name, item.path + flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: name, Usage: "Override application setting."}, (*string)(nil), stringValue, + func(in *processInput, v commandkit.Supplied[string]) { + if v.Set { + in.Overrides = append(in.Overrides, ck.Override{Path: path, Value: v.Value, Flag: "--" + name}) + } + })) + } + for _, item := range []struct { + name string + path ck.Path + }{ + {"validation-max-prompt-tokens", ck.Keys("validation", "max_prompt_tokens")}, {"target-sections", ck.Keys("chunking", "target_sections")}, + {"max-section-tokens", ck.Keys("chunking", "max_section_tokens")}, {"min-section-tokens", ck.Keys("chunking", "min_section_tokens")}, + {"normalize-max-segment-tokens", ck.Keys("normalization", "max_segment_tokens")}, + } { + name, path := item.name, item.path + flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: name, Usage: "Override application setting."}, (*int)(nil), strconv.Atoi, + func(in *processInput, v commandkit.Supplied[int]) { + if v.Set { + in.Overrides = append(in.Overrides, ck.Override{Path: path, Value: v.Value, Flag: "--" + name}) + } + })) + } + for _, item := range []struct { + name string + path ck.Path + }{ + {"normalize-max-segment-gap", ck.Keys("normalization", "max_segment_gap")}, {"normalize-ellipsis-gap", ck.Keys("normalization", "ellipsis_gap")}, + {"normalize-max-segment-duration", ck.Keys("normalization", "max_segment_duration")}, {"glossary-confidence-threshold", ck.Keys("thresholds", "glossary")}, + {"homophones-confidence-threshold", ck.Keys("thresholds", "homophones")}, {"spoken-word-confidence-threshold", ck.Keys("thresholds", "spoken_word")}, + {"grammar-confidence-threshold", ck.Keys("thresholds", "grammar")}, + } { + name, path := item.name, item.path + flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: name, Usage: "Override application setting."}, (*float64)(nil), func(s string) (float64, error) { return strconv.ParseFloat(s, 64) }, + func(in *processInput, v commandkit.Supplied[float64]) { + if v.Set { + in.Overrides = append(in.Overrides, ck.Override{Path: path, Value: v.Value, Flag: "--" + name}) + } + })) + } + flags = append(flags, commandkit.BoolFlag(commandkit.FlagMeta{Name: "reporting", Usage: "Enable ordinary reports."}, false, + func(in *processInput, v commandkit.Supplied[bool]) { + if v.Set { + in.Overrides = append(in.Overrides, ck.Override{Path: ck.Keys("reporting", "enabled"), Value: v.Value, Flag: "--reporting"}) + } + })) + return commandkit.NewCommand(commandkit.CommandSpec[processInput]{Name: "process", Summary: "Polish a transcript.", Flags: flags, + Arguments: []commandkit.Argument[processInput]{commandkit.ValueArgument(commandkit.ArgumentMeta{Name: "transcript", Usage: "Transcript JSON file."}, true, stringValue, func(in *processInput, v commandkit.Supplied[string]) { in.Transcript = v.Value })}, + }) +} diff --git a/internal/cli/run.go b/internal/cli/run.go index 047f7bf..6b3aa83 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -2,13 +2,11 @@ package cli import ( "context" - "flag" "fmt" "io" - "go.maximumdirect.net/releasekit/buildinfo" "gitea.maximumdirect.net/eric/audita/v3/internal/core/schema" - "gitea.maximumdirect.net/eric/audita/v3/internal/framework/llm" + commandkit "go.maximumdirect.net/commandkit" ) func sourceToCanonicalTranscript(source *schema.SourceTranscript) *schema.Transcript { @@ -30,241 +28,12 @@ func sourceToCanonicalTranscript(source *schema.SourceTranscript) *schema.Transc return &schema.Transcript{Segments: segments} } -// Run executes the Audita CLI with the provided arguments and streams. +// Run executes the application with the provided arguments and streams. func Run(ctx context.Context, args []string, stdout, stderr io.Writer) int { - if len(args) == 0 { - writeRootUsage(stdout) - return 0 - } - if len(args) == 2 && args[0] == "help" { - switch args[1] { - case "process": - writeProcessUsage(stdout) - return 0 - case "config": - writeConfigUsage(stdout) - return 0 - case "version": - writeVersionUsage(stdout) - return 0 - } - } - - if isHelpCommand(args) { - writeRootUsage(stdout) - return 0 - } - - if args[0] == "process" { - return runProcess(ctx, args[1:], stdout, stderr) - } - if args[0] == "config" { - return runConfig(ctx, args[1:], stdout, stderr) - } - if args[0] == "version" { - return runVersion(args[1:], stdout, stderr) - } - - fmt.Fprintf(stderr, "audita: unknown command %q\n\n", args[0]) - writeRootUsage(stderr) - return 2 -} - -func runVersion(args []string, stdout, stderr io.Writer) int { - flags := flag.NewFlagSet("version", flag.ContinueOnError) - flags.SetOutput(stderr) - flags.Usage = func() {} // Route usage after parsing so help stays on stdout. - if err := flags.Parse(args); err != nil { - if err == flag.ErrHelp { - writeVersionUsage(stdout) - return 0 - } - writeVersionUsage(stderr) - return 2 - } - if positional := flags.Args(); len(positional) > 0 { - fmt.Fprintf(stderr, "audita version: unexpected argument %q\n\n", positional[0]) - writeVersionUsage(stderr) - return 2 - } - fmt.Fprintf(stdout, "audita %s\n", buildinfo.Version()) - return 0 -} - -func runConfig(ctx context.Context, args []string, stdout, stderr io.Writer) int { - if len(args) == 0 || isHelpCommand(args) || args[0] == "--help" || args[0] == "-h" { - writeConfigUsage(stdout) - return 0 - } - - switch args[0] { - case "validate": - return runConfigValidate(ctx, args[1:], stdout, stderr) - case "print-effective": - return runConfigPrintEffective(ctx, args[1:], stdout, stderr) - default: - fmt.Fprintf(stderr, "audita config: unknown command %q\n\n", args[0]) - writeConfigUsage(stderr) - return 2 - } -} - -func runConfigValidate(ctx context.Context, args []string, stdout, stderr io.Writer) int { - input, help, err := parseConfigInput("validate", args, stderr) + app, err := buildApplication() if err != nil { - fmt.Fprintf(stderr, "audita config validate: %s\n", configInputError(err)) - return 2 - } - if help { - writeConfigValidateUsage(stdout) - return 0 - } - selected, err := loadConfigInput(input, "validate") - if err != nil { - fmt.Fprintf(stderr, "audita config validate: %s\n", configInputError(err)) - return 2 - } - if _, err := llm.NewRuntime(ctx, selected.Result.Config, selected.Path, nil); err != nil { - fmt.Fprintf(stderr, "audita config validate: %s\n", llm.RedactError(err, selected.Result.Config)) - if ctx.Err() != nil || llm.IsAssetError(err) { - return 1 - } - return 2 - } - fmt.Fprintln(stdout, "config is valid") - return 0 -} - -func runConfigPrintEffective(ctx context.Context, args []string, stdout, stderr io.Writer) int { - input, help, err := parseConfigInput("print-effective", args, stderr) - if err != nil { - fmt.Fprintf(stderr, "audita config print-effective: %s\n", configInputError(err)) - return 2 - } - if help { - writeConfigPrintEffectiveUsage(stdout) - return 0 - } - selected, err := loadConfigInput(input, "print-effective") - if err != nil { - fmt.Fprintf(stderr, "audita config print-effective: %s\n", configInputError(err)) - return 2 - } - llmRuntime, err := llm.NewRuntime(ctx, selected.Result.Config, selected.Path, nil) - if err != nil { - fmt.Fprintf(stderr, "audita config print-effective: %s\n", llm.RedactError(err, selected.Result.Config)) - if ctx.Err() != nil || llm.IsAssetError(err) { - return 1 - } - return 2 - } - if input.View == "settings" { - err = selected.Result.WriteEffective(stdout) - } else { - var output []byte - if input.View == "profiles" { - output, err = llmRuntime.SelectedProfilesYAML() - } else { - output, err = llm.ExplanationYAML(selected.Result) - } - if err == nil { - _, err = stdout.Write(output) - } - } - if err != nil { - fmt.Fprintf(stderr, "audita config print-effective: %v\n", err) + fmt.Fprintln(stderr, err) return 1 } - return 0 -} - -func isHelpCommand(args []string) bool { - if len(args) == 0 { - return false - } - if len(args) == 1 { - switch args[0] { - case "help", "-h", "--help": - return true - } - } - if len(args) == 2 && args[0] == "help" { - switch args[1] { - case "process": - return true - } - } - return false -} - -func writeRootUsage(w io.Writer) { - fmt.Fprintln(w, "Audita is a transcript processing CLI.") - fmt.Fprintln(w) - fmt.Fprintln(w, "Usage:") - fmt.Fprintln(w, " audita [options]") - fmt.Fprintln(w) - fmt.Fprintln(w, "Commands:") - fmt.Fprintln(w, " process Process a transcript JSON file") - fmt.Fprintln(w, " config Validate and inspect config") - fmt.Fprintln(w, " version Print the Audita version") - fmt.Fprintln(w) - fmt.Fprintln(w, "Example:") - fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json") -} - -func writeVersionUsage(w io.Writer) { - fmt.Fprintln(w, "Print the Audita version.") - fmt.Fprintln(w) - fmt.Fprintln(w, "Usage:") - fmt.Fprintln(w, " audita version") -} - -func writeConfigUsage(w io.Writer) { - fmt.Fprintln(w, "Validate and inspect Audita config.") - fmt.Fprintln(w) - fmt.Fprintln(w, "Usage:") - fmt.Fprintln(w, " audita config [flags]") - fmt.Fprintln(w) - fmt.Fprintln(w, "Commands:") - fmt.Fprintln(w, " validate Validate a versioned YAML config file") - fmt.Fprintln(w, " print-effective Print redacted effective settings YAML") -} - -func writeConfigValidateUsage(w io.Writer) { - fmt.Fprintln(w, "Validate a versioned YAML config file.") - fmt.Fprintln(w) - fmt.Fprintln(w, "Usage:") - fmt.Fprintln(w, " audita config validate --config [--overlay ] [setting flags]") -} - -func writeConfigPrintEffectiveUsage(w io.Writer) { - fmt.Fprintln(w, "Print redacted effective settings YAML.") - fmt.Fprintln(w) - fmt.Fprintln(w, "Usage:") - fmt.Fprintln(w, " audita config print-effective [--config ] [--overlay ] [setting flags]") -} - -func writeProcessUsage(w io.Writer) { - fmt.Fprintln(w, "Process a transcript JSON file.") - fmt.Fprintln(w) - fmt.Fprintln(w, "Usage:") - fmt.Fprintln(w, " audita process [flags]") - fmt.Fprintln(w) - fmt.Fprintln(w, "Flags:") - for _, name := range []string{ - "config", "overlay", "glossary", "output", - "proposal-profile", "validation-profile", "modules", "output-schema", - "validation-max-prompt-tokens", "target-sections", "max-section-tokens", "min-section-tokens", - "normalize-max-segment-gap", "normalize-ellipsis-gap", "normalize-max-segment-duration", "normalize-max-segment-tokens", - "glossary-confidence-threshold", "homophones-confidence-threshold", "spoken-word-confidence-threshold", "grammar-confidence-threshold", - "transcript-description", "work-dir", - } { - fmt.Fprintf(w, " --%s\n", name) - } - fmt.Fprintln(w, " --reporting[=true|false] Write the three-file report bundle (default true)") - fmt.Fprintln(w, " --report-dir DIR Select the report directory (stdout setting for stdout; output parent by default for files)") - fmt.Fprintln(w, " --debug[=true|false] Persist detailed diagnostics (default false)") - fmt.Fprintln(w) - fmt.Fprintln(w, "Example:") - fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json") + return app.Execute(ctx, args, commandkit.Streams{Out: stdout, Err: stderr}).Status }