Reconcile removed analysis artifacts

This commit is contained in:
2026-09-04 18:37:17 +00:00
parent a520cfdcc8
commit da99857fd0
3 changed files with 97 additions and 32 deletions

View File

@@ -71,25 +71,19 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
if err != nil {
return nil, fmt.Errorf("analyze: resolve run-stage layout: %w", err)
}
if env.Config.Pipeline.Scriptorium == nil {
return &StageResult{Metadata: map[string]any{
"stage": "analyze",
"skipped": true,
"reason": "pipeline.scriptorium is not configured",
}}, nil
}
if len(env.Config.Pipeline.Scriptorium.Artifacts) == 0 {
return &StageResult{Metadata: map[string]any{
"stage": "analyze",
"skipped": true,
"reason": "no scriptorium artifacts configured",
}}, nil
scriptoriumCfg := env.Config.Pipeline.Scriptorium
noWorkReason := ""
if scriptoriumCfg == nil {
noWorkReason = "pipeline.scriptorium is not configured"
scriptoriumCfg = &config.ScriptoriumConfig{}
} else if len(scriptoriumCfg.Artifacts) == 0 {
noWorkReason = "no scriptorium artifacts configured"
}
effective := env.EffectiveArtifacts
if !effective.Resolved() {
effective, err = artifacts.ResolveEffectiveArtifactSet(
artifacts.ConfiguredArtifactDefinitions(env.Config.Pipeline.Scriptorium.Artifacts),
artifacts.ConfiguredArtifactDefinitions(scriptoriumCfg.Artifacts),
env.SelectedArtifactKeys,
)
if err != nil {
@@ -100,7 +94,7 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
runtimeCatalog, err := buildAnalyzeRuntimeArtifactCatalog(
paths,
m,
env.Config.Pipeline.Scriptorium,
scriptoriumCfg,
env.Config.Pipeline.Notarius,
effective,
)
@@ -108,12 +102,8 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
return nil, fmt.Errorf("analyze: build runtime artifact catalog: %w", err)
}
if len(effective.Keys()) == 0 {
return &StageResult{Metadata: map[string]any{
"stage": "analyze",
"skipped": true,
"reason": "no selected scriptorium artifacts to execute",
}}, nil
if len(effective.Keys()) == 0 && noWorkReason == "" {
noWorkReason = "no selected scriptorium artifacts to execute"
}
execution := analyzeExecutionContext{
@@ -125,12 +115,12 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
TranscriptRefs: discoverAnalyzeTranscriptRefs(m, paths),
Catalog: runtimeCatalog,
}
reconciliation, err := reconcileAnalyzeArtifacts(env.Config.Pipeline.Scriptorium, execution)
reconciliation, err := reconcileAnalyzeArtifacts(scriptoriumCfg, execution)
if err != nil {
return nil, fmt.Errorf("analyze: reconcile configured artifacts: %w", err)
}
workPlan, err := planAnalyzeWork(
env.Config.Pipeline.Scriptorium,
scriptoriumCfg,
env.SelectedArtifactKeys,
env.Force,
reconciliation,
@@ -148,10 +138,10 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
}
for _, item := range workPlan.ExecutionOrder {
artifactCfg := env.Config.Pipeline.Scriptorium.Artifacts[item.Key]
artifactCfg := scriptoriumCfg.Artifacts[item.Key]
fingerprint, _, _, err := computeAnalyzeArtifactFingerprint(
item.Key,
env.Config.Pipeline.Scriptorium,
scriptoriumCfg,
execution,
)
if err != nil {
@@ -167,7 +157,12 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
return state.failedResult(item, artifactCfg, fingerprint, err), err
}
}
return state.successResult(), nil
result := state.successResult()
if noWorkReason != "" {
result.Metadata["skipped"] = true
result.Metadata["reason"] = noWorkReason
}
return result, nil
}
func analyzeArtifactOrigin(execution analyzeExecutionContext, key string) (config.ArtifactFamilyMemberOrigin, bool) {

View File

@@ -15,8 +15,8 @@ func (analyzeStage) ValidateResume(_ context.Context, env *Env, m *manifest.Mani
return ResumeValidation{}, fmt.Errorf("analyze resume: resolved stage environment config is required")
}
cfg := env.Config.Pipeline.Scriptorium
if cfg == nil || len(cfg.Artifacts) == 0 {
return ResumeValidation{Resumable: true, Analyze: &AnalyzeResumeSummary{}}, nil
if cfg == nil {
cfg = &config.ScriptoriumConfig{}
}
sessionID := ""
if m != nil {
@@ -39,9 +39,6 @@ func (analyzeStage) ValidateResume(_ context.Context, env *Env, m *manifest.Mani
return ResumeValidation{}, fmt.Errorf("analyze resume: resolve effective artifacts: %w", err)
}
}
if len(effective.Keys()) == 0 {
return ResumeValidation{Resumable: true, Analyze: &AnalyzeResumeSummary{}}, nil
}
paths := sessionPathsForEnv(env, sessionID)
catalog, err := buildAnalyzeRuntimeArtifactCatalog(
paths, m, cfg, env.Config.Pipeline.Notarius, effective,
@@ -62,9 +59,16 @@ func (analyzeStage) ValidateResume(_ context.Context, env *Env, m *manifest.Mani
return ResumeValidation{}, fmt.Errorf("analyze resume: plan configured artifacts: %w", err)
}
summary := analyzeResumeSummary(plan, env.Config.Pipeline)
if len(plan.ExecutionOrder) == 0 {
if len(plan.ExecutionOrder) == 0 && len(plan.Removed) == 0 {
return ResumeValidation{Resumable: true, Analyze: summary}, nil
}
if len(plan.ExecutionOrder) == 0 {
keys := analyzePlanKeysForMetadata(plan.Removed)
return ResumeValidation{
Reason: "analysis artifact state requires reconciliation: " + strings.Join(keys, ", "),
Analyze: summary,
}, nil
}
keys := analyzePlanKeysForMetadata(plan.ExecutionOrder)
return ResumeValidation{
Reason: "analysis artifacts require execution: " + strings.Join(keys, ", "),

View File

@@ -4,8 +4,11 @@ import (
"context"
"path/filepath"
"reflect"
"strings"
"testing"
"gitea.maximumdirect.net/eric/narratio/internal/adapters/scriptorium"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/config"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
)
@@ -37,6 +40,69 @@ func TestAnalyzeResumeValidationHonorsFullAndPartialSelections(t *testing.T) {
}
}
func TestAnalyzeRemovedConfigurationRequiresAndPersistsReconciliation(t *testing.T) {
for _, test := range []struct {
name string
remove func(*config.PipelineConfig)
reason string
}{
{
name: "scriptorium omitted",
remove: func(pipeline *config.PipelineConfig) {
pipeline.Scriptorium = nil
},
reason: "pipeline.scriptorium is not configured",
},
{
name: "artifact map empty",
remove: func(pipeline *config.PipelineConfig) {
pipeline.Scriptorium.Artifacts = map[string]config.ScriptoriumArtifactConfig{}
},
reason: "no scriptorium artifacts configured",
},
} {
t.Run(test.name, func(t *testing.T) {
env, m, initialRequests := currentAnalyzeFixture(t, false)
test.remove(env.Config.Pipeline)
validation, err := (analyzeStage{}).ValidateResume(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
if validation.Resumable || !strings.Contains(validation.Reason, "session_recap") {
t.Fatalf("validation = %#v, want reconciliation of removed session_recap", validation)
}
result, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
if result.Metadata["skipped"] != true || result.Metadata["reason"] != test.reason {
t.Fatalf("metadata = %#v, want skipped reason %q", result.Metadata, test.reason)
}
if len(result.Outputs) != 0 || len(m.Stages["analyze"].AnalyzeArtifacts) != 0 {
t.Fatalf("result outputs = %#v; analyze records = %#v, want both empty", result.Outputs, m.Stages["analyze"].AnalyzeArtifacts)
}
fake := env.Scriptorium.(*scriptorium.FakeRunner)
if len(fake.RunRequests) != initialRequests {
t.Fatalf("adapter requests = %d, want unchanged count %d", len(fake.RunRequests), initialRequests)
}
paths := sessionPathsForEnv(env, m.SessionID)
stored, err := env.ManifestStore.Load(
context.Background(),
artifacts.SessionManifestPathForCampaign(paths.WorkspaceRoot, m.Campaign, m.SessionID),
)
if err != nil {
t.Fatal(err)
}
if len(stored.Stages["analyze"].AnalyzeArtifacts) != 0 {
t.Fatalf("durable analyze records = %#v, want empty", stored.Stages["analyze"].AnalyzeArtifacts)
}
})
}
}
func TestAnalyzeResumeValidationReportsForceAndCurrentPrerequisites(t *testing.T) {
env, m, _ := currentAnalyzeFixture(t, true)
env.SelectedArtifactKeys = []string{"session_recap"}