234 lines
8.3 KiB
Go
234 lines
8.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
ck "gitea.maximumdirect.net/eric/ConfigKit"
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
|
|
)
|
|
|
|
var systemConfigPaths = []string{"/usr/local/etc/audita/config.yml", "/etc/audita/config.yml"}
|
|
|
|
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 selectConfigPath(explicit string, set bool, stat func(string) (os.FileInfo, error), candidates []string) (string, string, error) {
|
|
if set {
|
|
if strings.TrimSpace(explicit) == "" {
|
|
return "", "", fmt.Errorf("--config requires a nonempty path")
|
|
}
|
|
if _, err := stat(explicit); err != nil {
|
|
return "", "", fmt.Errorf("config file %q: %w", explicit, err)
|
|
}
|
|
return explicit, "flag", nil
|
|
}
|
|
for _, path := range candidates {
|
|
if _, err := stat(path); err == nil {
|
|
return path, "default", nil
|
|
} else if !os.IsNotExist(err) {
|
|
return "", "", fmt.Errorf("config file %q: %w", path, err)
|
|
}
|
|
}
|
|
return "", "", fmt.Errorf("no configuration file found; select a config with backend and profile settings")
|
|
}
|
|
|
|
func loadConfigInput(input configInput, command string) (selectedConfig, error) {
|
|
if command == "validate" && (!input.ConfigPathSet || strings.TrimSpace(input.ConfigPath) == "") {
|
|
return selectedConfig{}, fmt.Errorf("--config is required")
|
|
}
|
|
path, source, err := selectConfigPath(input.ConfigPath, input.ConfigPathSet, os.Stat, systemConfigPaths)
|
|
if err != nil {
|
|
return selectedConfig{}, err
|
|
}
|
|
loaded, err := config.Load(ck.File(path), input.Overlay, input.Overrides...)
|
|
if err != nil {
|
|
return selectedConfig{}, err
|
|
}
|
|
return selectedConfig{Path: path, Source: source, Result: loaded}, 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
|
|
}
|
|
var reportDir string
|
|
if outputPath != "" {
|
|
if reportDirSet {
|
|
reportDir = cfg.Reporting.StdoutDir
|
|
} else {
|
|
reportDir = filepath.Dir(outputPath)
|
|
}
|
|
} else {
|
|
reportDir = cfg.Reporting.StdoutDir
|
|
}
|
|
if strings.TrimSpace(reportDir) == "" {
|
|
return "", fmt.Errorf("report directory is required when reporting is enabled for the selected destination")
|
|
}
|
|
return reportDir, nil
|
|
}
|