Update artifact resolution so configured artifact IDs are resolved through the runtime catalog

This commit is contained in:
2026-05-19 18:49:27 -05:00
parent 859ae1ae10
commit 3e79cf4724
4 changed files with 307 additions and 2 deletions

View File

@@ -88,6 +88,11 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
}, nil
}
runtimeCatalog, err := buildAnalyzeRuntimeArtifactCatalog(paths, env.Config.Pipeline.Scriptorium, env.SelectedAnalyzeArtifacts)
if err != nil {
return nil, fmt.Errorf("analyze: build runtime artifact catalog: %w", err)
}
transcriptRefs := discoverAnalyzeTranscriptRefs(m, paths)
inputPaths := map[string]string{}
@@ -96,7 +101,7 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
inputNames := sortedScriptoriumInputNames(artifactCfg.Inputs)
for _, inputName := range inputNames {
inputCfg := artifactCfg.Inputs[inputName]
resolvedPath, resolved, resolveErr := resolveScriptoriumInput(inputName, inputCfg, m, paths, sessionDir)
resolvedPath, resolved, resolveErr := resolveScriptoriumInput(inputName, inputCfg, m, paths, sessionDir, runtimeCatalog)
if resolveErr != nil {
return nil, fmt.Errorf("analyze: resolve input %q: %w", inputName, resolveErr)
}
@@ -391,6 +396,7 @@ func resolveScriptoriumInput(
m *manifest.Manifest,
paths artifacts.SessionPaths,
sessionDir string,
runtimeCatalog *artifacts.ArtifactCatalog,
) (string, bool, error) {
switch strings.TrimSpace(inputCfg.Source) {
case "previous_session_artifact":
@@ -403,11 +409,17 @@ func resolveScriptoriumInput(
}
return resolved, true, nil
default:
resolved, err := artifacts.ResolveSessionArtifact(paths, m, inputCfg.Source)
resolved, err := artifacts.ResolveSessionArtifactWithCatalog(paths, m, inputCfg.Source, runtimeCatalog)
if err == nil {
return resolved.Path, true, nil
}
if errors.Is(err, artifacts.ErrSessionArtifactNotFound) {
if artifacts.IsConfiguredArtifactSource(inputCfg.Source) {
if inputCfg.Required {
return "", false, fmt.Errorf("configured artifact source %q is unavailable", inputCfg.Source)
}
return "", false, nil
}
normalized, normalizeErr := artifacts.NormalizeSessionArtifactSource(inputCfg.Source)
if normalizeErr != nil {
return "", false, normalizeErr
@@ -427,6 +439,52 @@ func resolveScriptoriumInput(
}
}
func buildAnalyzeRuntimeArtifactCatalog(
paths artifacts.SessionPaths,
scriptoriumCfg *config.ScriptoriumConfig,
selectedArtifacts []string,
) (*artifacts.ArtifactCatalog, error) {
catalog := artifacts.NewArtifactCatalog()
if err := catalog.RegisterBuiltIns(); err != nil {
return nil, err
}
if scriptoriumCfg == nil {
return catalog, nil
}
configured := map[string]artifacts.ConfiguredArtifactDefinition{}
for key, artifactCfg := range scriptoriumCfg.Artifacts {
configured[key] = artifacts.ConfiguredArtifactDefinition{
Enabled: artifactCfg.Enabled,
OutputPath: artifactCfg.OutputPath,
}
}
if err := catalog.RegisterConfiguredArtifacts(configured, selectedArtifacts); err != nil {
return nil, err
}
for _, entry := range catalog.ListConfigured() {
if entry.Executable {
continue
}
if strings.TrimSpace(entry.CanonicalRelPath) == "" {
continue
}
resolvedPath, err := resolveScriptoriumOutputPath(paths, entry.CanonicalRelPath)
if err != nil {
continue
}
if err := requireNonEmptyFile(resolvedPath, "configured artifact "+entry.SourceID); err != nil {
continue
}
if err := catalog.MarkAvailableFromDisk(entry.SourceID, resolvedPath); err != nil {
return nil, err
}
}
return catalog, nil
}
func resolveInputPathForRead(paths artifacts.SessionPaths, sessionDir, pathValue string) string {
trimmed := strings.TrimSpace(pathValue)
if trimmed == "" {

View File

@@ -414,6 +414,89 @@ func TestAnalyzeFailsWhenRequiredPreviousRecapMissing(t *testing.T) {
}
}
func TestAnalyzeResolvesConfiguredArtifactInputFromDisabledArtifactOutput(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
playerHandoutPath := filepath.Join(paths.ArtifactsDir, "player_handout.md")
writeAnalyzeFile(t, playerHandoutPath, "handout\n")
sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
sessionRecap.Inputs["recap"] = config.ScriptoriumInputConfig{
Source: "narratio.artifact.player_handout",
Required: true,
}
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = sessionRecap
env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{
Enabled: false,
OutputPath: "artifacts/player_handout.md",
}
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatalf("Run() error = %v", err)
}
if len(fake.RunRequests) != 1 {
t.Fatalf("run requests = %d, want 1", len(fake.RunRequests))
}
if got := fake.RunRequests[0].InputPaths["recap"]; got != playerHandoutPath {
t.Fatalf("recap input = %q, want %q", got, playerHandoutPath)
}
}
func TestAnalyzeFailsWhenRequiredConfiguredArtifactMissing(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
sessionRecap.Inputs["recap"] = config.ScriptoriumInputConfig{
Source: "narratio.artifact.player_handout",
Required: true,
}
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = sessionRecap
env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{
Enabled: false,
OutputPath: "artifacts/player_handout.md",
}
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), `configured artifact source "narratio.artifact.player_handout" is unavailable`) {
t.Fatalf("error = %q, want configured artifact unavailable context", err.Error())
}
}
func TestAnalyzeOmitsOptionalMissingConfiguredArtifactInput(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
sessionRecap.Inputs["recap"] = config.ScriptoriumInputConfig{
Source: "narratio.artifact.player_handout",
Required: false,
}
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = sessionRecap
env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{
Enabled: false,
OutputPath: "artifacts/player_handout.md",
}
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatalf("Run() error = %v", err)
}
if len(fake.RunRequests) != 1 {
t.Fatalf("run requests = %d, want 1", len(fake.RunRequests))
}
if _, exists := fake.RunRequests[0].InputPaths["recap"]; exists {
t.Fatalf("optional recap input should be omitted when unavailable, got %q", fake.RunRequests[0].InputPaths["recap"])
}
}
func TestAnalyzeFailsWhenOutputPathMissing(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)