Decompose analyze artifact execution

This commit is contained in:
2026-09-04 15:50:51 +00:00
parent 6d18a3126e
commit 5c520dc0d4
3 changed files with 386 additions and 264 deletions

View File

@@ -121,7 +121,7 @@ During every stage:
## Stage 1 — Decompose The Single-Artifact Transaction
**Status:** Pending
**Status:** Completed
### Goal

View File

@@ -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

View 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,
}
}