package cli import ( "bytes" "context" "crypto/rand" "encoding/hex" "fmt" "io" "path/filepath" "time" "gitea.maximumdirect.net/eric/audita/v3/internal/core/config" "gitea.maximumdirect.net/eric/audita/v3/internal/core/diagnostics" coreio "gitea.maximumdirect.net/eric/audita/v3/internal/core/io" "gitea.maximumdirect.net/eric/audita/v3/internal/core/reporting" "gitea.maximumdirect.net/eric/audita/v3/internal/core/schema" "gitea.maximumdirect.net/eric/audita/v3/internal/framework/llm" "gitea.maximumdirect.net/eric/audita/v3/internal/framework/preflight" "go.maximumdirect.net/ckconfig" commandkit "go.maximumdirect.net/commandkit" ck "go.maximumdirect.net/configkit" ) type processInputs struct { Configuration *ck.Result[config.Config] Runtime *llm.Runtime Source *schema.SourceTranscript RawTranscript []byte ProtectedPaths []string } func loadProcessInputs(ctx context.Context, input processInput, mode commandkit.ReportingMode) (*processInputs, error) { if mode == commandkit.ReportingModeQuiet && input.Output.Value == "" { return nil, fmt.Errorf("quiet processing requires an output file or /dev/null") } selection := ckconfig.Selection{Config: input.Config, Overlay: input.Overlay, Overrides: input.Overrides} loaded, err := loadApplication(ctx, selection, struct{}{}, nil) if err != nil { return nil, err } runtime, err := llm.NewRuntime(ctx, loaded, nil) if err != nil { return nil, err } raw, err := coreio.ReadRequiredFile(input.Transcript, "transcript") if err != nil { return nil, err } source, err := schema.ParseSourceTranscriptJSON(raw) if err != nil { return nil, err } protected := append([]string(nil), runtime.SourcePaths...) for _, entry := range loaded.Sources() { if entry.Kind == ck.SourceKindFile && entry.Path != "" { protected = append(protected, entry.Path) } } return &processInputs{Configuration: loaded, Runtime: runtime, Source: source, RawTranscript: raw, ProtectedPaths: protected}, nil } // preparedProcess groups the output resources and the facts needed for reports. type preparedProcess struct { Prepared *preflight.Prepared RunID string Started time.Time } func prepareProcess(ctx context.Context, input processInput, loaded *processInputs, stdout io.Writer) (*preparedProcess, error) { cfg := &loaded.Configuration.Config reportDir := "" reportDirSet := false for _, override := range input.Overrides { if override.Path.String() == "/reporting/stdout_dir" { reportDirSet = true } } if cfg.Reporting.Enabled { reportDir = cfg.Reporting.StdoutDir if input.Output.Value != "" && !reportDirSet { reportDir = filepath.Dir(input.Output.Value) } } runID, err := generateRunID() if err != nil { return nil, err } started := time.Now().UTC() var secrets []string if cfg.Reporting.Enabled || input.Debug.Value { secrets = loaded.Runtime.ConfiguredSecrets(ctx) } var initial diagnostics.ReportBundle var failed func(error, []diagnostics.FileFailure) diagnostics.ReportBundle if cfg.Reporting.Enabled { snapshot := reporting.Snapshot{RunID: runID, Status: reporting.StatusInProgress, StartedAt: started, TranscriptPath: input.Transcript, OutputPath: input.Output.Value} for index, item := range cfg.Pipeline.Stages { snapshot.StageNames = append(snapshot.StageNames, reporting.StageLabel(index, item.Module, item.ID)) } initial, err = renderReportBundle(snapshot, secrets...) if err != nil { return nil, err } failed = func(primary error, failures []diagnostics.FileFailure) diagnostics.ReportBundle { failedSnapshot := snapshot completed := time.Now().UTC() failedSnapshot.CompletedAt = &completed if primary != nil { failedSnapshot.Errors = []reporting.ErrorRecord{{Scope: "run", Phase: "initialization", Reason: "canceled", Message: "Processing was canceled."}} } bundle, _ := renderReportBundle(failedOutputSnapshot(failedSnapshot, failures), secrets...) return bundle } } var debug *preflight.DebugInput var warnings []error if input.Debug.Value { var effective bytes.Buffer if err := loaded.Configuration.WriteEffective(&effective); err != nil { warnings = append(warnings, err) } debug = &preflight.DebugInput{WorkDir: cfg.Diagnostics.WorkDir, RunID: runID, Secrets: secrets, EffectiveConfig: effective.Bytes(), Metadata: diagnostics.InvocationMetadata{Operation: "process", TranscriptPath: input.Transcript, OutputPath: input.Output.Value, Overlay: input.Overlay.Value, ConfigVersion: &cfg.Version, RunID: runID, StartedAt: started}} } prepared, err := preflight.Prepare(ctx, preflight.Input{InspectProfile: func(ctx context.Context, id string) error { profile, err := loaded.Runtime.Engine.InspectProfile(ctx, id) if err != nil { return err } if profile.Backend != nil { _, err = loaded.Runtime.Engine.InspectBackend(ctx, *profile.Backend) } return err }, Config: cfg, Transcript: loaded.Source, Output: diagnostics.OutputSelection{InputTranscriptPath: input.Transcript, TranscriptPath: input.Output.Value, ReportDir: reportDir, ReportingEnabled: cfg.Reporting.Enabled, ProtectedPaths: loaded.ProtectedPaths, Stdout: stdout}, InitialReports: initial, FailedReports: failed, Debug: debug}) if err != nil { return nil, err } prepared.Warnings = append(warnings, prepared.Warnings...) return &preparedProcess{Prepared: prepared, RunID: runID, Started: started}, nil } func generateRunID() (string, error) { var random [12]byte if _, err := rand.Read(random[:]); err != nil { return "", err } return "run-" + hex.EncodeToString(random[:]), nil } func renderReportBundle(value reporting.Snapshot, secrets ...string) (diagnostics.ReportBundle, error) { errorsYAML, err := reporting.YAML(value.ErrorsDocument(), secrets...) if err != nil { return diagnostics.ReportBundle{}, err } recoveriesYAML, err := reporting.YAML(value.RecoveriesDocument(), secrets...) if err != nil { return diagnostics.ReportBundle{}, err } return diagnostics.ReportBundle{Errors: errorsYAML, Recoveries: recoveriesYAML, Markdown: reporting.Markdown(value, secrets...)}, nil }