Files
audita/internal/framework/preflight/preflight.go

143 lines
4.5 KiB
Go

// Package preflight checks resolved processing inputs and prepares output resources.
package preflight
import (
"context"
"fmt"
"slices"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/diagnostics"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/modulecatalog"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
)
// Input contains already acquired application and transcript inputs.
type Input struct {
InspectProfile func(context.Context, string) error
Config *config.Config
Transcript *schema.SourceTranscript
Output diagnostics.OutputSelection
InitialReports diagnostics.ReportBundle
FailedReports func(error, []diagnostics.FileFailure) diagnostics.ReportBundle
Debug *DebugInput
}
type DebugInput struct {
WorkDir string
RunID string
Metadata diagnostics.InvocationMetadata
EffectiveConfig []byte
Secrets []string
}
type Prepared struct {
Output *diagnostics.OutputSession
Debug *diagnostics.RunDirectory
Warnings []error
}
// Prepare validates invocation inputs and collisions before acquiring outputs.
func Prepare(ctx context.Context, input Input) (*Prepared, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if input.Config == nil {
return nil, fmt.Errorf("application configuration is required")
}
if input.Transcript == nil {
return nil, fmt.Errorf("transcript is required")
}
if err := CheckReadiness(ctx, input.Config, input.InspectProfile); err != nil {
return nil, err
}
session, err := diagnostics.SelectOutput(input.Output)
if err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
if err := session.Prepare(); err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
_ = session.Close()
return nil, err
}
failedReports := func(primary error) func([]diagnostics.FileFailure) diagnostics.ReportBundle {
return func(failures []diagnostics.FileFailure) diagnostics.ReportBundle {
if input.FailedReports == nil {
return diagnostics.ReportBundle{}
}
return input.FailedReports(primary, failures)
}
}
if err := session.Initialize(input.InitialReports, failedReports(nil)); err != nil {
return nil, err
}
cancelOutput := func(err error) error {
failed := failedReports(err)
return session.Finish(err, nil, failed(nil), failed)
}
if err := ctx.Err(); err != nil {
return nil, cancelOutput(err)
}
result := &Prepared{Output: session}
if input.Debug != nil {
result.Debug, result.Warnings = session.PrepareDebug(input.Debug.WorkDir, input.Debug.RunID, input.Debug.Metadata, input.Debug.EffectiveConfig, input.Debug.Secrets...)
}
if err := ctx.Err(); err != nil {
return nil, cancelOutput(err)
}
return result, nil
}
// CheckReadiness checks all resolved processing selections before output preparation.
func CheckReadiness(ctx context.Context, cfg *config.Config, inspectProfile func(context.Context, string) error) error {
if cfg == nil {
return fmt.Errorf("application configuration is required")
}
label := func(index int, item config.Stage) string {
if item.ID != nil {
return fmt.Sprintf("stage %d (%s, id %q)", index+1, item.Module, *item.ID)
}
return fmt.Sprintf("stage %d (%s)", index+1, item.Module)
}
for index, item := range cfg.Pipeline.Stages {
if item.Module != modulecatalog.KeyGlossary {
continue
}
hasEntries := slices.ContainsFunc(item.Glossaries, func(name string) bool {
return len(cfg.GlossaryCollections[name].Entries) > 0
})
if !hasEntries {
return fmt.Errorf("%s has no glossary entries in selected collections %q", label(index, item), item.Glossaries)
}
}
for index, item := range cfg.Pipeline.Stages {
selections := []struct {
role string
profile *string
}{{"proposal", item.LLM.ProposalProfile}, {"validation", item.LLM.ValidationProfile}}
if item.LLM.CorrectionRepairAttempts > 0 {
selections = append(selections, struct {
role string
profile *string
}{"correction repair", item.LLM.CorrectionRepairProfile})
}
for _, selection := range selections {
if selection.profile == nil || *selection.profile == "" {
return fmt.Errorf("%s requires a %s profile", label(index, item), selection.role)
}
if inspectProfile == nil {
return fmt.Errorf("profile inspection is required")
}
if err := inspectProfile(ctx, *selection.profile); err != nil {
return fmt.Errorf("%s %s profile %q: %w", label(index, item), selection.role, *selection.profile, err)
}
}
}
return ctx.Err()
}