Decompose analyze artifact execution
This commit is contained in:
@@ -2,8 +2,6 @@ package stage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
@@ -11,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/scriptorium"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
@@ -491,266 +488,6 @@ func cloneAnalyzeOutputContract(value *artifactmodel.ContractMetadata) *artifact
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func executeAnalyzeArtifact(
|
||||
ctx context.Context,
|
||||
execution analyzeExecutionContext,
|
||||
plan analyzeArtifactExecutionPlan,
|
||||
) (*analyzeArtifactExecutionResult, error) {
|
||||
env := execution.Env
|
||||
paths := execution.Paths
|
||||
runLayout := execution.RunLayout
|
||||
sessionID := execution.SessionID
|
||||
transcriptRefs := execution.TranscriptRefs
|
||||
artifactName := plan.Name
|
||||
artifactCfg := plan.Cfg
|
||||
|
||||
resolvedInputs, err := resolveAnalyzeInputIdentities(artifactCfg.Inputs, execution)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve inputs for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
inputPaths := resolvedInputs.Paths()
|
||||
omittedOptionalInputs := []string{}
|
||||
reusedArtifacts := []map[string]any{}
|
||||
for _, identity := range resolvedInputs.Ordered {
|
||||
if !identity.Present {
|
||||
omittedOptionalInputs = append(omittedOptionalInputs, identity.Name)
|
||||
continue
|
||||
}
|
||||
resolvedArtifact := resolvedInputs.Artifact(identity.Name)
|
||||
if resolvedArtifact != nil && resolvedArtifact.Provenance == artifacts.ArtifactProvenanceCurrentAnalyzeManifest {
|
||||
reusedArtifacts = append(reusedArtifacts, map[string]any{
|
||||
"name": configuredArtifactNameFromSourceID(resolvedArtifact.ID),
|
||||
"source_id": resolvedArtifact.ID,
|
||||
"path": resolvedArtifact.Path,
|
||||
"provenance": resolvedArtifact.Provenance,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
vars, err := buildScriptoriumVars(artifactCfg.Vars, env.Config.Session)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve vars for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
vars = withScriptoriumStickySessionVar(vars, sessionID)
|
||||
|
||||
canonicalOutputPath, err := resolveScriptoriumOutputPath(paths, artifactCfg.OutputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve output path for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
outputPath, err := runLocalPathForCanonical(runLayout, paths, canonicalOutputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve run-local output path for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
|
||||
stdoutLogPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".stdout.log")
|
||||
stderrLogPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".stderr.log")
|
||||
generatedConfigPath := filepath.Join(paths.ConfigDir, "scriptorium."+artifactName+".generated.yml")
|
||||
if runLayout.Enabled {
|
||||
stdoutLogPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".stdout.log")
|
||||
stderrLogPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".stderr.log")
|
||||
generatedConfigPath = filepath.Join(runLayout.ConfigDir, "scriptorium."+artifactName+".generated.yml")
|
||||
}
|
||||
|
||||
timeout, err := resolveScriptoriumTimeout(env.Config.Pipeline.Scriptorium.Timeout, artifactCfg.Timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve timeout for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
|
||||
logPaths := []string{}
|
||||
generatedConfigs := []string{}
|
||||
meta := map[string]any{
|
||||
"stage": "analyze",
|
||||
"name": artifactName,
|
||||
"artifact_name": artifactName,
|
||||
"source_id": artifacts.ConfiguredArtifactSourceID(artifactName),
|
||||
"output_kind": "scriptorium_artifact",
|
||||
"prompt_id": artifactCfg.PromptID,
|
||||
"profile_id": artifactCfg.ProfileID,
|
||||
"binary": env.Config.Pipeline.Scriptorium.Binary,
|
||||
"config_path": env.Config.Pipeline.Scriptorium.ConfigPath,
|
||||
"input_paths": inputPaths,
|
||||
"input_names": sortedMapKeys(inputPaths),
|
||||
"omitted_optional_inputs": omittedOptionalInputs,
|
||||
"vars": vars,
|
||||
"timeout": timeout.String(),
|
||||
"processed_transcript_path": transcriptRefs.ProcessedPath,
|
||||
"processed_transcript_source": transcriptRefs.ProcessedSource,
|
||||
"normalized_transcript_path": transcriptRefs.NormalizedPath,
|
||||
"normalized_transcript_source": transcriptRefs.NormalizedSource,
|
||||
"trimmed_transcript_path": transcriptRefs.TrimmedPath,
|
||||
"trimmed_transcript_source": transcriptRefs.TrimmedSource,
|
||||
"render_debug_enabled": resolveRenderDebugEnabled(env.Config.Pipeline.Scriptorium.RenderDebug, artifactCfg.RenderDebug),
|
||||
}
|
||||
|
||||
if meta["render_debug_enabled"] == true {
|
||||
renderOutputPath := filepath.Join(paths.ArtifactsDir, artifactName+".render.json")
|
||||
if runLayout.Enabled {
|
||||
renderOutputPath = filepath.Join(runLayout.ReportsDir, artifactName+".render.json")
|
||||
}
|
||||
renderStdoutPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".render.stdout.log")
|
||||
renderStderrPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".render.stderr.log")
|
||||
renderGeneratedConfigPath := filepath.Join(paths.ConfigDir, "scriptorium."+artifactName+".render.generated.yml")
|
||||
if runLayout.Enabled {
|
||||
renderStdoutPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".render.stdout.log")
|
||||
renderStderrPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".render.stderr.log")
|
||||
renderGeneratedConfigPath = filepath.Join(runLayout.ConfigDir, "scriptorium."+artifactName+".render.generated.yml")
|
||||
}
|
||||
|
||||
renderReq := scriptorium.RenderArtifactRequest{
|
||||
Binary: env.Config.Pipeline.Scriptorium.Binary,
|
||||
ConfigPath: env.Config.Pipeline.Scriptorium.ConfigPath,
|
||||
PromptID: artifactCfg.PromptID,
|
||||
ProfileID: artifactCfg.ProfileID,
|
||||
InputPaths: inputPaths,
|
||||
Vars: vars,
|
||||
OutputPath: renderOutputPath,
|
||||
StdoutLogPath: renderStdoutPath,
|
||||
StderrLogPath: renderStderrPath,
|
||||
GeneratedConfigPath: renderGeneratedConfigPath,
|
||||
Timeout: timeout,
|
||||
}
|
||||
renderRes, renderErr := env.Scriptorium.RenderArtifact(ctx, renderReq)
|
||||
if renderErr != nil {
|
||||
return nil, fmt.Errorf("analyze: scriptorium render failed for artifact %q: %w", artifactName, renderErr)
|
||||
}
|
||||
if renderRes.ValidationFailed {
|
||||
return nil, fmt.Errorf("analyze: scriptorium render returned validation_failed=true for artifact %q", artifactName)
|
||||
}
|
||||
finalRenderOutputPath, err := authoritativeOutputPath(renderReq.OutputPath, renderRes.OutputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
if err := requireNonEmptyFile(finalRenderOutputPath, artifactName+" render output"); err != nil {
|
||||
return nil, fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
if err := validateJSONFile(finalRenderOutputPath); err != nil {
|
||||
return nil, fmt.Errorf("analyze: render diagnostics %q invalid json: %w", finalRenderOutputPath, err)
|
||||
}
|
||||
|
||||
logPaths = append(logPaths, renderStdoutPath, renderStderrPath)
|
||||
generatedConfigs = append(generatedConfigs, renderGeneratedConfigPath)
|
||||
meta["render_output_path"] = finalRenderOutputPath
|
||||
meta["render_stdout_log_path"] = renderStdoutPath
|
||||
meta["render_stderr_log_path"] = renderStderrPath
|
||||
meta["render_generated_config_path"] = renderGeneratedConfigPath
|
||||
meta["render_adapter_exit_code"] = renderRes.ExitCode
|
||||
meta["render_adapter_duration_ms"] = renderRes.Duration.Milliseconds()
|
||||
meta["render_adapter_command_mode"] = renderRes.CommandMode
|
||||
meta["render_adapter_prompt_id"] = renderRes.PromptID
|
||||
meta["render_adapter_profile_id"] = renderRes.ProfileID
|
||||
meta["render_adapter_output_path"] = renderRes.OutputPath
|
||||
meta["render_adapter_generated_config"] = renderRes.GeneratedConfigPath
|
||||
meta["render_adapter_stdout_log_path"] = renderRes.StdoutLogPath
|
||||
meta["render_adapter_stderr_log_path"] = renderRes.StderrLogPath
|
||||
if renderRes.Metadata != nil {
|
||||
meta["render_adapter_metadata"] = renderRes.Metadata
|
||||
}
|
||||
}
|
||||
|
||||
req := scriptorium.RunArtifactRequest{
|
||||
Binary: env.Config.Pipeline.Scriptorium.Binary,
|
||||
ConfigPath: env.Config.Pipeline.Scriptorium.ConfigPath,
|
||||
PromptID: artifactCfg.PromptID,
|
||||
ProfileID: artifactCfg.ProfileID,
|
||||
InputPaths: inputPaths,
|
||||
Vars: vars,
|
||||
OutputPath: outputPath,
|
||||
StdoutLogPath: stdoutLogPath,
|
||||
StderrLogPath: stderrLogPath,
|
||||
GeneratedConfigPath: generatedConfigPath,
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
res, runErr := env.Scriptorium.RunArtifact(ctx, req)
|
||||
if runErr != nil {
|
||||
if res.ValidationFailed {
|
||||
return nil, fmt.Errorf(
|
||||
"analyze: scriptorium validation failed (artifact=%q, prompt_id=%q, output_path=%q, exit_code=%d, stdout_log=%q, stderr_log=%q): %w",
|
||||
artifactName,
|
||||
req.PromptID,
|
||||
req.OutputPath,
|
||||
res.ExitCode,
|
||||
coalesceString(res.StdoutLogPath, req.StdoutLogPath),
|
||||
coalesceString(res.StderrLogPath, req.StderrLogPath),
|
||||
runErr,
|
||||
)
|
||||
}
|
||||
return nil, fmt.Errorf("analyze: scriptorium run failed for artifact %q: %w", artifactName, runErr)
|
||||
}
|
||||
if res.ValidationFailed {
|
||||
return nil, fmt.Errorf("analyze: scriptorium run returned validation_failed=true for artifact %q", artifactName)
|
||||
}
|
||||
|
||||
finalOutputPath, err := authoritativeOutputPath(req.OutputPath, res.OutputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
if err := requireNonEmptyFile(finalOutputPath, artifactName+" output"); err != nil {
|
||||
return nil, fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
validatedOutput, err := readExternalResult(finalOutputPath, artifactName+" output")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
contract := &artifactmodel.ContractMetadata{
|
||||
MediaType: "text/markdown", SchemaID: "narratio." + artifactName, SchemaVersion: "1",
|
||||
}
|
||||
relativeOutputPath, err := normalizedAnalyzeOutputIdentity(artifactCfg.OutputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: normalize output identity for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
materializedArtifact, err := materializeRunLocalOutput(env.ArtifactStore, finalOutputPath, canonicalOutputPath, artifacts.Ref{
|
||||
Kind: artifactName,
|
||||
SourceID: artifacts.ConfiguredArtifactSourceID(artifactName),
|
||||
Category: "artifacts",
|
||||
SessionID: sessionID,
|
||||
RelativePath: relativeOutputPath,
|
||||
Contract: contract,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: materialize artifact output for %q: %w", artifactName, err)
|
||||
}
|
||||
expectedDigest := sha256.Sum256(validatedOutput)
|
||||
if materializedArtifact.Checksum != hex.EncodeToString(expectedDigest[:]) {
|
||||
return nil, fmt.Errorf("analyze: materialized artifact output for %q differs from validated run-local bytes", artifactName)
|
||||
}
|
||||
|
||||
logPaths = append(logPaths, stdoutLogPath, stderrLogPath)
|
||||
generatedConfigs = append(generatedConfigs, generatedConfigPath)
|
||||
meta["run_output_path"] = finalOutputPath
|
||||
meta["path"] = canonicalOutputPath
|
||||
meta["output_path"] = canonicalOutputPath
|
||||
meta["generated_config_path"] = generatedConfigPath
|
||||
meta["stdout_log_path"] = stdoutLogPath
|
||||
meta["stderr_log_path"] = stderrLogPath
|
||||
meta["adapter_exit_code"] = res.ExitCode
|
||||
meta["adapter_duration_ms"] = res.Duration.Milliseconds()
|
||||
meta["adapter_command_mode"] = res.CommandMode
|
||||
meta["adapter_prompt_id"] = res.PromptID
|
||||
meta["adapter_profile_id"] = res.ProfileID
|
||||
meta["adapter_validation_failed"] = res.ValidationFailed
|
||||
meta["adapter_output_path"] = res.OutputPath
|
||||
meta["adapter_generated_config"] = res.GeneratedConfigPath
|
||||
meta["adapter_stdout_log_path"] = res.StdoutLogPath
|
||||
meta["adapter_stderr_log_path"] = res.StderrLogPath
|
||||
meta["provenance"] = artifacts.ArtifactProvenanceGeneratedCurrentAnalyzeRun
|
||||
if res.Metadata != nil {
|
||||
meta["adapter_metadata"] = res.Metadata
|
||||
}
|
||||
|
||||
return &analyzeArtifactExecutionResult{
|
||||
Output: materializedArtifact,
|
||||
OutputSize: int64(len(validatedOutput)),
|
||||
Scriptorium: manifest.AnalyzeArtifactProvenance{
|
||||
PromptID: artifactCfg.PromptID, ProfileID: artifactCfg.ProfileID, CommandMode: res.CommandMode,
|
||||
},
|
||||
Logs: logPaths,
|
||||
GeneratedConfigs: generatedConfigs,
|
||||
Metadata: meta,
|
||||
ReusedArtifacts: reusedArtifacts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func configuredArtifactNameFromSourceID(sourceID string) string {
|
||||
name, _ := artifactpolicy.ParseConfiguredSource(sourceID)
|
||||
return name
|
||||
|
||||
Reference in New Issue
Block a user