*/ protected function casts(): array { return [ 'lifecycle_state' => BaselineSnapshotLifecycleState::class, 'summary_jsonb' => 'array', 'completion_meta_jsonb' => 'array', 'captured_at' => 'datetime', 'completed_at' => 'datetime', 'failed_at' => 'datetime', ]; } public function workspace(): BelongsTo { return $this->belongsTo(Workspace::class); } public function baselineProfile(): BelongsTo { return $this->belongsTo(BaselineProfile::class); } public function items(): HasMany { return $this->hasMany(BaselineSnapshotItem::class); } public function scopeConsumable(Builder $query): Builder { return $query->where('lifecycle_state', BaselineSnapshotLifecycleState::Complete->value); } public function scopeLatestConsumable(Builder $query): Builder { return $query ->consumable() ->orderByDesc('completed_at') ->orderByDesc('captured_at') ->orderByDesc('id'); } public function isConsumable(): bool { return $this->lifecycleState() === BaselineSnapshotLifecycleState::Complete; } public function isBuilding(): bool { return $this->lifecycleState() === BaselineSnapshotLifecycleState::Building; } public function isComplete(): bool { return $this->lifecycleState() === BaselineSnapshotLifecycleState::Complete; } public function isIncomplete(): bool { return $this->lifecycleState() === BaselineSnapshotLifecycleState::Incomplete; } public function markBuilding(array $completionMeta = []): void { $this->forceFill([ 'lifecycle_state' => BaselineSnapshotLifecycleState::Building, 'completed_at' => null, 'failed_at' => null, 'completion_meta_jsonb' => $this->mergedCompletionMeta($completionMeta), ])->save(); } public function markComplete(string $identityHash, array $completionMeta = []): void { if ($this->isIncomplete()) { throw new RuntimeException('Incomplete baseline snapshots cannot transition back to complete.'); } $this->forceFill([ 'snapshot_identity_hash' => $identityHash, 'lifecycle_state' => BaselineSnapshotLifecycleState::Complete, 'completed_at' => now(), 'failed_at' => null, 'completion_meta_jsonb' => $this->mergedCompletionMeta($completionMeta), ])->save(); } public function markIncomplete(?string $reasonCode = null, array $completionMeta = []): void { $this->forceFill([ 'lifecycle_state' => BaselineSnapshotLifecycleState::Incomplete, 'completed_at' => null, 'failed_at' => now(), 'completion_meta_jsonb' => $this->mergedCompletionMeta(array_filter([ 'finalization_reason_code' => $reasonCode ?? BaselineReasonCodes::SNAPSHOT_INCOMPLETE, ...$completionMeta, ], static fn (mixed $value): bool => $value !== null)), ])->save(); } public function lifecycleState(): BaselineSnapshotLifecycleState { if ($this->lifecycle_state instanceof BaselineSnapshotLifecycleState) { return $this->lifecycle_state; } if (is_string($this->lifecycle_state) && BaselineSnapshotLifecycleState::tryFrom($this->lifecycle_state) instanceof BaselineSnapshotLifecycleState) { return BaselineSnapshotLifecycleState::from($this->lifecycle_state); } return BaselineSnapshotLifecycleState::Incomplete; } /** * @param array $completionMeta * @return array */ private function mergedCompletionMeta(array $completionMeta): array { $existing = is_array($this->completion_meta_jsonb) ? $this->completion_meta_jsonb : []; return array_replace($existing, $completionMeta); } }