package app import ( "fmt" "reflect" "sort" "gitea.maximumdirect.net/eric/narratio/internal/manifest" "gitea.maximumdirect.net/eric/narratio/internal/stage" ) type validatedAnalyzeProjection struct { session map[string]manifest.AnalyzeArtifactRecord invocation map[string]manifest.AnalyzeArtifactRecord } type analyzeStateSnapshot struct { version int records map[string]manifest.AnalyzeArtifactRecord } func captureAnalyzeState(manifestValue *manifest.Manifest, stageName string) analyzeStateSnapshot { if manifestValue == nil || stageName != "analyze" || manifestValue.Stages["analyze"] == nil { return analyzeStateSnapshot{} } record := manifestValue.Stages["analyze"] return analyzeStateSnapshot{ version: record.AnalyzeStateVersion, records: manifest.CloneAnalyzeArtifactCollection(record.AnalyzeArtifacts), } } func restoreAnalyzeState(manifestValue *manifest.Manifest, snapshot analyzeStateSnapshot) { if manifestValue == nil || manifestValue.Stages["analyze"] == nil { return } record := manifestValue.Stages["analyze"] record.AnalyzeStateVersion = snapshot.version record.AnalyzeArtifacts = manifest.CloneAnalyzeArtifactCollection(snapshot.records) } func validateSuccessfulAnalyzeProjection(stageName string, result *stage.StageResult) (*validatedAnalyzeProjection, error) { if result == nil || result.AnalyzeState == nil { return nil, nil } if stageName != "analyze" { return nil, fmt.Errorf("stage %q returned analyze-owned state projection", stageName) } if result.Disposition == stage.StageDispositionSkipped { return nil, fmt.Errorf("skipped analyze result cannot contain analyze-owned state projection") } if len(result.Outputs) != 0 { return nil, fmt.Errorf("analyze result with state projection cannot contain ordinary outputs") } return validateAndCloneAnalyzeProjection(result.AnalyzeState) } func validateFailedAnalyzeProjection(stageName string, result *stage.StageResult) (*validatedAnalyzeProjection, error) { if result == nil || result.AnalyzeState == nil { return nil, nil } if stageName != "analyze" { return nil, fmt.Errorf("stage %q returned analyze-owned state projection with an error", stageName) } if result.Disposition != stage.StageDispositionSucceeded || result.SkipReason != "" || len(result.Outputs) != 0 || len(result.Logs) != 0 || len(result.GeneratedConfigs) != 0 || len(result.Metadata) != 0 { return nil, fmt.Errorf("analyze result with an error may contain only analyze-owned state projection") } return validateAndCloneAnalyzeProjection(result.AnalyzeState) } func validateAndCloneAnalyzeProjection(projection *stage.AnalyzeStateProjection) (*validatedAnalyzeProjection, error) { session := manifest.CloneAnalyzeArtifactCollection(projection.Session) invocation := manifest.CloneAnalyzeArtifactCollection(projection.Invocation) if err := manifest.ValidateAnalyzeArtifactCollection(manifest.AnalyzeStateContractVersion, session); err != nil { return nil, fmt.Errorf("validate reconciled session analyze state: %w", err) } if err := manifest.ValidateAnalyzeArtifactCollection(manifest.AnalyzeStateContractVersion, invocation); err != nil { return nil, fmt.Errorf("validate invocation analyze state: %w", err) } for key, invocationRecord := range invocation { sessionRecord, ok := session[key] if !ok { return nil, fmt.Errorf("invocation analyze artifact %q is absent from reconciled session state", key) } if !reflect.DeepEqual(invocationRecord, sessionRecord) { return nil, fmt.Errorf("invocation analyze artifact %q contradicts reconciled session state", key) } } return &validatedAnalyzeProjection{session: session, invocation: invocation}, nil } func applyAnalyzeProjection( sessionManifest *manifest.Manifest, runManifest *manifest.RunManifest, projection *validatedAnalyzeProjection, ) { if projection == nil { return } if sessionManifest != nil && sessionManifest.Stages["analyze"] != nil { record := sessionManifest.Stages["analyze"] record.AnalyzeStateVersion = manifest.AnalyzeStateContractVersion record.AnalyzeArtifacts = manifest.CloneAnalyzeArtifactCollection(projection.session) } if runManifest != nil && runManifest.Stages["analyze"] != nil { record := runManifest.Stages["analyze"] record.AnalyzeStateVersion = manifest.AnalyzeStateContractVersion record.AnalyzeArtifacts = manifest.CloneAnalyzeArtifactCollection(projection.invocation) } } func analyzeProjectionOutputs(records map[string]manifest.AnalyzeArtifactRecord, producerRunID string) []manifest.ArtifactRecord { keys := make([]string, 0, len(records)) for key, record := range records { if record.Status != manifest.AnalyzeArtifactCurrent || record.Output == nil { continue } if producerRunID != "" && record.ProducerRunID != producerRunID { continue } keys = append(keys, key) } sort.Strings(keys) outputs := make([]manifest.ArtifactRecord, 0, len(keys)) for _, key := range keys { record := manifest.CloneAnalyzeArtifactCollection(map[string]manifest.AnalyzeArtifactRecord{key: records[key]})[key] output := *record.Output if output.ProducerRunID == "" { output.ProducerRunID = record.ProducerRunID } outputs = append(outputs, output) } return outputs }