Decompose analyze artifact execution
This commit is contained in:
@@ -121,7 +121,7 @@ During every stage:
|
|||||||
|
|
||||||
## Stage 1 — Decompose The Single-Artifact Transaction
|
## Stage 1 — Decompose The Single-Artifact Transaction
|
||||||
|
|
||||||
**Status:** Pending
|
**Status:** Completed
|
||||||
|
|
||||||
### Goal
|
### Goal
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package stage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -11,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/scriptorium"
|
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||||
@@ -491,266 +488,6 @@ func cloneAnalyzeOutputContract(value *artifactmodel.ContractMetadata) *artifact
|
|||||||
return &cloned
|
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 {
|
func configuredArtifactNameFromSourceID(sourceID string) string {
|
||||||
name, _ := artifactpolicy.ParseConfiguredSource(sourceID)
|
name, _ := artifactpolicy.ParseConfiguredSource(sourceID)
|
||||||
return name
|
return name
|
||||||
|
|||||||
385
internal/stage/analyze_artifact_execution.go
Normal file
385
internal/stage/analyze_artifact_execution.go
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
package stage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/scriptorium"
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type analyzeArtifactPreparation struct {
|
||||||
|
name string
|
||||||
|
promptID string
|
||||||
|
profileID string
|
||||||
|
inputPaths map[string]string
|
||||||
|
vars map[string]string
|
||||||
|
timeout time.Duration
|
||||||
|
canonicalOutputPath string
|
||||||
|
runOutputPath string
|
||||||
|
stdoutLogPath string
|
||||||
|
stderrLogPath string
|
||||||
|
generatedConfigPath string
|
||||||
|
renderEnabled bool
|
||||||
|
renderOutputPath string
|
||||||
|
renderStdoutPath string
|
||||||
|
renderStderrPath string
|
||||||
|
renderConfigPath string
|
||||||
|
metadata map[string]any
|
||||||
|
reusedArtifacts []map[string]any
|
||||||
|
logs []string
|
||||||
|
generatedConfigs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type analyzeArtifactInvocation struct {
|
||||||
|
request scriptorium.RunArtifactRequest
|
||||||
|
result scriptorium.ArtifactResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type analyzeArtifactMaterialization struct {
|
||||||
|
output artifacts.Ref
|
||||||
|
outputSize int64
|
||||||
|
runOutputPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeAnalyzeArtifact(
|
||||||
|
ctx context.Context,
|
||||||
|
execution analyzeExecutionContext,
|
||||||
|
plan analyzeArtifactExecutionPlan,
|
||||||
|
) (*analyzeArtifactExecutionResult, error) {
|
||||||
|
prepared, err := prepareAnalyzeArtifact(execution, plan)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := invokeAnalyzeRender(ctx, execution, prepared); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
invocation, err := invokeAnalyzeArtifact(ctx, execution, prepared)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
materialized, err := validateAndMaterializeAnalyzeArtifact(execution, plan, prepared, invocation)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buildAnalyzeArtifactExecutionResult(plan, prepared, invocation, materialized), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareAnalyzeArtifact(
|
||||||
|
execution analyzeExecutionContext,
|
||||||
|
plan analyzeArtifactExecutionPlan,
|
||||||
|
) (*analyzeArtifactPreparation, error) {
|
||||||
|
env := execution.Env
|
||||||
|
paths := execution.Paths
|
||||||
|
runLayout := execution.RunLayout
|
||||||
|
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, execution.SessionID)
|
||||||
|
|
||||||
|
canonicalOutputPath, err := resolveScriptoriumOutputPath(paths, artifactCfg.OutputPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("analyze: resolve output path for artifact %q: %w", artifactName, err)
|
||||||
|
}
|
||||||
|
runOutputPath, 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")
|
||||||
|
renderOutputPath := filepath.Join(paths.ArtifactsDir, artifactName+".render.json")
|
||||||
|
renderStdoutPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".render.stdout.log")
|
||||||
|
renderStderrPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".render.stderr.log")
|
||||||
|
renderConfigPath := filepath.Join(paths.ConfigDir, "scriptorium."+artifactName+".render.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")
|
||||||
|
renderOutputPath = filepath.Join(runLayout.ReportsDir, artifactName+".render.json")
|
||||||
|
renderStdoutPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".render.stdout.log")
|
||||||
|
renderStderrPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".render.stderr.log")
|
||||||
|
renderConfigPath = filepath.Join(runLayout.ConfigDir, "scriptorium."+artifactName+".render.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)
|
||||||
|
}
|
||||||
|
renderEnabled := resolveRenderDebugEnabled(env.Config.Pipeline.Scriptorium.RenderDebug, artifactCfg.RenderDebug)
|
||||||
|
|
||||||
|
return &analyzeArtifactPreparation{
|
||||||
|
name: artifactName,
|
||||||
|
promptID: artifactCfg.PromptID,
|
||||||
|
profileID: artifactCfg.ProfileID,
|
||||||
|
inputPaths: inputPaths,
|
||||||
|
vars: vars,
|
||||||
|
timeout: timeout,
|
||||||
|
canonicalOutputPath: canonicalOutputPath,
|
||||||
|
runOutputPath: runOutputPath,
|
||||||
|
stdoutLogPath: stdoutLogPath,
|
||||||
|
stderrLogPath: stderrLogPath,
|
||||||
|
generatedConfigPath: generatedConfigPath,
|
||||||
|
renderEnabled: renderEnabled,
|
||||||
|
renderOutputPath: renderOutputPath,
|
||||||
|
renderStdoutPath: renderStdoutPath,
|
||||||
|
renderStderrPath: renderStderrPath,
|
||||||
|
renderConfigPath: renderConfigPath,
|
||||||
|
reusedArtifacts: reusedArtifacts,
|
||||||
|
metadata: 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": execution.TranscriptRefs.ProcessedPath,
|
||||||
|
"processed_transcript_source": execution.TranscriptRefs.ProcessedSource,
|
||||||
|
"normalized_transcript_path": execution.TranscriptRefs.NormalizedPath,
|
||||||
|
"normalized_transcript_source": execution.TranscriptRefs.NormalizedSource,
|
||||||
|
"trimmed_transcript_path": execution.TranscriptRefs.TrimmedPath,
|
||||||
|
"trimmed_transcript_source": execution.TranscriptRefs.TrimmedSource,
|
||||||
|
"render_debug_enabled": renderEnabled,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func invokeAnalyzeRender(
|
||||||
|
ctx context.Context,
|
||||||
|
execution analyzeExecutionContext,
|
||||||
|
prepared *analyzeArtifactPreparation,
|
||||||
|
) error {
|
||||||
|
if !prepared.renderEnabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
req := scriptorium.RenderArtifactRequest{
|
||||||
|
Binary: execution.Env.Config.Pipeline.Scriptorium.Binary,
|
||||||
|
ConfigPath: execution.Env.Config.Pipeline.Scriptorium.ConfigPath,
|
||||||
|
PromptID: prepared.promptID,
|
||||||
|
ProfileID: prepared.profileID,
|
||||||
|
InputPaths: prepared.inputPaths,
|
||||||
|
Vars: prepared.vars,
|
||||||
|
OutputPath: prepared.renderOutputPath,
|
||||||
|
StdoutLogPath: prepared.renderStdoutPath,
|
||||||
|
StderrLogPath: prepared.renderStderrPath,
|
||||||
|
GeneratedConfigPath: prepared.renderConfigPath,
|
||||||
|
Timeout: prepared.timeout,
|
||||||
|
}
|
||||||
|
res, err := execution.Env.Scriptorium.RenderArtifact(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("analyze: scriptorium render failed for artifact %q: %w", prepared.name, err)
|
||||||
|
}
|
||||||
|
if res.ValidationFailed {
|
||||||
|
return fmt.Errorf("analyze: scriptorium render returned validation_failed=true for artifact %q", prepared.name)
|
||||||
|
}
|
||||||
|
outputPath, err := authoritativeOutputPath(req.OutputPath, res.OutputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("analyze: %w", err)
|
||||||
|
}
|
||||||
|
if err := requireNonEmptyFile(outputPath, prepared.name+" render output"); err != nil {
|
||||||
|
return fmt.Errorf("analyze: %w", err)
|
||||||
|
}
|
||||||
|
if err := validateJSONFile(outputPath); err != nil {
|
||||||
|
return fmt.Errorf("analyze: render diagnostics %q invalid json: %w", outputPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
prepared.logs = append(prepared.logs, prepared.renderStdoutPath, prepared.renderStderrPath)
|
||||||
|
prepared.generatedConfigs = append(prepared.generatedConfigs, prepared.renderConfigPath)
|
||||||
|
prepared.metadata["render_output_path"] = outputPath
|
||||||
|
prepared.metadata["render_stdout_log_path"] = prepared.renderStdoutPath
|
||||||
|
prepared.metadata["render_stderr_log_path"] = prepared.renderStderrPath
|
||||||
|
prepared.metadata["render_generated_config_path"] = prepared.renderConfigPath
|
||||||
|
prepared.metadata["render_adapter_exit_code"] = res.ExitCode
|
||||||
|
prepared.metadata["render_adapter_duration_ms"] = res.Duration.Milliseconds()
|
||||||
|
prepared.metadata["render_adapter_command_mode"] = res.CommandMode
|
||||||
|
prepared.metadata["render_adapter_prompt_id"] = res.PromptID
|
||||||
|
prepared.metadata["render_adapter_profile_id"] = res.ProfileID
|
||||||
|
prepared.metadata["render_adapter_output_path"] = res.OutputPath
|
||||||
|
prepared.metadata["render_adapter_generated_config"] = res.GeneratedConfigPath
|
||||||
|
prepared.metadata["render_adapter_stdout_log_path"] = res.StdoutLogPath
|
||||||
|
prepared.metadata["render_adapter_stderr_log_path"] = res.StderrLogPath
|
||||||
|
if res.Metadata != nil {
|
||||||
|
prepared.metadata["render_adapter_metadata"] = res.Metadata
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func invokeAnalyzeArtifact(
|
||||||
|
ctx context.Context,
|
||||||
|
execution analyzeExecutionContext,
|
||||||
|
prepared *analyzeArtifactPreparation,
|
||||||
|
) (analyzeArtifactInvocation, error) {
|
||||||
|
req := scriptorium.RunArtifactRequest{
|
||||||
|
Binary: execution.Env.Config.Pipeline.Scriptorium.Binary,
|
||||||
|
ConfigPath: execution.Env.Config.Pipeline.Scriptorium.ConfigPath,
|
||||||
|
PromptID: prepared.promptID,
|
||||||
|
ProfileID: prepared.profileID,
|
||||||
|
InputPaths: prepared.inputPaths,
|
||||||
|
Vars: prepared.vars,
|
||||||
|
OutputPath: prepared.runOutputPath,
|
||||||
|
StdoutLogPath: prepared.stdoutLogPath,
|
||||||
|
StderrLogPath: prepared.stderrLogPath,
|
||||||
|
GeneratedConfigPath: prepared.generatedConfigPath,
|
||||||
|
Timeout: prepared.timeout,
|
||||||
|
}
|
||||||
|
res, err := execution.Env.Scriptorium.RunArtifact(ctx, req)
|
||||||
|
invocation := analyzeArtifactInvocation{request: req, result: res}
|
||||||
|
if err != nil {
|
||||||
|
if res.ValidationFailed {
|
||||||
|
return invocation, fmt.Errorf(
|
||||||
|
"analyze: scriptorium validation failed (artifact=%q, prompt_id=%q, output_path=%q, exit_code=%d, stdout_log=%q, stderr_log=%q): %w",
|
||||||
|
prepared.name,
|
||||||
|
req.PromptID,
|
||||||
|
req.OutputPath,
|
||||||
|
res.ExitCode,
|
||||||
|
coalesceString(res.StdoutLogPath, req.StdoutLogPath),
|
||||||
|
coalesceString(res.StderrLogPath, req.StderrLogPath),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return invocation, fmt.Errorf("analyze: scriptorium run failed for artifact %q: %w", prepared.name, err)
|
||||||
|
}
|
||||||
|
if res.ValidationFailed {
|
||||||
|
return invocation, fmt.Errorf("analyze: scriptorium run returned validation_failed=true for artifact %q", prepared.name)
|
||||||
|
}
|
||||||
|
return invocation, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAndMaterializeAnalyzeArtifact(
|
||||||
|
execution analyzeExecutionContext,
|
||||||
|
plan analyzeArtifactExecutionPlan,
|
||||||
|
prepared *analyzeArtifactPreparation,
|
||||||
|
invocation analyzeArtifactInvocation,
|
||||||
|
) (analyzeArtifactMaterialization, error) {
|
||||||
|
outputPath, err := authoritativeOutputPath(invocation.request.OutputPath, invocation.result.OutputPath)
|
||||||
|
if err != nil {
|
||||||
|
return analyzeArtifactMaterialization{}, fmt.Errorf("analyze: %w", err)
|
||||||
|
}
|
||||||
|
if err := requireNonEmptyFile(outputPath, prepared.name+" output"); err != nil {
|
||||||
|
return analyzeArtifactMaterialization{}, fmt.Errorf("analyze: %w", err)
|
||||||
|
}
|
||||||
|
validatedOutput, err := readExternalResult(outputPath, prepared.name+" output")
|
||||||
|
if err != nil {
|
||||||
|
return analyzeArtifactMaterialization{}, fmt.Errorf("analyze: %w", err)
|
||||||
|
}
|
||||||
|
contract := &artifactmodel.ContractMetadata{
|
||||||
|
MediaType: "text/markdown", SchemaID: "narratio." + prepared.name, SchemaVersion: "1",
|
||||||
|
}
|
||||||
|
relativeOutputPath, err := normalizedAnalyzeOutputIdentity(plan.Cfg.OutputPath)
|
||||||
|
if err != nil {
|
||||||
|
return analyzeArtifactMaterialization{}, fmt.Errorf("analyze: normalize output identity for artifact %q: %w", prepared.name, err)
|
||||||
|
}
|
||||||
|
materialized, err := materializeRunLocalOutput(
|
||||||
|
execution.Env.ArtifactStore,
|
||||||
|
outputPath,
|
||||||
|
prepared.canonicalOutputPath,
|
||||||
|
artifacts.Ref{
|
||||||
|
Kind: prepared.name,
|
||||||
|
SourceID: artifacts.ConfiguredArtifactSourceID(prepared.name),
|
||||||
|
Category: "artifacts",
|
||||||
|
SessionID: execution.SessionID,
|
||||||
|
RelativePath: relativeOutputPath,
|
||||||
|
Contract: contract,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return analyzeArtifactMaterialization{}, fmt.Errorf("analyze: materialize artifact output for %q: %w", prepared.name, err)
|
||||||
|
}
|
||||||
|
expectedDigest := sha256.Sum256(validatedOutput)
|
||||||
|
if materialized.Checksum != hex.EncodeToString(expectedDigest[:]) {
|
||||||
|
return analyzeArtifactMaterialization{}, fmt.Errorf(
|
||||||
|
"analyze: materialized artifact output for %q differs from validated run-local bytes",
|
||||||
|
prepared.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return analyzeArtifactMaterialization{
|
||||||
|
output: materialized,
|
||||||
|
outputSize: int64(len(validatedOutput)),
|
||||||
|
runOutputPath: outputPath,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAnalyzeArtifactExecutionResult(
|
||||||
|
plan analyzeArtifactExecutionPlan,
|
||||||
|
prepared *analyzeArtifactPreparation,
|
||||||
|
invocation analyzeArtifactInvocation,
|
||||||
|
materialized analyzeArtifactMaterialization,
|
||||||
|
) *analyzeArtifactExecutionResult {
|
||||||
|
res := invocation.result
|
||||||
|
prepared.logs = append(prepared.logs, prepared.stdoutLogPath, prepared.stderrLogPath)
|
||||||
|
prepared.generatedConfigs = append(prepared.generatedConfigs, prepared.generatedConfigPath)
|
||||||
|
prepared.metadata["run_output_path"] = materialized.runOutputPath
|
||||||
|
prepared.metadata["path"] = prepared.canonicalOutputPath
|
||||||
|
prepared.metadata["output_path"] = prepared.canonicalOutputPath
|
||||||
|
prepared.metadata["generated_config_path"] = prepared.generatedConfigPath
|
||||||
|
prepared.metadata["stdout_log_path"] = prepared.stdoutLogPath
|
||||||
|
prepared.metadata["stderr_log_path"] = prepared.stderrLogPath
|
||||||
|
prepared.metadata["adapter_exit_code"] = res.ExitCode
|
||||||
|
prepared.metadata["adapter_duration_ms"] = res.Duration.Milliseconds()
|
||||||
|
prepared.metadata["adapter_command_mode"] = res.CommandMode
|
||||||
|
prepared.metadata["adapter_prompt_id"] = res.PromptID
|
||||||
|
prepared.metadata["adapter_profile_id"] = res.ProfileID
|
||||||
|
prepared.metadata["adapter_validation_failed"] = res.ValidationFailed
|
||||||
|
prepared.metadata["adapter_output_path"] = res.OutputPath
|
||||||
|
prepared.metadata["adapter_generated_config"] = res.GeneratedConfigPath
|
||||||
|
prepared.metadata["adapter_stdout_log_path"] = res.StdoutLogPath
|
||||||
|
prepared.metadata["adapter_stderr_log_path"] = res.StderrLogPath
|
||||||
|
prepared.metadata["provenance"] = artifacts.ArtifactProvenanceGeneratedCurrentAnalyzeRun
|
||||||
|
if res.Metadata != nil {
|
||||||
|
prepared.metadata["adapter_metadata"] = res.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
return &analyzeArtifactExecutionResult{
|
||||||
|
Output: materialized.output,
|
||||||
|
OutputSize: materialized.outputSize,
|
||||||
|
Scriptorium: manifest.AnalyzeArtifactProvenance{
|
||||||
|
PromptID: plan.Cfg.PromptID, ProfileID: plan.Cfg.ProfileID, CommandMode: res.CommandMode,
|
||||||
|
},
|
||||||
|
Logs: prepared.logs,
|
||||||
|
GeneratedConfigs: prepared.generatedConfigs,
|
||||||
|
Metadata: prepared.metadata,
|
||||||
|
ReusedArtifacts: prepared.reusedArtifacts,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user