# `Scrypath.Operator.FailedWork`
[🔗](https://github.com/szTheory/scrypath/blob/v0.3.5/lib/scrypath/operator/failed_work.ex#L1)

Failed sync work returned by `Scrypath.failed_sync_work/2`.

Each entry reports a stable Scrypath-owned identifier, operation kind,
retryability, and summarized reason. Recovery details are exposed through an
explicit `Scrypath.Operator.RecoveryAction` when durable replay is possible.

## Enriched fields (additive, v1.3)

- `attempt` / `max_attempts` — set from the Oban job when `source == :oban`;
  `nil` for Meilisearch task rows and any path without queue attempt metadata.
- `reason_class` — bounded operator-facing classification: `:transport`,
  `:validation`, `:backend_rejected`, `:queue_exhausted`, or `:unknown`
  (default when signals are missing or ambiguous).
- `last_attempt_at` — mirrors `failed_at` for every constructor path (soft
  alias for “when this failure was last observed”).

## Telemetry

Each constructed row emits **once**:

    :telemetry.execute(
      [:scrypath, :operator, :failed_work, :observed],
      %{count: 1},
      metadata
    )

**Required** metadata keys: `:reason_class`, `:schema`, `:mode`.

**Optional** metadata keys (v1.3): `:operation`, `:retryable?` — same meanings as
the struct fields.

Treat **`schema` module atoms and other rich metadata as unsafe for
low-cardinality metric labels** (for example Prometheus or OTel attribute rules)
unless you aggregate or sample; they are appropriate for logs, traces, and
structured handlers that filter explicitly.

## Rollups

`reason_class_counts/1` summarizes a row list into per-class pileup counts. If
you filter rows for a view, compute counts from that same filtered list;
`total` then matches the filtered length, not an unfiltered source length.

# `operation`

```elixir
@type operation() :: :upsert | :delete | :unknown
```

# `reason_class`

```elixir
@type reason_class() ::
  :transport | :validation | :backend_rejected | :queue_exhausted | :unknown
```

# `state`

```elixir
@type state() :: :failed | :retrying
```

# `t`

```elixir
@type t() :: %Scrypath.Operator.FailedWork{
  attempt: non_neg_integer() | nil,
  failed_at: DateTime.t() | nil,
  id: term(),
  last_attempt_at: DateTime.t() | nil,
  max_attempts: non_neg_integer() | nil,
  metadata: map(),
  mode: :inline | :manual | :oban | atom(),
  operation: operation(),
  reason: String.t() | nil,
  reason_class: reason_class() | nil,
  recovery: Scrypath.Operator.RecoveryAction.t() | nil,
  retryable?: boolean(),
  schema: module(),
  source: :meilisearch | :oban | atom(),
  state: state()
}
```

# `list`

```elixir
@spec list(module(), keyword(), keyword()) :: {:ok, [t()]} | {:error, term()}
```

# `reason_class_counts`

```elixir
@spec reason_class_counts([t()]) :: Scrypath.Operator.ReasonClassCounts.t()
```

# `recovery_action`

```elixir
@spec recovery_action(t()) :: Scrypath.Operator.RecoveryAction.t() | nil
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
