Adopt native command routing and inspection
This commit is contained in:
@@ -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
|
||||
|
||||
74
internal/cli/app_wiring.go
Normal file
74
internal/cli/app_wiring.go
Normal file
@@ -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...)
|
||||
}
|
||||
77
internal/cli/app_wiring_test.go
Normal file
77
internal/cli/app_wiring_test.go
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
85
internal/cli/process_command.go
Normal file
85
internal/cli/process_command.go
Normal file
@@ -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 })},
|
||||
})
|
||||
}
|
||||
@@ -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 <command> [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 <command> [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 <path> [--overlay <name>] [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 <path>] [--overlay <name>] [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 <transcript.json> [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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user