Consolidate analyze session execution state

This commit is contained in:
2026-09-04 15:57:54 +00:00
parent 5c520dc0d4
commit 172e2d6a4f
12 changed files with 401 additions and 268 deletions

View File

@@ -173,7 +173,7 @@ changing its persistence timing. This stage is behavior-preserving.
## Stage 2 — Establish Session-Only Analyze Execution State
**Status:** Pending
**Status:** Completed
### Goal

View File

@@ -283,6 +283,10 @@ func mapResultOutputs(stageName string, result *stage.StageResult, runID string)
sourceID = sourceIDForOutputKind(kind)
}
}
producerRunID := strings.TrimSpace(ref.ProducerRunID)
if producerRunID == "" {
producerRunID = runID
}
out = append(out, manifest.ArtifactRecord{
Kind: kind,
SourceID: sourceID,
@@ -290,7 +294,7 @@ func mapResultOutputs(stageName string, result *stage.StageResult, runID string)
ArchivePath: ref.ArchivePath,
Contract: cloneContractMetadata(ref.Contract),
ExternalProvenance: cloneExternalProvenance(ref.ExternalProvenance),
ProducerRunID: runID,
ProducerRunID: producerRunID,
RemoteKey: ref.RemoteKey,
Checksum: ref.Checksum,
})

View File

@@ -375,6 +375,7 @@ func TestMapResultOutputsPrefersExplicitSourceAndCopiesMetadata(t *testing.T) {
Kind: "structured_data",
SourceID: "narratio.example.npcs",
RelativePath: "artifacts/npcs.json",
ProducerRunID: "original-run",
Contract: contract,
ExternalProvenance: provenance,
}}}
@@ -389,6 +390,9 @@ func TestMapResultOutputsPrefersExplicitSourceAndCopiesMetadata(t *testing.T) {
if got[0].Kind != "structured_data" {
t.Fatalf("kind = %q, want explicit output kind preserved", got[0].Kind)
}
if got[0].ProducerRunID != "original-run" {
t.Fatalf("producer run id = %q, want explicit producer", got[0].ProducerRunID)
}
if got[0].Contract == nil || *got[0].Contract != *contract {
t.Fatalf("contract = %#v, want %#v", got[0].Contract, contract)
}
@@ -407,11 +411,17 @@ func TestMapResultOutputsRetainsFallbackInference(t *testing.T) {
if len(transcript) != 1 || transcript[0].SourceID != artifacts.ArtifactTranscriptFinalTrimmed {
t.Fatalf("transcript fallback = %#v, want final-trimmed source", transcript)
}
if transcript[0].ProducerRunID != "run-id" {
t.Fatalf("transcript producer run id = %q, want active run fallback", transcript[0].ProducerRunID)
}
analyze := mapResultOutputs("analyze", &stage.StageResult{Outputs: []artifacts.Ref{{Kind: "session_recap"}}}, "run-id")
if len(analyze) != 1 || analyze[0].SourceID != "narratio.artifact.session_recap" || analyze[0].Kind != "scriptorium_artifact" {
t.Fatalf("analyze fallback = %#v, want configured artifact inference", analyze)
}
if analyze[0].ProducerRunID != "run-id" {
t.Fatalf("analyze producer run id = %q, want active run fallback", analyze[0].ProducerRunID)
}
}
func TestNeedsObjectStoreForRunPrepareWithPreviousRequirements(t *testing.T) {

View File

@@ -18,6 +18,7 @@ type Ref struct {
ArchivePath string
RemoteKey string
Checksum string
ProducerRunID string
Contract *artifactmodel.ContractMetadata
ExternalProvenance *artifactmodel.ExternalProvenance
}

View File

@@ -142,25 +142,7 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
return nil, fmt.Errorf("analyze: scriptorium adapter is required")
}
logs := []string{}
generatedConfigs := []string{}
artifactMetadata := make([]map[string]any, 0, len(workPlan.ExecutionOrder))
reusedArtifacts := []map[string]any{}
reusedSeen := map[string]struct{}{}
sessionRecords := manifest.CloneAnalyzeArtifactCollection(workPlan.ProjectedRecords)
priorCurrentRecords := make(map[string]manifest.AnalyzeArtifactRecord)
for _, item := range reconciliation.Ordered {
if item.Stored != nil && item.Stored.Status == manifest.AnalyzeArtifactCurrent {
priorCurrentRecords[item.Key] = *item.Stored
}
}
currentKeys := make(map[string]struct{}, len(workPlan.ExecutionOrder)+len(workPlan.ReusedCurrent))
for _, item := range workPlan.ExecutionOrder {
currentKeys[item.Key] = struct{}{}
}
for _, item := range workPlan.ReusedCurrent {
currentKeys[item.Key] = struct{}{}
}
state := newAnalyzeExecutionState(execution, workPlan, reconciliation)
for _, item := range workPlan.ExecutionOrder {
artifactCfg := env.Config.Pipeline.Scriptorium.Artifacts[item.Key]
@@ -171,158 +153,18 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
)
if err != nil {
executionErr := fmt.Errorf("analyze: compute execution fingerprint for artifact %q: %w", item.Key, err)
return failedAnalyzeResult(
execution,
item,
artifactCfg,
"",
executionErr,
sessionRecords,
), executionErr
return state.failedResult(item, artifactCfg, "", executionErr), executionErr
}
priorRecord, hadPriorRecord := priorCurrentRecords[item.Key]
plan := analyzeArtifactExecutionPlan{Name: item.Key, Cfg: artifactCfg}
artifactResult, err := executeAnalyzeArtifact(ctx, execution, plan)
if err != nil {
return failedAnalyzeResult(
execution,
item,
artifactCfg,
fingerprint,
err,
sessionRecords,
), err
return state.failedResult(item, artifactCfg, fingerprint, err), err
}
logs = append(logs, artifactResult.Logs...)
generatedConfigs = append(generatedConfigs, artifactResult.GeneratedConfigs...)
if origin, ok := analyzeArtifactOrigin(execution, plan.Name); ok {
artifactResult.Metadata["family"] = origin.Family
artifactResult.Metadata["character_id"] = origin.CharacterID
}
artifactMetadata = append(artifactMetadata, artifactResult.Metadata)
for _, reused := range artifactResult.ReusedArtifacts {
sourceID, _ := reused["source_id"].(string)
path, _ := reused["path"].(string)
key := sourceID + "|" + path
if _, exists := reusedSeen[key]; exists {
continue
}
reusedSeen[key] = struct{}{}
reusedArtifacts = append(reusedArtifacts, reused)
}
record, err := currentAnalyzeArtifactRecord(
execution,
plan,
fingerprint,
artifactResult,
)
if err != nil {
recordErr := fmt.Errorf("analyze: record artifact %q: %w", plan.Name, err)
return failedAnalyzeResult(
execution,
item,
artifactCfg,
fingerprint,
recordErr,
sessionRecords,
), recordErr
}
sessionRecords[plan.Name] = record
sourceID, ok := runtimeCatalog.SourceIDForConfiguredKey(plan.Name)
if !ok {
catalogErr := fmt.Errorf("analyze: source id not found for artifact %q", plan.Name)
return failedAnalyzeResult(
execution, item, artifactCfg, fingerprint, catalogErr,
sessionRecords,
), catalogErr
}
if err := runtimeCatalog.MarkAvailableGeneratedEvidence(
sourceID,
artifactResult.Output.AbsolutePath,
record.ProducerRunID,
record.Output.Checksum,
record.OutputSize,
record.Output.Contract,
); err != nil {
catalogErr := fmt.Errorf("analyze: mark generated artifact %q available: %w", sourceID, err)
return failedAnalyzeResult(
execution, item, artifactCfg, fingerprint, catalogErr,
sessionRecords,
), catalogErr
}
if !hadPriorRecord || !sameAnalyzeOutputIdentity(priorRecord, record) {
staleUnscheduledAnalyzeDependents(
env.Config.Pipeline.Scriptorium.Artifacts,
plan.Name,
currentKeys,
sessionRecords,
)
if err := state.acceptArtifact(plan, fingerprint, artifactResult); err != nil {
return state.failedResult(item, artifactCfg, fingerprint, err), err
}
}
metadata := map[string]any{
"stage": "analyze",
"selected_artifacts": append([]string(nil), workPlan.ExplicitTargets...),
"executed_artifacts": analyzePlanKeysForMetadata(workPlan.ExecutionOrder),
"reused_current": analyzePlanKeysForMetadata(workPlan.ReusedCurrent),
"generated_artifacts": artifactMetadata,
"reused_artifacts": reusedArtifacts,
"artifact_count": len(artifactMetadata),
"reused_artifact_count": len(reusedArtifacts),
}
if len(artifactMetadata) == 1 {
for k, v := range artifactMetadata[0] {
metadata[k] = v
}
}
return &StageResult{
Logs: dedupeAndSortPaths(logs),
GeneratedConfigs: dedupeAndSortPaths(generatedConfigs),
Metadata: metadata,
AnalyzeState: &AnalyzeStateProjection{
Session: sessionRecords,
},
}, nil
}
func failedAnalyzeResult(
execution analyzeExecutionContext,
item analyzePlanItem,
artifactCfg config.ScriptoriumArtifactConfig,
fingerprint string,
cause error,
sessionRecords map[string]manifest.AnalyzeArtifactRecord,
) *StageResult {
record := manifest.AnalyzeArtifactRecord{
Key: item.Key,
Status: manifest.AnalyzeArtifactFailed,
Dependencies: normalizedAnalyzeDependencyKeys(artifactCfg.DependsOn),
ProducerRunID: analyzeProducerRunID(execution),
UpdatedAt: time.Now().UTC(),
Error: NonResumable(cause.Error()).Reason,
}
if origin, ok := analyzeArtifactOrigin(execution, item.Key); ok {
record.Family = origin.Family
record.CharacterID = origin.CharacterID
}
if fingerprint != "" {
record.FingerprintVersion = manifest.AnalyzeFingerprintContractVersion
record.Fingerprint = fingerprint
}
if artifactCfg.PromptID != "" || artifactCfg.ProfileID != "" {
record.Scriptorium = &manifest.AnalyzeArtifactProvenance{
PromptID: artifactCfg.PromptID, ProfileID: artifactCfg.ProfileID,
}
}
sessionRecords[item.Key] = record
staleAnalyzeDependents(artifactCfgMap(execution), item.Key, sessionRecords)
return &StageResult{AnalyzeState: &AnalyzeStateProjection{
Session: manifest.CloneAnalyzeArtifactCollection(sessionRecords),
}}
return state.successResult(), nil
}
func analyzeArtifactOrigin(execution analyzeExecutionContext, key string) (config.ArtifactFamilyMemberOrigin, bool) {
@@ -464,7 +306,7 @@ func staleAnalyzeDependentClosure(
if eligible != nil && !eligible(key) {
continue
}
staleProjectedAnalyzeRecord(records, key)
staleAnalyzeRecord(records, key)
queue = append(queue, reverse[key]...)
}
}

View File

@@ -0,0 +1,236 @@
package stage
import (
"fmt"
"sort"
"strings"
"time"
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/config"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
)
type analyzeExecutionState struct {
execution analyzeExecutionContext
workPlan analyzeWorkPlan
records map[string]manifest.AnalyzeArtifactRecord
priorCurrentRecords map[string]manifest.AnalyzeArtifactRecord
evaluatedKeys map[string]struct{}
catalog *artifacts.ArtifactCatalog
logs []string
generatedConfigs []string
artifactMetadata []map[string]any
reusedArtifacts []map[string]any
reusedSeen map[string]struct{}
}
func newAnalyzeExecutionState(
execution analyzeExecutionContext,
workPlan analyzeWorkPlan,
reconciliation analyzeReconciliation,
) *analyzeExecutionState {
state := &analyzeExecutionState{
execution: execution,
workPlan: workPlan,
records: manifest.CloneAnalyzeArtifactCollection(workPlan.StartingRecords),
priorCurrentRecords: make(map[string]manifest.AnalyzeArtifactRecord),
evaluatedKeys: make(map[string]struct{}, len(workPlan.ExecutionOrder)+len(workPlan.ReusedCurrent)),
catalog: execution.Catalog,
artifactMetadata: make([]map[string]any, 0, len(workPlan.ExecutionOrder)),
reusedSeen: make(map[string]struct{}),
}
for _, item := range reconciliation.Ordered {
if item.Stored != nil && item.Stored.Status == manifest.AnalyzeArtifactCurrent {
state.priorCurrentRecords[item.Key] = *item.Stored
}
}
for _, item := range workPlan.ExecutionOrder {
state.evaluatedKeys[item.Key] = struct{}{}
}
for _, item := range workPlan.ReusedCurrent {
state.evaluatedKeys[item.Key] = struct{}{}
}
return state
}
func (state *analyzeExecutionState) acceptArtifact(
plan analyzeArtifactExecutionPlan,
fingerprint string,
result *analyzeArtifactExecutionResult,
) error {
state.collectDiagnostics(plan.Name, result)
record, err := currentAnalyzeArtifactRecord(state.execution, plan, fingerprint, result)
if err != nil {
return fmt.Errorf("analyze: record artifact %q: %w", plan.Name, err)
}
state.records[plan.Name] = record
sourceID, ok := state.catalog.SourceIDForConfiguredKey(plan.Name)
if !ok {
return fmt.Errorf("analyze: source id not found for artifact %q", plan.Name)
}
if err := state.catalog.MarkAvailableGeneratedEvidence(
sourceID,
result.Output.AbsolutePath,
record.ProducerRunID,
record.Output.Checksum,
record.OutputSize,
record.Output.Contract,
); err != nil {
return fmt.Errorf("analyze: mark generated artifact %q available: %w", sourceID, err)
}
prior, hadPrior := state.priorCurrentRecords[plan.Name]
if !hadPrior || !sameAnalyzeOutputIdentity(prior, record) {
staleUnscheduledAnalyzeDependents(
artifactCfgMap(state.execution),
plan.Name,
state.evaluatedKeys,
state.records,
)
}
return nil
}
func (state *analyzeExecutionState) collectDiagnostics(name string, result *analyzeArtifactExecutionResult) {
state.logs = append(state.logs, result.Logs...)
state.generatedConfigs = append(state.generatedConfigs, result.GeneratedConfigs...)
if origin, ok := analyzeArtifactOrigin(state.execution, name); ok {
result.Metadata["family"] = origin.Family
result.Metadata["character_id"] = origin.CharacterID
}
state.artifactMetadata = append(state.artifactMetadata, result.Metadata)
for _, reused := range result.ReusedArtifacts {
sourceID, _ := reused["source_id"].(string)
path, _ := reused["path"].(string)
key := sourceID + "|" + path
if _, exists := state.reusedSeen[key]; exists {
continue
}
state.reusedSeen[key] = struct{}{}
state.reusedArtifacts = append(state.reusedArtifacts, reused)
}
}
func (state *analyzeExecutionState) failedResult(
item analyzePlanItem,
artifactCfg config.ScriptoriumArtifactConfig,
fingerprint string,
cause error,
) *StageResult {
record := manifest.AnalyzeArtifactRecord{
Key: item.Key,
Status: manifest.AnalyzeArtifactFailed,
Dependencies: normalizedAnalyzeDependencyKeys(artifactCfg.DependsOn),
ProducerRunID: analyzeProducerRunID(state.execution),
UpdatedAt: time.Now().UTC(),
Error: NonResumable(cause.Error()).Reason,
}
if origin, ok := analyzeArtifactOrigin(state.execution, item.Key); ok {
record.Family = origin.Family
record.CharacterID = origin.CharacterID
}
if fingerprint != "" {
record.FingerprintVersion = manifest.AnalyzeFingerprintContractVersion
record.Fingerprint = fingerprint
}
if artifactCfg.PromptID != "" || artifactCfg.ProfileID != "" {
record.Scriptorium = &manifest.AnalyzeArtifactProvenance{
PromptID: artifactCfg.PromptID, ProfileID: artifactCfg.ProfileID,
}
}
state.records[item.Key] = record
staleAnalyzeDependents(artifactCfgMap(state.execution), item.Key, state.records)
state.installManifestCollection()
return &StageResult{AnalyzeState: state.projection()}
}
func (state *analyzeExecutionState) successResult() *StageResult {
metadata := map[string]any{
"stage": "analyze",
"selected_artifacts": append([]string(nil), state.workPlan.ExplicitTargets...),
"executed_artifacts": analyzePlanKeysForMetadata(state.workPlan.ExecutionOrder),
"reused_current": analyzePlanKeysForMetadata(state.workPlan.ReusedCurrent),
"generated_artifacts": state.artifactMetadata,
"reused_artifacts": state.reusedArtifacts,
"artifact_count": len(state.artifactMetadata),
"reused_artifact_count": len(state.reusedArtifacts),
}
if len(state.artifactMetadata) == 1 {
for key, value := range state.artifactMetadata[0] {
metadata[key] = value
}
}
state.installManifestCollection()
return &StageResult{
Logs: dedupeAndSortPaths(state.logs),
GeneratedConfigs: dedupeAndSortPaths(state.generatedConfigs),
Metadata: metadata,
AnalyzeState: state.projection(),
}
}
func (state *analyzeExecutionState) installManifestCollection() {
m := state.execution.Manifest
if m == nil {
return
}
if m.Stages == nil {
m.Stages = make(map[string]*manifest.StageRecord)
}
record := m.Stages["analyze"]
if record == nil {
record = &manifest.StageRecord{Name: "analyze"}
m.Stages["analyze"] = record
}
record.AnalyzeStateVersion = manifest.AnalyzeStateContractVersion
record.AnalyzeArtifacts = manifest.CloneAnalyzeArtifactCollection(state.records)
}
func (state *analyzeExecutionState) projection() *AnalyzeStateProjection {
return &AnalyzeStateProjection{Session: manifest.CloneAnalyzeArtifactCollection(state.records)}
}
func analyzeArtifactRefs(records map[string]manifest.AnalyzeArtifactRecord) []artifacts.Ref {
keys := make([]string, 0, len(records))
for key, record := range records {
if record.Status == manifest.AnalyzeArtifactCurrent && record.Output != nil {
keys = append(keys, key)
}
}
sortStringsByNormalizedArtifactKey(keys)
refs := make([]artifacts.Ref, 0, len(keys))
for _, key := range keys {
record := records[key]
output := record.Output
producerRunID := strings.TrimSpace(output.ProducerRunID)
if producerRunID == "" {
producerRunID = strings.TrimSpace(record.ProducerRunID)
}
refs = append(refs, artifacts.Ref{
Kind: output.Kind,
SourceID: output.SourceID,
RelativePath: output.LocalPath,
ArchivePath: output.ArchivePath,
RemoteKey: output.RemoteKey,
Checksum: output.Checksum,
Contract: cloneAnalyzeOutputContract(output.Contract),
ExternalProvenance: cloneAnalyzeExternalProvenance(output.ExternalProvenance),
ProducerRunID: producerRunID,
})
}
return refs
}
func sortStringsByNormalizedArtifactKey(keys []string) {
sort.Strings(keys)
}
func cloneAnalyzeExternalProvenance(value *artifactmodel.ExternalProvenance) *artifactmodel.ExternalProvenance {
if value == nil {
return nil
}
cloned := *value
return &cloned
}

View File

@@ -13,7 +13,7 @@ import (
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
)
func TestAnalyzeAdapterFailureProjectsCompletedAndFailedArtifacts(t *testing.T) {
func TestAnalyzeAdapterFailureRecordsCompletedAndFailedArtifacts(t *testing.T) {
for _, failAt := range []int{1, 2, 3} {
t.Run(map[int]string{1: "first", 2: "middle", 3: "last"}[failAt], func(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t)
@@ -26,12 +26,13 @@ func TestAnalyzeAdapterFailureProjectsCompletedAndFailedArtifacts(t *testing.T)
if !errors.Is(err, cause) {
t.Fatalf("Run() error = %v, want injected cause", err)
}
if result == nil || result.AnalyzeState == nil {
t.Fatal("failure result has no analyze projection")
if result == nil || m.Stages["analyze"] == nil {
t.Fatal("failure did not expose analyze manifest state")
}
records := m.Stages["analyze"].AnalyzeArtifacts
keys := []string{"alpha", "beta", "gamma"}
for index, key := range keys {
record, exists := result.AnalyzeState.Session[key]
record, exists := records[key]
switch {
case index < failAt-1:
if !exists || record.Status != manifest.AnalyzeArtifactCurrent || record.Output == nil {
@@ -54,11 +55,10 @@ func TestAnalyzeAdapterFailureProjectsCompletedAndFailedArtifacts(t *testing.T)
func TestAnalyzeMiddleFailureStalesDependentsAndPreservesUnrelatedCurrent(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t)
env.Config.Pipeline.Scriptorium.Artifacts = chainedAnalyzeArtifacts()
first, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
installAnalyzeProjection(m, first.AnalyzeState)
unrelated := m.Stages["analyze"].AnalyzeArtifacts["unrelated"]
stale := m.Stages["analyze"].AnalyzeArtifacts["alpha"]
stale.Status = manifest.AnalyzeArtifactStale
@@ -68,36 +68,37 @@ func TestAnalyzeMiddleFailureStalesDependentsAndPreservesUnrelatedCurrent(t *tes
env.SelectedArtifactKeys = []string{"gamma"}
cause := errors.New("middle artifact failed")
env.Scriptorium = &indexedAnalyzeFailureRunner{FailAt: 2, Err: cause}
result, err := (analyzeStage{}).Run(context.Background(), env, m)
if !errors.Is(err, cause) {
t.Fatalf("Run() error = %v", err)
}
if result.AnalyzeState.Session["alpha"].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("completed prerequisite = %#v", result.AnalyzeState.Session["alpha"])
}
if result.AnalyzeState.Session["beta"].Status != manifest.AnalyzeArtifactFailed {
t.Fatalf("failed middle = %#v", result.AnalyzeState.Session["beta"])
}
if target := result.AnalyzeState.Session["gamma"]; target.Status != manifest.AnalyzeArtifactStale || target.Output != nil {
t.Fatalf("dependent target = %#v, want stale", target)
}
if got := result.AnalyzeState.Session["unrelated"]; got.Status != manifest.AnalyzeArtifactCurrent ||
got.Fingerprint != unrelated.Fingerprint || got.Output == nil || got.Output.Checksum != unrelated.Output.Checksum {
t.Fatalf("unrelated record = %#v, want preserved %#v", got, unrelated)
}
if result.AnalyzeState.Session["gamma"].Status != manifest.AnalyzeArtifactStale {
t.Fatalf("dependent target state = %#v, want stale", result.AnalyzeState.Session["gamma"])
}
if old := m.Stages["analyze"].AnalyzeArtifacts["beta"]; old.Output == nil {
t.Fatal("fixture did not retain old canonical evidence before failure")
}
if failed := result.AnalyzeState.Session["beta"]; failed.Output != nil {
_, err = (analyzeStage{}).Run(context.Background(), env, m)
if !errors.Is(err, cause) {
t.Fatalf("Run() error = %v", err)
}
records := m.Stages["analyze"].AnalyzeArtifacts
if records["alpha"].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("completed prerequisite = %#v", records["alpha"])
}
if records["beta"].Status != manifest.AnalyzeArtifactFailed {
t.Fatalf("failed middle = %#v", records["beta"])
}
if target := records["gamma"]; target.Status != manifest.AnalyzeArtifactStale || target.Output != nil {
t.Fatalf("dependent target = %#v, want stale", target)
}
if got := records["unrelated"]; got.Status != manifest.AnalyzeArtifactCurrent ||
got.Fingerprint != unrelated.Fingerprint || got.Output == nil || got.Output.Checksum != unrelated.Output.Checksum {
t.Fatalf("unrelated record = %#v, want preserved %#v", got, unrelated)
}
if records["gamma"].Status != manifest.AnalyzeArtifactStale {
t.Fatalf("dependent target state = %#v, want stale", records["gamma"])
}
if failed := records["beta"]; failed.Output != nil {
t.Fatal("old canonical bytes made failed artifact current")
}
}
func TestAnalyzeInvalidOutputReturnsFailedProjection(t *testing.T) {
func TestAnalyzeInvalidOutputRecordsFailure(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
@@ -105,17 +106,17 @@ func TestAnalyzeInvalidOutputReturnsFailedProjection(t *testing.T) {
m.RunID = "run-invalid-output"
env.Scriptorium = invalidAnalyzeOutputRunner{}
result, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err == nil {
t.Fatal("Run() error = nil")
}
record := result.AnalyzeState.Session["session_recap"]
record := m.Stages["analyze"].AnalyzeArtifacts["session_recap"]
if record.Status != manifest.AnalyzeArtifactFailed || record.Output != nil {
t.Fatalf("failed record = %#v", record)
}
}
func TestAnalyzeMaterializationFailureReturnsFailedProjection(t *testing.T) {
func TestAnalyzeMaterializationFailureRecordsFailure(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
@@ -125,11 +126,11 @@ func TestAnalyzeMaterializationFailureReturnsFailedProjection(t *testing.T) {
canonical := filepath.Join(paths.ArtifactsDir, "session_recap.md")
env.ArtifactStore = &failingAnalyzeArtifactStore{Store: env.ArtifactStore, FailPath: canonical, Err: cause}
result, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if !errors.Is(err, cause) {
t.Fatalf("Run() error = %v, want write cause", err)
}
record := result.AnalyzeState.Session["session_recap"]
record := m.Stages["analyze"].AnalyzeArtifacts["session_recap"]
if record.Status != manifest.AnalyzeArtifactFailed || record.Output != nil {
t.Fatalf("failed record = %#v", record)
}

View File

@@ -19,7 +19,7 @@ func TestAnalyzeReusesCurrentArtifactWithoutScriptorium(t *testing.T) {
if err != nil {
t.Fatal(err)
}
installAnalyzeProjection(m, first.AnalyzeState)
assertAnalyzeProjectionMatchesManifest(t, m, first.AnalyzeState)
if len(fake.RunRequests) != 1 {
t.Fatalf("initial requests = %d, want 1", len(fake.RunRequests))
}
@@ -35,8 +35,8 @@ func TestAnalyzeReusesCurrentArtifactWithoutScriptorium(t *testing.T) {
if got := second.Metadata["reused_current"]; !reflect.DeepEqual(got, []string{"session_recap"}) {
t.Fatalf("reused current = %#v, want session_recap", got)
}
if second.AnalyzeState.Session["session_recap"].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("session state = %#v", second.AnalyzeState.Session)
if m.Stages["analyze"].AnalyzeArtifacts["session_recap"].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("session state = %#v", m.Stages["analyze"].AnalyzeArtifacts)
}
}
@@ -46,11 +46,10 @@ func TestAnalyzePartialForcePreservesUnrelatedCurrentRecord(t *testing.T) {
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
addAnalyzeDependentArtifact(env)
first, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
installAnalyzeProjection(m, first.AnalyzeState)
if len(fake.RunRequests) != 2 {
t.Fatalf("initial requests = %d, want 2", len(fake.RunRequests))
}
@@ -60,17 +59,17 @@ func TestAnalyzePartialForcePreservesUnrelatedCurrentRecord(t *testing.T) {
env.Force = true
secondRunner := &orderedScriptoriumRunner{RunBody: "scriptorium noop/fake run artifact\n"}
env.Scriptorium = secondRunner
second, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err = (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
if got := secondRunner.Calls; !reflect.DeepEqual(got, []string{"run"}) {
t.Fatalf("calls = %#v, want one forced target run", got)
}
if got := second.AnalyzeState.Session["player_handout"]; !reflect.DeepEqual(got, priorDependent) {
if got := m.Stages["analyze"].AnalyzeArtifacts["player_handout"]; !reflect.DeepEqual(got, priorDependent) {
t.Fatalf("unrelated dependent changed = %#v, want %#v", got, priorDependent)
}
if _, current := second.AnalyzeState.Session["player_handout"]; !current {
if _, current := m.Stages["analyze"].AnalyzeArtifacts["player_handout"]; !current {
t.Fatal("unselected dependent was removed from session state")
}
}
@@ -81,18 +80,17 @@ func TestAnalyzeExposesReusedPrerequisiteToScheduledDependent(t *testing.T) {
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
addAnalyzeDependentArtifact(env)
first, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
installAnalyzeProjection(m, first.AnalyzeState)
dependent := env.Config.Pipeline.Scriptorium.Artifacts["player_handout"]
dependent.PromptID = "dnd.player_handout.revised"
env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = dependent
env.SelectedArtifactKeys = []string{"player_handout"}
fake.RunRequests = nil
second, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err = (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
@@ -102,9 +100,9 @@ func TestAnalyzeExposesReusedPrerequisiteToScheduledDependent(t *testing.T) {
if got := fake.RunRequests[0].InputPaths["recap"]; got != filepath.Join(paths.ArtifactsDir, "session_recap.md") {
t.Fatalf("reused prerequisite input = %q", got)
}
if second.AnalyzeState.Session["session_recap"].Status != manifest.AnalyzeArtifactCurrent ||
second.AnalyzeState.Session["player_handout"].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("session records = %#v, want reused prerequisite and produced dependent", second.AnalyzeState.Session)
if m.Stages["analyze"].AnalyzeArtifacts["session_recap"].Status != manifest.AnalyzeArtifactCurrent ||
m.Stages["analyze"].AnalyzeArtifacts["player_handout"].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("session records = %#v, want reused prerequisite and produced dependent", m.Stages["analyze"].AnalyzeArtifacts)
}
}
@@ -114,25 +112,24 @@ func TestAnalyzeChangedOutputStalesUnselectedDependents(t *testing.T) {
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
addAnalyzeDependentArtifact(env)
first, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
installAnalyzeProjection(m, first.AnalyzeState)
env.SelectedArtifactKeys = []string{"session_recap"}
env.Force = true
env.Scriptorium = &orderedScriptoriumRunner{RunBody: "changed recap\n"}
second, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err = (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
dependent := second.AnalyzeState.Session["player_handout"]
dependent := m.Stages["analyze"].AnalyzeArtifacts["player_handout"]
if dependent.Status != manifest.AnalyzeArtifactStale || dependent.Output != nil || dependent.OutputSize != 0 {
t.Fatalf("dependent record = %#v, want stale without output", dependent)
}
if second.AnalyzeState.Session["player_handout"].Status != manifest.AnalyzeArtifactStale {
t.Fatalf("dependent state = %#v, want stale", second.AnalyzeState.Session["player_handout"])
if m.Stages["analyze"].AnalyzeArtifacts["player_handout"].Status != manifest.AnalyzeArtifactStale {
t.Fatalf("dependent state = %#v, want stale", m.Stages["analyze"].AnalyzeArtifacts["player_handout"])
}
}
@@ -149,11 +146,10 @@ func TestAnalyzeRebuildsNestedStalePrerequisitesInDependencyOrder(t *testing.T)
},
}
first, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
installAnalyzeProjection(m, first.AnalyzeState)
if len(fake.RunRequests) != 2 {
t.Fatalf("initial enabled requests = %d, want 2", len(fake.RunRequests))
}
@@ -165,7 +161,7 @@ func TestAnalyzeRebuildsNestedStalePrerequisitesInDependencyOrder(t *testing.T)
env.SelectedArtifactKeys = []string{"quest_log"}
fake.RunRequests = nil
second, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err = (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
@@ -178,8 +174,8 @@ func TestAnalyzeRebuildsNestedStalePrerequisitesInDependencyOrder(t *testing.T)
t.Fatalf("prompt order = %#v, want %#v", gotPrompts, wantPrompts)
}
for _, key := range []string{"session_recap", "player_handout", "quest_log"} {
if second.AnalyzeState.Session[key].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("%s state = %#v, want current", key, second.AnalyzeState.Session[key])
if m.Stages["analyze"].AnalyzeArtifacts[key].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("%s state = %#v, want current", key, m.Stages["analyze"].AnalyzeArtifacts[key])
}
}
}
@@ -207,7 +203,7 @@ func TestAnalyzeLegacyExecutionPromotesOnlyEffectiveArtifacts(t *testing.T) {
Outputs: []manifest.ArtifactRecord{{Kind: "player_handout", LocalPath: filepath.Join(paths.ArtifactsDir, "player_handout.md")}},
}
result, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
@@ -218,11 +214,11 @@ func TestAnalyzeLegacyExecutionPromotesOnlyEffectiveArtifacts(t *testing.T) {
if !reflect.DeepEqual(gotPrompts, test.wantPrompts) {
t.Fatalf("prompts = %#v, want %#v", gotPrompts, test.wantPrompts)
}
if len(result.AnalyzeState.Session) != len(test.wantPrompts) {
t.Fatalf("session records = %#v, want only regenerated effective artifacts", result.AnalyzeState.Session)
if len(m.Stages["analyze"].AnalyzeArtifacts) != len(test.wantPrompts) {
t.Fatalf("session records = %#v, want only regenerated effective artifacts", m.Stages["analyze"].AnalyzeArtifacts)
}
if len(test.selected) > 0 {
if _, promoted := result.AnalyzeState.Session["player_handout"]; promoted {
if _, promoted := m.Stages["analyze"].AnalyzeArtifacts["player_handout"]; promoted {
t.Fatal("legacy unselected canonical output was promoted")
}
}
@@ -240,10 +236,14 @@ func addAnalyzeDependentArtifact(env *Env) {
}
}
func installAnalyzeProjection(m *manifest.Manifest, projection *AnalyzeStateProjection) {
if m.Stages["analyze"] == nil {
m.Stages["analyze"] = &manifest.StageRecord{Name: "analyze"}
func assertAnalyzeProjectionMatchesManifest(
t *testing.T,
m *manifest.Manifest,
projection *AnalyzeStateProjection,
) {
t.Helper()
if projection == nil || m.Stages["analyze"] == nil ||
!reflect.DeepEqual(projection.Session, m.Stages["analyze"].AnalyzeArtifacts) {
t.Fatalf("projection = %#v, manifest records = %#v", projection, m.Stages["analyze"])
}
m.Stages["analyze"].AnalyzeStateVersion = manifest.AnalyzeStateContractVersion
m.Stages["analyze"].AnalyzeArtifacts = manifest.CloneAnalyzeArtifactCollection(projection.Session)
}

View File

@@ -40,7 +40,7 @@ type analyzeWorkPlan struct {
ReusedCurrent []analyzePlanItem
Invalidated []analyzePlanItem
Removed []analyzePlanItem
ProjectedRecords map[string]manifest.AnalyzeArtifactRecord
StartingRecords map[string]manifest.AnalyzeArtifactRecord
}
// planAnalyzeWork turns selection and reconciliation into one deterministic,
@@ -87,9 +87,9 @@ func planAnalyzeWork(
sort.Slice(removed, func(i, j int) bool { return removed[i].Key < removed[j].Key })
plan := analyzeWorkPlan{
ExplicitTargets: append([]string(nil), targets...),
Removed: removed,
ProjectedRecords: projectedAnalyzeRecords(scriptoriumCfg.Artifacts, reconciliation),
ExplicitTargets: append([]string(nil), targets...),
Removed: removed,
StartingRecords: reconciledAnalyzeRecords(scriptoriumCfg.Artifacts, reconciliation),
}
invalidated := make(map[string]struct{})
for _, state := range reconciliation.Ordered {
@@ -142,7 +142,7 @@ func planAnalyzeWork(
plan.Invalidated = append(plan.Invalidated, item)
invalidated[key] = struct{}{}
}
staleProjectedAnalyzeRecord(plan.ProjectedRecords, key)
staleAnalyzeRecord(plan.StartingRecords, key)
}
continue
}
@@ -183,11 +183,11 @@ func analyzeTargetDependencyClosure(
return closure, nil
}
func projectedAnalyzeRecords(
func reconciledAnalyzeRecords(
configured map[string]config.ScriptoriumArtifactConfig,
reconciliation analyzeReconciliation,
) map[string]manifest.AnalyzeArtifactRecord {
projected := make(map[string]manifest.AnalyzeArtifactRecord)
records := make(map[string]manifest.AnalyzeArtifactRecord)
for _, item := range reconciliation.Ordered {
if item.Stored == nil || item.Reason == analyzeReconciliationRemoved {
continue
@@ -202,15 +202,15 @@ func projectedAnalyzeRecords(
); err != nil {
continue
}
projected[item.Key] = record
records[item.Key] = record
if item.Reason != analyzeReconciliationCurrent && record.Status == manifest.AnalyzeArtifactCurrent {
staleProjectedAnalyzeRecord(projected, item.Key)
staleAnalyzeRecord(records, item.Key)
}
}
return manifest.CloneAnalyzeArtifactCollection(projected)
return manifest.CloneAnalyzeArtifactCollection(records)
}
func staleProjectedAnalyzeRecord(records map[string]manifest.AnalyzeArtifactRecord, key string) {
func staleAnalyzeRecord(records map[string]manifest.AnalyzeArtifactRecord, key string) {
record, ok := records[key]
if !ok || record.Status != manifest.AnalyzeArtifactCurrent {
return

View File

@@ -203,7 +203,7 @@ func TestPlanAnalyzeWorkRejectsUnknownSelectionAndInvalidGraphs(t *testing.T) {
}
}
func TestPlanAnalyzeWorkProjectionPreservesOnlyValidConfiguredAuthority(t *testing.T) {
func TestPlanAnalyzeWorkStartingRecordsPreserveOnlyValidConfiguredAuthority(t *testing.T) {
configured := map[string]config.ScriptoriumArtifactConfig{
"target": {Enabled: true}, "unrelated": {Enabled: true}, "invalid_unselected": {Enabled: true},
}
@@ -228,14 +228,14 @@ func TestPlanAnalyzeWorkProjectionPreservesOnlyValidConfiguredAuthority(t *testi
if err != nil {
t.Fatal(err)
}
if got := plan.ProjectedRecords["unrelated"].Status; got != manifest.AnalyzeArtifactCurrent {
if got := plan.StartingRecords["unrelated"].Status; got != manifest.AnalyzeArtifactCurrent {
t.Fatalf("unrelated status = %q, want current", got)
}
if got := plan.ProjectedRecords["invalid_unselected"].Status; got != manifest.AnalyzeArtifactStale {
if got := plan.StartingRecords["invalid_unselected"].Status; got != manifest.AnalyzeArtifactStale {
t.Fatalf("invalid unselected status = %q, want stale", got)
}
if _, exists := plan.ProjectedRecords["removed"]; exists {
t.Fatal("removed record survived projected authority")
if _, exists := plan.StartingRecords["removed"]; exists {
t.Fatal("removed record survived reconciled authority")
}
assertAnalyzePlanKeys(t, "invalidated", plan.Invalidated, []string{"invalid_unselected"})
assertAnalyzePlanKeys(t, "removed", plan.Removed, []string{"removed"})
@@ -256,8 +256,8 @@ func TestPlanAnalyzeWorkNonResumablePartialSelectionDoesNotPromoteUnselectedOutp
t.Fatal(err)
}
assertAnalyzePlanKeys(t, "execution", plan.ExecutionOrder, []string{"target"})
if len(plan.ProjectedRecords) != 0 {
t.Fatalf("non-resumable projection = %#v, want no current records", plan.ProjectedRecords)
if len(plan.StartingRecords) != 0 {
t.Fatalf("non-resumable starting records = %#v, want no current records", plan.StartingRecords)
}
}

View File

@@ -165,11 +165,10 @@ func TestAnalyzeResumeSemanticChangesRemainArtifactGranular(t *testing.T) {
"transcript": {Source: "narratio.transcript.final_trimmed", Required: true},
},
}
result, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
installAnalyzeProjection(m, result.AnalyzeState)
m.Stages["analyze"].Status = manifest.StatusSucceeded
test.mutate(env.Config.Pipeline.Scriptorium)
@@ -210,8 +209,8 @@ func TestAnalyzeStaleAggregateRestoresSuccessWithoutAdapterWork(t *testing.T) {
if got, _ := result.Metadata["executed_artifacts"].([]string); len(got) != 0 {
t.Fatalf("executed artifacts = %#v, want none", got)
}
if result.AnalyzeState.Session["session_recap"].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("session projection = %#v, want current recap", result.AnalyzeState.Session)
if m.Stages["analyze"].AnalyzeArtifacts["session_recap"].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("session records = %#v, want current recap", m.Stages["analyze"].AnalyzeArtifacts)
}
}
@@ -223,11 +222,10 @@ func currentAnalyzeFixture(t *testing.T, dependent bool) (*Env, *manifest.Manife
if dependent {
addAnalyzeDependentArtifact(env)
}
result, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatal(err)
}
installAnalyzeProjection(m, result.AnalyzeState)
m.Stages["analyze"].Status = manifest.StatusSucceeded
return env, m, len(fake.RunRequests)
}

View File

@@ -62,8 +62,8 @@ func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) {
t.Fatalf("session_id var = %q, want sticky narratio session id", req.Vars["session_id"])
}
if result.AnalyzeState == nil || result.AnalyzeState.Session["session_recap"].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("analyze state = %#v, want current session_recap", result.AnalyzeState)
if m.Stages["analyze"].AnalyzeArtifacts["session_recap"].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("analyze state = %#v, want current session_recap", m.Stages["analyze"].AnalyzeArtifacts)
}
if len(result.Logs) != 2 {
t.Fatalf("logs = %#v, want stdout+stderr logs", result.Logs)
@@ -306,7 +306,7 @@ func TestAnalyzeUsesRunLocalPathsAndMaterializesCanonical(t *testing.T) {
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
setCurrentBuiltInEvidence(t, m, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"))
result, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatalf("Run() error = %v", err)
}
@@ -316,8 +316,8 @@ func TestAnalyzeUsesRunLocalPathsAndMaterializesCanonical(t *testing.T) {
if !strings.Contains(fake.RunRequests[0].OutputPath, filepath.Join("runs", m.RunID, "analyze", "outputs")) {
t.Fatalf("run output path = %q, want run-local path", fake.RunRequests[0].OutputPath)
}
if result.AnalyzeState == nil || result.AnalyzeState.Session["session_recap"].Output == nil {
t.Fatalf("analyze state = %#v, want session recap output evidence", result.AnalyzeState)
if m.Stages["analyze"].AnalyzeArtifacts["session_recap"].Output == nil {
t.Fatalf("analyze state = %#v, want session recap output evidence", m.Stages["analyze"].AnalyzeArtifacts)
}
if err := requireNonEmptyFile(filepath.Join(paths.ArtifactsDir, "session_recap.md"), "materialized output"); err != nil {
t.Fatalf("canonical output = %v", err)
@@ -676,7 +676,7 @@ func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) {
}
env.SelectedArtifactKeys = []string{"player_handout"}
result, err := (analyzeStage{}).Run(context.Background(), env, m)
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatalf("Run() error = %v", err)
}
@@ -686,8 +686,8 @@ func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) {
if fake.RunRequests[0].PromptID != "dnd.player_handout" {
t.Fatalf("prompt id = %q, want dnd.player_handout", fake.RunRequests[0].PromptID)
}
if result.AnalyzeState == nil || len(result.AnalyzeState.Session) != 1 || result.AnalyzeState.Session["player_handout"].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("analyze state = %#v, want only current player_handout", result.AnalyzeState)
if len(m.Stages["analyze"].AnalyzeArtifacts) != 1 || m.Stages["analyze"].AnalyzeArtifacts["player_handout"].Status != manifest.AnalyzeArtifactCurrent {
t.Fatalf("analyze state = %#v, want only current player_handout", m.Stages["analyze"].AnalyzeArtifacts)
}
}
@@ -1391,7 +1391,7 @@ func TestAnalyzeRecordsRefsAndMetadata(t *testing.T) {
if err != nil {
t.Fatalf("Run() error = %v", err)
}
record, ok := result.AnalyzeState.Session["session_recap"]
record, ok := m.Stages["analyze"].AnalyzeArtifacts["session_recap"]
if !ok || record.Output == nil || record.Output.LocalPath != "artifacts/session_recap.md" {
t.Fatalf("analyze output record = %#v, want session recap evidence", record)
}
@@ -1409,6 +1409,47 @@ func TestAnalyzeRecordsRefsAndMetadata(t *testing.T) {
}
}
func TestAnalyzeArtifactRefsPreserveCurrentRecordIdentityInKeyOrder(t *testing.T) {
contract := &artifactmodel.ContractMetadata{
MediaType: "text/markdown", SchemaID: "narratio.recap", SchemaVersion: "1",
}
records := map[string]manifest.AnalyzeArtifactRecord{
"zeta": {
Key: "zeta", Status: manifest.AnalyzeArtifactCurrent, ProducerRunID: "record-run",
Output: &manifest.ArtifactRecord{
Kind: "scriptorium_artifact", SourceID: "narratio.artifact.zeta",
LocalPath: "artifacts/zeta.md", Checksum: strings.Repeat("a", 64), Contract: contract,
},
},
"alpha": {
Key: "alpha", Status: manifest.AnalyzeArtifactCurrent, ProducerRunID: "record-fallback",
Output: &manifest.ArtifactRecord{
Kind: "scriptorium_artifact", SourceID: "narratio.artifact.alpha",
LocalPath: "artifacts/alpha.md", Checksum: strings.Repeat("b", 64),
Contract: contract, ProducerRunID: "output-run",
},
},
"stale": {
Key: "stale", Status: manifest.AnalyzeArtifactStale,
},
}
refs := analyzeArtifactRefs(records)
if len(refs) != 2 || refs[0].SourceID != "narratio.artifact.alpha" || refs[1].SourceID != "narratio.artifact.zeta" {
t.Fatalf("refs = %#v, want alpha then zeta current records", refs)
}
if refs[0].ProducerRunID != "output-run" || refs[1].ProducerRunID != "record-run" {
t.Fatalf("producer run ids = [%q %q], want output and record identities", refs[0].ProducerRunID, refs[1].ProducerRunID)
}
if refs[0].RelativePath != "artifacts/alpha.md" || refs[0].Checksum != strings.Repeat("b", 64) ||
refs[0].Contract == nil || *refs[0].Contract != *contract {
t.Fatalf("alpha ref = %#v, want preserved output evidence", refs[0])
}
if refs[0].Contract == contract {
t.Fatal("artifact ref contract aliases manifest record")
}
}
func TestAnalyzeHandlesAdapterError(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)