136 lines
4.3 KiB
Go
136 lines
4.3 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 {
|
|
for _, selection := range []struct {
|
|
role string
|
|
profile *string
|
|
}{{"proposal", item.LLM.ProposalProfile}, {"validation", item.LLM.ValidationProfile}} {
|
|
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()
|
|
}
|