package manifest import ( "strings" "time" ) // ErrorRecord captures structured error metadata at run or stage scope. type ErrorRecord struct { Message string `json:"message"` Code string `json:"code,omitempty"` At *time.Time `json:"at,omitempty"` } // InputRecord captures one resolved input and optional checksum. type InputRecord struct { Kind string `json:"kind"` Path string `json:"path"` Checksum string `json:"checksum,omitempty"` Source string `json:"source,omitempty"` S3Bucket string `json:"s3_bucket,omitempty"` S3Key string `json:"s3_key,omitempty"` S3Size int64 `json:"s3_size,omitempty"` S3ETag string `json:"s3_etag,omitempty"` SpoolPath string `json:"spool_path,omitempty"` } // ArtifactRecord captures one produced artifact and optional remote metadata. type ArtifactRecord struct { Kind string `json:"kind"` LocalPath string `json:"local_path"` // ProducerRunID identifies the run that produced this durable artifact. ProducerRunID string `json:"producer_run_id,omitempty"` RemoteKey string `json:"remote_key,omitempty"` Checksum string `json:"checksum,omitempty"` } // StageRecord tracks lifecycle and provenance for one pipeline stage. type StageRecord struct { Name string `json:"name"` Status StageStatus `json:"status"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` StartedAt *time.Time `json:"started_at,omitempty"` CompletedAt *time.Time `json:"completed_at,omitempty"` Outputs []ArtifactRecord `json:"outputs,omitempty"` Logs []string `json:"logs,omitempty"` GeneratedConfigs []string `json:"generated_configs,omitempty"` Error *ErrorRecord `json:"error,omitempty"` Metadata map[string]any `json:"metadata,omitempty"` } // Manifest is the durable run-state record for a session execution. type Manifest struct { SessionID string `json:"session_id"` Campaign string `json:"campaign,omitempty"` RunID string `json:"run_id,omitempty"` LocalWorkDir string `json:"local_workdir,omitempty"` LocalSpoolDir string `json:"local_spool_dir,omitempty"` S3Bucket string `json:"s3_bucket,omitempty"` S3SessionPrefix string `json:"s3_session_prefix,omitempty"` S3RunPrefix string `json:"s3_run_prefix,omitempty"` PipelineVersion string `json:"pipeline_version,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` LastError *ErrorRecord `json:"last_error,omitempty"` Inputs []InputRecord `json:"inputs,omitempty"` Artifacts []ArtifactRecord `json:"artifacts,omitempty"` Stages map[string]*StageRecord `json:"stages"` } // New constructs a new manifest with deterministic timestamps. func New(sessionID string, now time.Time) *Manifest { return &Manifest{ SessionID: strings.TrimSpace(sessionID), CreatedAt: now, UpdatedAt: now, Stages: map[string]*StageRecord{}, } } // MarkStageRunning marks a stage as running and updates timestamps. func (m *Manifest) MarkStageRunning(name string, at time.Time) { s := m.ensureStage(name, at) s.Status = StatusRunning s.StartedAt = timePtr(at) s.CompletedAt = nil s.Error = nil s.UpdatedAt = at m.UpdatedAt = at } // MarkStageSucceeded marks a stage as succeeded, stores outputs, and updates timestamps. func (m *Manifest) MarkStageSucceeded(name string, at time.Time, outputs []ArtifactRecord) { s := m.ensureStage(name, at) s.Status = StatusSucceeded s.CompletedAt = timePtr(at) s.Error = nil s.Outputs = append([]ArtifactRecord(nil), outputs...) s.UpdatedAt = at m.UpdatedAt = at } // MarkStageFailed marks a stage as failed and records error metadata. func (m *Manifest) MarkStageFailed(name string, at time.Time, message string) { s := m.ensureStage(name, at) s.Status = StatusFailed s.CompletedAt = timePtr(at) s.Error = &ErrorRecord{Message: strings.TrimSpace(message), At: timePtr(at)} s.UpdatedAt = at m.LastError = &ErrorRecord{Message: strings.TrimSpace(message), At: timePtr(at)} m.UpdatedAt = at } // MarkStageSkipped marks a stage as skipped and records the skip reason. func (m *Manifest) MarkStageSkipped(name string, at time.Time, reason string) { s := m.ensureStage(name, at) s.Status = StatusSkipped s.CompletedAt = timePtr(at) s.Error = &ErrorRecord{Message: strings.TrimSpace(reason), Code: "skipped", At: timePtr(at)} s.UpdatedAt = at m.UpdatedAt = at } // MarkStageStale marks a stage as stale so it is not skipped as idempotently complete. func (m *Manifest) MarkStageStale(name string, at time.Time, reason string) { s := m.ensureStage(name, at) s.Status = StatusStale s.Error = &ErrorRecord{Message: strings.TrimSpace(reason), Code: "stale", At: timePtr(at)} s.UpdatedAt = at m.UpdatedAt = at } func (m *Manifest) ensureStage(name string, at time.Time) *StageRecord { if m.Stages == nil { m.Stages = map[string]*StageRecord{} } stageName := strings.TrimSpace(name) s, ok := m.Stages[stageName] if !ok || s == nil { s = &StageRecord{ Name: stageName, Status: StatusPending, CreatedAt: at, UpdatedAt: at, } m.Stages[stageName] = s } if s.Name == "" { s.Name = stageName } if s.CreatedAt.IsZero() { s.CreatedAt = at } return s } func timePtr(t time.Time) *time.Time { v := t return &v }