Raxol. Harness. Projection. Recovery
(Raxol v2.6.1)
View Source
Cross-cutting recovery mechanics shared by Raxol.Harness.Projection:
the global id-monotonic filter (N-ADV-02/03/forward-gap), family
partitioning (loop vs meta vs untyped, N-DORM-04), turn bucketing in
first-seen order (N-ADV-05), and the single diagnostic-emission seam
every recovered condition goes through.
Every recovery emits [:raxol, :harness, :projection, :recovered]
telemetry with %{reason, event_id} -- the same event
Raxol.UI.Components.Harness.Block uses for its own internal
rescues. Recovery here is never silent: see Raxol.Harness.Projection's
moduledoc for the recovery policy table and the two identity keys this
feeds.
Summary
Functions
Groups events by turn_id, preserving FIRST-SEEN turn order (not
raw id/arrival order) so interleaved turns render grouped, never
cross-bled (N-ADV-05). Each turn's own event list keeps its original
relative order.
Emits one recovery diagnostic: fires the telemetry event and returns
the diagnostic map so callers can also accumulate it in
t.diagnostics.
Global id-monotonic recovery pass, applied across BOTH :loop and
:meta events (a single session-wide journal offset, protocol §3)
Whether this turn's event list never saw a turn_started event.
Splits already id-recovered events into {loop_events, meta_events, diagnostics}. :meta events are legal interleaving (no diagnostic,
P-DET-06). Anything with neither family: :loop nor family: :meta
(missing, nil, or unrecognised) is the FI-12-mirrored untyped
record: excluded from both, diagnosed (N-DORM-04).
The block.content keys that carry recovery provenance metadata.
Types
Functions
Groups events by turn_id, preserving FIRST-SEEN turn order (not
raw id/arrival order) so interleaved turns render grouped, never
cross-bled (N-ADV-05). Each turn's own event list keeps its original
relative order.
@spec emit(atom(), term()) :: diagnostic()
Emits one recovery diagnostic: fires the telemetry event and returns
the diagnostic map so callers can also accumulate it in
t.diagnostics.
@spec filter_ids([map()]) :: {[map()], [diagnostic()], boolean()}
Global id-monotonic recovery pass, applied across BOTH :loop and
:meta events (a single session-wide journal offset, protocol §3):
- a duplicate
id(already accepted) is dropped -- idempotent, second application is a no-op (N-ADV-03). - an out-of-order
id(lower than the highest accepted so far, and not itself a duplicate) is dropped loud (N-ADV-02). - a forward gap -- an id strictly more than one past the highest
accepted so far (e.g. ids 1, 2, 5 with 3 and 4 never arriving) --
is accepted (soft-render: the survivor blocks are not withheld)
but diagnosed loud AND marks the returned
damaged?flagtrue(hard-mark). MirrorsRaxol.Agent.Journal's own interior-loss contract (status/1->:damaged): the journal fails closed upstream on real corruption, so a gap reaching this filter means dense-id events were dropped somewhere between journal and here. This projection's job is diagnose + mark, not withhold, so a downstream consumer (T18) can refuse a gapped tail as canonical without losing the events that DID survive.
The very first id accepted in a given filter_ids/1 call is exempt
from both the out-of-order and forward-gap checks -- there is no
"highest accepted so far" yet, and a call may legitimately start
mid-stream (a suffix replay from a durable block boundary, offset-based
reattach, etc.) where the first id is not 0/1.
Events without an integer id (the untyped-record generator case)
bypass this check entirely -- they have no ordering semantics to
violate -- and are handled by partition_families/1 instead.
Whether this turn's event list never saw a turn_started event.
@spec partition_families([map()]) :: {[map()], [map()], [diagnostic()]}
Splits already id-recovered events into {loop_events, meta_events, diagnostics}. :meta events are legal interleaving (no diagnostic,
P-DET-06). Anything with neither family: :loop nor family: :meta
(missing, nil, or unrecognised) is the FI-12-mirrored untyped
record: excluded from both, diagnosed (N-DORM-04).
A family: :loop record whose :type is not an atom (e.g. a
string "item_started" surviving from a malformed/legacy producer)
would otherwise reach BlockBuilder.fold_event/3's catch-all clause
and be dropped with NO diagnostic -- the same silent-discard failure
mode this module exists to prevent. Guarded here instead: a non-atom
:type on an otherwise-loop record is treated as untyped too.
@spec recovery_meta_keys() :: [atom()]
The block.content keys that carry recovery provenance metadata.