Consolidate analyze session execution state
This commit is contained in:
@@ -173,7 +173,7 @@ changing its persistence timing. This stage is behavior-preserving.
|
|||||||
|
|
||||||
## Stage 2 — Establish Session-Only Analyze Execution State
|
## Stage 2 — Establish Session-Only Analyze Execution State
|
||||||
|
|
||||||
**Status:** Pending
|
**Status:** Completed
|
||||||
|
|
||||||
### Goal
|
### Goal
|
||||||
|
|
||||||
|
|||||||
@@ -283,6 +283,10 @@ func mapResultOutputs(stageName string, result *stage.StageResult, runID string)
|
|||||||
sourceID = sourceIDForOutputKind(kind)
|
sourceID = sourceIDForOutputKind(kind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
producerRunID := strings.TrimSpace(ref.ProducerRunID)
|
||||||
|
if producerRunID == "" {
|
||||||
|
producerRunID = runID
|
||||||
|
}
|
||||||
out = append(out, manifest.ArtifactRecord{
|
out = append(out, manifest.ArtifactRecord{
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
SourceID: sourceID,
|
SourceID: sourceID,
|
||||||
@@ -290,7 +294,7 @@ func mapResultOutputs(stageName string, result *stage.StageResult, runID string)
|
|||||||
ArchivePath: ref.ArchivePath,
|
ArchivePath: ref.ArchivePath,
|
||||||
Contract: cloneContractMetadata(ref.Contract),
|
Contract: cloneContractMetadata(ref.Contract),
|
||||||
ExternalProvenance: cloneExternalProvenance(ref.ExternalProvenance),
|
ExternalProvenance: cloneExternalProvenance(ref.ExternalProvenance),
|
||||||
ProducerRunID: runID,
|
ProducerRunID: producerRunID,
|
||||||
RemoteKey: ref.RemoteKey,
|
RemoteKey: ref.RemoteKey,
|
||||||
Checksum: ref.Checksum,
|
Checksum: ref.Checksum,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -375,6 +375,7 @@ func TestMapResultOutputsPrefersExplicitSourceAndCopiesMetadata(t *testing.T) {
|
|||||||
Kind: "structured_data",
|
Kind: "structured_data",
|
||||||
SourceID: "narratio.example.npcs",
|
SourceID: "narratio.example.npcs",
|
||||||
RelativePath: "artifacts/npcs.json",
|
RelativePath: "artifacts/npcs.json",
|
||||||
|
ProducerRunID: "original-run",
|
||||||
Contract: contract,
|
Contract: contract,
|
||||||
ExternalProvenance: provenance,
|
ExternalProvenance: provenance,
|
||||||
}}}
|
}}}
|
||||||
@@ -389,6 +390,9 @@ func TestMapResultOutputsPrefersExplicitSourceAndCopiesMetadata(t *testing.T) {
|
|||||||
if got[0].Kind != "structured_data" {
|
if got[0].Kind != "structured_data" {
|
||||||
t.Fatalf("kind = %q, want explicit output kind preserved", got[0].Kind)
|
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 {
|
if got[0].Contract == nil || *got[0].Contract != *contract {
|
||||||
t.Fatalf("contract = %#v, want %#v", 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 {
|
if len(transcript) != 1 || transcript[0].SourceID != artifacts.ArtifactTranscriptFinalTrimmed {
|
||||||
t.Fatalf("transcript fallback = %#v, want final-trimmed source", transcript)
|
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")
|
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" {
|
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)
|
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) {
|
func TestNeedsObjectStoreForRunPrepareWithPreviousRequirements(t *testing.T) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type Ref struct {
|
|||||||
ArchivePath string
|
ArchivePath string
|
||||||
RemoteKey string
|
RemoteKey string
|
||||||
Checksum string
|
Checksum string
|
||||||
|
ProducerRunID string
|
||||||
Contract *artifactmodel.ContractMetadata
|
Contract *artifactmodel.ContractMetadata
|
||||||
ExternalProvenance *artifactmodel.ExternalProvenance
|
ExternalProvenance *artifactmodel.ExternalProvenance
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
return nil, fmt.Errorf("analyze: scriptorium adapter is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
logs := []string{}
|
state := newAnalyzeExecutionState(execution, workPlan, reconciliation)
|
||||||
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{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range workPlan.ExecutionOrder {
|
for _, item := range workPlan.ExecutionOrder {
|
||||||
artifactCfg := env.Config.Pipeline.Scriptorium.Artifacts[item.Key]
|
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 {
|
if err != nil {
|
||||||
executionErr := fmt.Errorf("analyze: compute execution fingerprint for artifact %q: %w", item.Key, err)
|
executionErr := fmt.Errorf("analyze: compute execution fingerprint for artifact %q: %w", item.Key, err)
|
||||||
return failedAnalyzeResult(
|
return state.failedResult(item, artifactCfg, "", executionErr), executionErr
|
||||||
execution,
|
|
||||||
item,
|
|
||||||
artifactCfg,
|
|
||||||
"",
|
|
||||||
executionErr,
|
|
||||||
sessionRecords,
|
|
||||||
), executionErr
|
|
||||||
}
|
}
|
||||||
priorRecord, hadPriorRecord := priorCurrentRecords[item.Key]
|
|
||||||
plan := analyzeArtifactExecutionPlan{Name: item.Key, Cfg: artifactCfg}
|
plan := analyzeArtifactExecutionPlan{Name: item.Key, Cfg: artifactCfg}
|
||||||
artifactResult, err := executeAnalyzeArtifact(ctx, execution, plan)
|
artifactResult, err := executeAnalyzeArtifact(ctx, execution, plan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failedAnalyzeResult(
|
return state.failedResult(item, artifactCfg, fingerprint, err), err
|
||||||
execution,
|
|
||||||
item,
|
|
||||||
artifactCfg,
|
|
||||||
fingerprint,
|
|
||||||
err,
|
|
||||||
sessionRecords,
|
|
||||||
), err
|
|
||||||
}
|
}
|
||||||
|
if err := state.acceptArtifact(plan, fingerprint, artifactResult); err != nil {
|
||||||
logs = append(logs, artifactResult.Logs...)
|
return state.failedResult(item, artifactCfg, fingerprint, err), err
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return state.successResult(), nil
|
||||||
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),
|
|
||||||
}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func analyzeArtifactOrigin(execution analyzeExecutionContext, key string) (config.ArtifactFamilyMemberOrigin, bool) {
|
func analyzeArtifactOrigin(execution analyzeExecutionContext, key string) (config.ArtifactFamilyMemberOrigin, bool) {
|
||||||
@@ -464,7 +306,7 @@ func staleAnalyzeDependentClosure(
|
|||||||
if eligible != nil && !eligible(key) {
|
if eligible != nil && !eligible(key) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
staleProjectedAnalyzeRecord(records, key)
|
staleAnalyzeRecord(records, key)
|
||||||
queue = append(queue, reverse[key]...)
|
queue = append(queue, reverse[key]...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
236
internal/stage/analyze_execution_state.go
Normal file
236
internal/stage/analyze_execution_state.go
Normal 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
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAnalyzeAdapterFailureProjectsCompletedAndFailedArtifacts(t *testing.T) {
|
func TestAnalyzeAdapterFailureRecordsCompletedAndFailedArtifacts(t *testing.T) {
|
||||||
for _, failAt := range []int{1, 2, 3} {
|
for _, failAt := range []int{1, 2, 3} {
|
||||||
t.Run(map[int]string{1: "first", 2: "middle", 3: "last"}[failAt], func(t *testing.T) {
|
t.Run(map[int]string{1: "first", 2: "middle", 3: "last"}[failAt], func(t *testing.T) {
|
||||||
env, m, _ := setupAnalyzeEnv(t)
|
env, m, _ := setupAnalyzeEnv(t)
|
||||||
@@ -26,12 +26,13 @@ func TestAnalyzeAdapterFailureProjectsCompletedAndFailedArtifacts(t *testing.T)
|
|||||||
if !errors.Is(err, cause) {
|
if !errors.Is(err, cause) {
|
||||||
t.Fatalf("Run() error = %v, want injected cause", err)
|
t.Fatalf("Run() error = %v, want injected cause", err)
|
||||||
}
|
}
|
||||||
if result == nil || result.AnalyzeState == nil {
|
if result == nil || m.Stages["analyze"] == nil {
|
||||||
t.Fatal("failure result has no analyze projection")
|
t.Fatal("failure did not expose analyze manifest state")
|
||||||
}
|
}
|
||||||
|
records := m.Stages["analyze"].AnalyzeArtifacts
|
||||||
keys := []string{"alpha", "beta", "gamma"}
|
keys := []string{"alpha", "beta", "gamma"}
|
||||||
for index, key := range keys {
|
for index, key := range keys {
|
||||||
record, exists := result.AnalyzeState.Session[key]
|
record, exists := records[key]
|
||||||
switch {
|
switch {
|
||||||
case index < failAt-1:
|
case index < failAt-1:
|
||||||
if !exists || record.Status != manifest.AnalyzeArtifactCurrent || record.Output == nil {
|
if !exists || record.Status != manifest.AnalyzeArtifactCurrent || record.Output == nil {
|
||||||
@@ -54,11 +55,10 @@ func TestAnalyzeAdapterFailureProjectsCompletedAndFailedArtifacts(t *testing.T)
|
|||||||
func TestAnalyzeMiddleFailureStalesDependentsAndPreservesUnrelatedCurrent(t *testing.T) {
|
func TestAnalyzeMiddleFailureStalesDependentsAndPreservesUnrelatedCurrent(t *testing.T) {
|
||||||
env, m, _ := setupAnalyzeEnv(t)
|
env, m, _ := setupAnalyzeEnv(t)
|
||||||
env.Config.Pipeline.Scriptorium.Artifacts = chainedAnalyzeArtifacts()
|
env.Config.Pipeline.Scriptorium.Artifacts = chainedAnalyzeArtifacts()
|
||||||
first, err := (analyzeStage{}).Run(context.Background(), env, m)
|
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
installAnalyzeProjection(m, first.AnalyzeState)
|
|
||||||
unrelated := m.Stages["analyze"].AnalyzeArtifacts["unrelated"]
|
unrelated := m.Stages["analyze"].AnalyzeArtifacts["unrelated"]
|
||||||
stale := m.Stages["analyze"].AnalyzeArtifacts["alpha"]
|
stale := m.Stages["analyze"].AnalyzeArtifacts["alpha"]
|
||||||
stale.Status = manifest.AnalyzeArtifactStale
|
stale.Status = manifest.AnalyzeArtifactStale
|
||||||
@@ -68,36 +68,37 @@ func TestAnalyzeMiddleFailureStalesDependentsAndPreservesUnrelatedCurrent(t *tes
|
|||||||
env.SelectedArtifactKeys = []string{"gamma"}
|
env.SelectedArtifactKeys = []string{"gamma"}
|
||||||
cause := errors.New("middle artifact failed")
|
cause := errors.New("middle artifact failed")
|
||||||
env.Scriptorium = &indexedAnalyzeFailureRunner{FailAt: 2, Err: cause}
|
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 {
|
if old := m.Stages["analyze"].AnalyzeArtifacts["beta"]; old.Output == nil {
|
||||||
t.Fatal("fixture did not retain old canonical evidence before failure")
|
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")
|
t.Fatal("old canonical bytes made failed artifact current")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnalyzeInvalidOutputReturnsFailedProjection(t *testing.T) {
|
func TestAnalyzeInvalidOutputRecordsFailure(t *testing.T) {
|
||||||
env, m, _ := setupAnalyzeEnv(t)
|
env, m, _ := setupAnalyzeEnv(t)
|
||||||
paths := sessionPathsForEnv(env, m.SessionID)
|
paths := sessionPathsForEnv(env, m.SessionID)
|
||||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||||
@@ -105,17 +106,17 @@ func TestAnalyzeInvalidOutputReturnsFailedProjection(t *testing.T) {
|
|||||||
m.RunID = "run-invalid-output"
|
m.RunID = "run-invalid-output"
|
||||||
env.Scriptorium = invalidAnalyzeOutputRunner{}
|
env.Scriptorium = invalidAnalyzeOutputRunner{}
|
||||||
|
|
||||||
result, err := (analyzeStage{}).Run(context.Background(), env, m)
|
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Run() error = 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 {
|
if record.Status != manifest.AnalyzeArtifactFailed || record.Output != nil {
|
||||||
t.Fatalf("failed record = %#v", record)
|
t.Fatalf("failed record = %#v", record)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnalyzeMaterializationFailureReturnsFailedProjection(t *testing.T) {
|
func TestAnalyzeMaterializationFailureRecordsFailure(t *testing.T) {
|
||||||
env, m, _ := setupAnalyzeEnv(t)
|
env, m, _ := setupAnalyzeEnv(t)
|
||||||
paths := sessionPathsForEnv(env, m.SessionID)
|
paths := sessionPathsForEnv(env, m.SessionID)
|
||||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
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")
|
canonical := filepath.Join(paths.ArtifactsDir, "session_recap.md")
|
||||||
env.ArtifactStore = &failingAnalyzeArtifactStore{Store: env.ArtifactStore, FailPath: canonical, Err: cause}
|
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) {
|
if !errors.Is(err, cause) {
|
||||||
t.Fatalf("Run() error = %v, want write cause", err)
|
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 {
|
if record.Status != manifest.AnalyzeArtifactFailed || record.Output != nil {
|
||||||
t.Fatalf("failed record = %#v", record)
|
t.Fatalf("failed record = %#v", record)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func TestAnalyzeReusesCurrentArtifactWithoutScriptorium(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
installAnalyzeProjection(m, first.AnalyzeState)
|
assertAnalyzeProjectionMatchesManifest(t, m, first.AnalyzeState)
|
||||||
if len(fake.RunRequests) != 1 {
|
if len(fake.RunRequests) != 1 {
|
||||||
t.Fatalf("initial requests = %d, want 1", len(fake.RunRequests))
|
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"}) {
|
if got := second.Metadata["reused_current"]; !reflect.DeepEqual(got, []string{"session_recap"}) {
|
||||||
t.Fatalf("reused current = %#v, want session_recap", got)
|
t.Fatalf("reused current = %#v, want session_recap", got)
|
||||||
}
|
}
|
||||||
if second.AnalyzeState.Session["session_recap"].Status != manifest.AnalyzeArtifactCurrent {
|
if m.Stages["analyze"].AnalyzeArtifacts["session_recap"].Status != manifest.AnalyzeArtifactCurrent {
|
||||||
t.Fatalf("session state = %#v", second.AnalyzeState.Session)
|
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":[]}`)
|
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||||
addAnalyzeDependentArtifact(env)
|
addAnalyzeDependentArtifact(env)
|
||||||
|
|
||||||
first, err := (analyzeStage{}).Run(context.Background(), env, m)
|
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
installAnalyzeProjection(m, first.AnalyzeState)
|
|
||||||
if len(fake.RunRequests) != 2 {
|
if len(fake.RunRequests) != 2 {
|
||||||
t.Fatalf("initial requests = %d, want 2", len(fake.RunRequests))
|
t.Fatalf("initial requests = %d, want 2", len(fake.RunRequests))
|
||||||
}
|
}
|
||||||
@@ -60,17 +59,17 @@ func TestAnalyzePartialForcePreservesUnrelatedCurrentRecord(t *testing.T) {
|
|||||||
env.Force = true
|
env.Force = true
|
||||||
secondRunner := &orderedScriptoriumRunner{RunBody: "scriptorium noop/fake run artifact\n"}
|
secondRunner := &orderedScriptoriumRunner{RunBody: "scriptorium noop/fake run artifact\n"}
|
||||||
env.Scriptorium = secondRunner
|
env.Scriptorium = secondRunner
|
||||||
second, err := (analyzeStage{}).Run(context.Background(), env, m)
|
_, err = (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if got := secondRunner.Calls; !reflect.DeepEqual(got, []string{"run"}) {
|
if got := secondRunner.Calls; !reflect.DeepEqual(got, []string{"run"}) {
|
||||||
t.Fatalf("calls = %#v, want one forced target run", got)
|
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)
|
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")
|
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":[]}`)
|
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||||
addAnalyzeDependentArtifact(env)
|
addAnalyzeDependentArtifact(env)
|
||||||
|
|
||||||
first, err := (analyzeStage{}).Run(context.Background(), env, m)
|
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
installAnalyzeProjection(m, first.AnalyzeState)
|
|
||||||
dependent := env.Config.Pipeline.Scriptorium.Artifacts["player_handout"]
|
dependent := env.Config.Pipeline.Scriptorium.Artifacts["player_handout"]
|
||||||
dependent.PromptID = "dnd.player_handout.revised"
|
dependent.PromptID = "dnd.player_handout.revised"
|
||||||
env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = dependent
|
env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = dependent
|
||||||
env.SelectedArtifactKeys = []string{"player_handout"}
|
env.SelectedArtifactKeys = []string{"player_handout"}
|
||||||
fake.RunRequests = nil
|
fake.RunRequests = nil
|
||||||
|
|
||||||
second, err := (analyzeStage{}).Run(context.Background(), env, m)
|
_, err = (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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") {
|
if got := fake.RunRequests[0].InputPaths["recap"]; got != filepath.Join(paths.ArtifactsDir, "session_recap.md") {
|
||||||
t.Fatalf("reused prerequisite input = %q", got)
|
t.Fatalf("reused prerequisite input = %q", got)
|
||||||
}
|
}
|
||||||
if second.AnalyzeState.Session["session_recap"].Status != manifest.AnalyzeArtifactCurrent ||
|
if m.Stages["analyze"].AnalyzeArtifacts["session_recap"].Status != manifest.AnalyzeArtifactCurrent ||
|
||||||
second.AnalyzeState.Session["player_handout"].Status != manifest.AnalyzeArtifactCurrent {
|
m.Stages["analyze"].AnalyzeArtifacts["player_handout"].Status != manifest.AnalyzeArtifactCurrent {
|
||||||
t.Fatalf("session records = %#v, want reused prerequisite and produced dependent", second.AnalyzeState.Session)
|
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":[]}`)
|
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||||
addAnalyzeDependentArtifact(env)
|
addAnalyzeDependentArtifact(env)
|
||||||
|
|
||||||
first, err := (analyzeStage{}).Run(context.Background(), env, m)
|
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
installAnalyzeProjection(m, first.AnalyzeState)
|
|
||||||
|
|
||||||
env.SelectedArtifactKeys = []string{"session_recap"}
|
env.SelectedArtifactKeys = []string{"session_recap"}
|
||||||
env.Force = true
|
env.Force = true
|
||||||
env.Scriptorium = &orderedScriptoriumRunner{RunBody: "changed recap\n"}
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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 {
|
if dependent.Status != manifest.AnalyzeArtifactStale || dependent.Output != nil || dependent.OutputSize != 0 {
|
||||||
t.Fatalf("dependent record = %#v, want stale without output", dependent)
|
t.Fatalf("dependent record = %#v, want stale without output", dependent)
|
||||||
}
|
}
|
||||||
if second.AnalyzeState.Session["player_handout"].Status != manifest.AnalyzeArtifactStale {
|
if m.Stages["analyze"].AnalyzeArtifacts["player_handout"].Status != manifest.AnalyzeArtifactStale {
|
||||||
t.Fatalf("dependent state = %#v, want stale", second.AnalyzeState.Session["player_handout"])
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
installAnalyzeProjection(m, first.AnalyzeState)
|
|
||||||
if len(fake.RunRequests) != 2 {
|
if len(fake.RunRequests) != 2 {
|
||||||
t.Fatalf("initial enabled requests = %d, want 2", len(fake.RunRequests))
|
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"}
|
env.SelectedArtifactKeys = []string{"quest_log"}
|
||||||
fake.RunRequests = nil
|
fake.RunRequests = nil
|
||||||
second, err := (analyzeStage{}).Run(context.Background(), env, m)
|
_, err = (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -178,8 +174,8 @@ func TestAnalyzeRebuildsNestedStalePrerequisitesInDependencyOrder(t *testing.T)
|
|||||||
t.Fatalf("prompt order = %#v, want %#v", gotPrompts, wantPrompts)
|
t.Fatalf("prompt order = %#v, want %#v", gotPrompts, wantPrompts)
|
||||||
}
|
}
|
||||||
for _, key := range []string{"session_recap", "player_handout", "quest_log"} {
|
for _, key := range []string{"session_recap", "player_handout", "quest_log"} {
|
||||||
if second.AnalyzeState.Session[key].Status != manifest.AnalyzeArtifactCurrent {
|
if m.Stages["analyze"].AnalyzeArtifacts[key].Status != manifest.AnalyzeArtifactCurrent {
|
||||||
t.Fatalf("%s state = %#v, want current", key, second.AnalyzeState.Session[key])
|
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")}},
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -218,11 +214,11 @@ func TestAnalyzeLegacyExecutionPromotesOnlyEffectiveArtifacts(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(gotPrompts, test.wantPrompts) {
|
if !reflect.DeepEqual(gotPrompts, test.wantPrompts) {
|
||||||
t.Fatalf("prompts = %#v, want %#v", gotPrompts, test.wantPrompts)
|
t.Fatalf("prompts = %#v, want %#v", gotPrompts, test.wantPrompts)
|
||||||
}
|
}
|
||||||
if len(result.AnalyzeState.Session) != len(test.wantPrompts) {
|
if len(m.Stages["analyze"].AnalyzeArtifacts) != len(test.wantPrompts) {
|
||||||
t.Fatalf("session records = %#v, want only regenerated effective artifacts", result.AnalyzeState.Session)
|
t.Fatalf("session records = %#v, want only regenerated effective artifacts", m.Stages["analyze"].AnalyzeArtifacts)
|
||||||
}
|
}
|
||||||
if len(test.selected) > 0 {
|
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")
|
t.Fatal("legacy unselected canonical output was promoted")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,10 +236,14 @@ func addAnalyzeDependentArtifact(env *Env) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func installAnalyzeProjection(m *manifest.Manifest, projection *AnalyzeStateProjection) {
|
func assertAnalyzeProjectionMatchesManifest(
|
||||||
if m.Stages["analyze"] == nil {
|
t *testing.T,
|
||||||
m.Stages["analyze"] = &manifest.StageRecord{Name: "analyze"}
|
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ type analyzeWorkPlan struct {
|
|||||||
ReusedCurrent []analyzePlanItem
|
ReusedCurrent []analyzePlanItem
|
||||||
Invalidated []analyzePlanItem
|
Invalidated []analyzePlanItem
|
||||||
Removed []analyzePlanItem
|
Removed []analyzePlanItem
|
||||||
ProjectedRecords map[string]manifest.AnalyzeArtifactRecord
|
StartingRecords map[string]manifest.AnalyzeArtifactRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
// planAnalyzeWork turns selection and reconciliation into one deterministic,
|
// 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 })
|
sort.Slice(removed, func(i, j int) bool { return removed[i].Key < removed[j].Key })
|
||||||
|
|
||||||
plan := analyzeWorkPlan{
|
plan := analyzeWorkPlan{
|
||||||
ExplicitTargets: append([]string(nil), targets...),
|
ExplicitTargets: append([]string(nil), targets...),
|
||||||
Removed: removed,
|
Removed: removed,
|
||||||
ProjectedRecords: projectedAnalyzeRecords(scriptoriumCfg.Artifacts, reconciliation),
|
StartingRecords: reconciledAnalyzeRecords(scriptoriumCfg.Artifacts, reconciliation),
|
||||||
}
|
}
|
||||||
invalidated := make(map[string]struct{})
|
invalidated := make(map[string]struct{})
|
||||||
for _, state := range reconciliation.Ordered {
|
for _, state := range reconciliation.Ordered {
|
||||||
@@ -142,7 +142,7 @@ func planAnalyzeWork(
|
|||||||
plan.Invalidated = append(plan.Invalidated, item)
|
plan.Invalidated = append(plan.Invalidated, item)
|
||||||
invalidated[key] = struct{}{}
|
invalidated[key] = struct{}{}
|
||||||
}
|
}
|
||||||
staleProjectedAnalyzeRecord(plan.ProjectedRecords, key)
|
staleAnalyzeRecord(plan.StartingRecords, key)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -183,11 +183,11 @@ func analyzeTargetDependencyClosure(
|
|||||||
return closure, nil
|
return closure, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func projectedAnalyzeRecords(
|
func reconciledAnalyzeRecords(
|
||||||
configured map[string]config.ScriptoriumArtifactConfig,
|
configured map[string]config.ScriptoriumArtifactConfig,
|
||||||
reconciliation analyzeReconciliation,
|
reconciliation analyzeReconciliation,
|
||||||
) map[string]manifest.AnalyzeArtifactRecord {
|
) map[string]manifest.AnalyzeArtifactRecord {
|
||||||
projected := make(map[string]manifest.AnalyzeArtifactRecord)
|
records := make(map[string]manifest.AnalyzeArtifactRecord)
|
||||||
for _, item := range reconciliation.Ordered {
|
for _, item := range reconciliation.Ordered {
|
||||||
if item.Stored == nil || item.Reason == analyzeReconciliationRemoved {
|
if item.Stored == nil || item.Reason == analyzeReconciliationRemoved {
|
||||||
continue
|
continue
|
||||||
@@ -202,15 +202,15 @@ func projectedAnalyzeRecords(
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
projected[item.Key] = record
|
records[item.Key] = record
|
||||||
if item.Reason != analyzeReconciliationCurrent && record.Status == manifest.AnalyzeArtifactCurrent {
|
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]
|
record, ok := records[key]
|
||||||
if !ok || record.Status != manifest.AnalyzeArtifactCurrent {
|
if !ok || record.Status != manifest.AnalyzeArtifactCurrent {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ func TestPlanAnalyzeWorkRejectsUnknownSelectionAndInvalidGraphs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPlanAnalyzeWorkProjectionPreservesOnlyValidConfiguredAuthority(t *testing.T) {
|
func TestPlanAnalyzeWorkStartingRecordsPreserveOnlyValidConfiguredAuthority(t *testing.T) {
|
||||||
configured := map[string]config.ScriptoriumArtifactConfig{
|
configured := map[string]config.ScriptoriumArtifactConfig{
|
||||||
"target": {Enabled: true}, "unrelated": {Enabled: true}, "invalid_unselected": {Enabled: true},
|
"target": {Enabled: true}, "unrelated": {Enabled: true}, "invalid_unselected": {Enabled: true},
|
||||||
}
|
}
|
||||||
@@ -228,14 +228,14 @@ func TestPlanAnalyzeWorkProjectionPreservesOnlyValidConfiguredAuthority(t *testi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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)
|
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)
|
t.Fatalf("invalid unselected status = %q, want stale", got)
|
||||||
}
|
}
|
||||||
if _, exists := plan.ProjectedRecords["removed"]; exists {
|
if _, exists := plan.StartingRecords["removed"]; exists {
|
||||||
t.Fatal("removed record survived projected authority")
|
t.Fatal("removed record survived reconciled authority")
|
||||||
}
|
}
|
||||||
assertAnalyzePlanKeys(t, "invalidated", plan.Invalidated, []string{"invalid_unselected"})
|
assertAnalyzePlanKeys(t, "invalidated", plan.Invalidated, []string{"invalid_unselected"})
|
||||||
assertAnalyzePlanKeys(t, "removed", plan.Removed, []string{"removed"})
|
assertAnalyzePlanKeys(t, "removed", plan.Removed, []string{"removed"})
|
||||||
@@ -256,8 +256,8 @@ func TestPlanAnalyzeWorkNonResumablePartialSelectionDoesNotPromoteUnselectedOutp
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
assertAnalyzePlanKeys(t, "execution", plan.ExecutionOrder, []string{"target"})
|
assertAnalyzePlanKeys(t, "execution", plan.ExecutionOrder, []string{"target"})
|
||||||
if len(plan.ProjectedRecords) != 0 {
|
if len(plan.StartingRecords) != 0 {
|
||||||
t.Fatalf("non-resumable projection = %#v, want no current records", plan.ProjectedRecords)
|
t.Fatalf("non-resumable starting records = %#v, want no current records", plan.StartingRecords)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,11 +165,10 @@ func TestAnalyzeResumeSemanticChangesRemainArtifactGranular(t *testing.T) {
|
|||||||
"transcript": {Source: "narratio.transcript.final_trimmed", Required: true},
|
"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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
installAnalyzeProjection(m, result.AnalyzeState)
|
|
||||||
m.Stages["analyze"].Status = manifest.StatusSucceeded
|
m.Stages["analyze"].Status = manifest.StatusSucceeded
|
||||||
|
|
||||||
test.mutate(env.Config.Pipeline.Scriptorium)
|
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 {
|
if got, _ := result.Metadata["executed_artifacts"].([]string); len(got) != 0 {
|
||||||
t.Fatalf("executed artifacts = %#v, want none", got)
|
t.Fatalf("executed artifacts = %#v, want none", got)
|
||||||
}
|
}
|
||||||
if result.AnalyzeState.Session["session_recap"].Status != manifest.AnalyzeArtifactCurrent {
|
if m.Stages["analyze"].AnalyzeArtifacts["session_recap"].Status != manifest.AnalyzeArtifactCurrent {
|
||||||
t.Fatalf("session projection = %#v, want current recap", result.AnalyzeState.Session)
|
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 {
|
if dependent {
|
||||||
addAnalyzeDependentArtifact(env)
|
addAnalyzeDependentArtifact(env)
|
||||||
}
|
}
|
||||||
result, err := (analyzeStage{}).Run(context.Background(), env, m)
|
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
installAnalyzeProjection(m, result.AnalyzeState)
|
|
||||||
m.Stages["analyze"].Status = manifest.StatusSucceeded
|
m.Stages["analyze"].Status = manifest.StatusSucceeded
|
||||||
return env, m, len(fake.RunRequests)
|
return env, m, len(fake.RunRequests)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) {
|
|||||||
t.Fatalf("session_id var = %q, want sticky narratio session id", req.Vars["session_id"])
|
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 {
|
if m.Stages["analyze"].AnalyzeArtifacts["session_recap"].Status != manifest.AnalyzeArtifactCurrent {
|
||||||
t.Fatalf("analyze state = %#v, want current session_recap", result.AnalyzeState)
|
t.Fatalf("analyze state = %#v, want current session_recap", m.Stages["analyze"].AnalyzeArtifacts)
|
||||||
}
|
}
|
||||||
if len(result.Logs) != 2 {
|
if len(result.Logs) != 2 {
|
||||||
t.Fatalf("logs = %#v, want stdout+stderr logs", result.Logs)
|
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":[]}`)
|
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"))
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Run() error = %v", err)
|
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")) {
|
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)
|
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 {
|
if m.Stages["analyze"].AnalyzeArtifacts["session_recap"].Output == nil {
|
||||||
t.Fatalf("analyze state = %#v, want session recap output evidence", result.AnalyzeState)
|
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 {
|
if err := requireNonEmptyFile(filepath.Join(paths.ArtifactsDir, "session_recap.md"), "materialized output"); err != nil {
|
||||||
t.Fatalf("canonical output = %v", err)
|
t.Fatalf("canonical output = %v", err)
|
||||||
@@ -676,7 +676,7 @@ func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
env.SelectedArtifactKeys = []string{"player_handout"}
|
env.SelectedArtifactKeys = []string{"player_handout"}
|
||||||
|
|
||||||
result, err := (analyzeStage{}).Run(context.Background(), env, m)
|
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Run() error = %v", err)
|
t.Fatalf("Run() error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -686,8 +686,8 @@ func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) {
|
|||||||
if fake.RunRequests[0].PromptID != "dnd.player_handout" {
|
if fake.RunRequests[0].PromptID != "dnd.player_handout" {
|
||||||
t.Fatalf("prompt id = %q, want dnd.player_handout", fake.RunRequests[0].PromptID)
|
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 {
|
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", result.AnalyzeState)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Run() error = %v", err)
|
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" {
|
if !ok || record.Output == nil || record.Output.LocalPath != "artifacts/session_recap.md" {
|
||||||
t.Fatalf("analyze output record = %#v, want session recap evidence", record)
|
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) {
|
func TestAnalyzeHandlesAdapterError(t *testing.T) {
|
||||||
env, m, fake := setupAnalyzeEnv(t)
|
env, m, fake := setupAnalyzeEnv(t)
|
||||||
paths := sessionPathsForEnv(env, m.SessionID)
|
paths := sessionPathsForEnv(env, m.SessionID)
|
||||||
|
|||||||
Reference in New Issue
Block a user