Raxol. Harness. RecencyPolicy
(Raxol v2.6.1)
View Source
Turn recency -> per-block prominence: the policy that decides WHICH
prominence (0.0..1.0) a transcript block gets, based purely on how
many turns behind the current one its own turn is. This module is pure
turn-identity arithmetic -- no processes, no clocks, no config reads,
no rendering. It feeds context[:prominence] into
Raxol.UI.Components.Harness.Block.render/2
(Raxol.UI.Harness.Prominence.resolve/3 underneath), but never calls
either.
The ladder
turns_behind steps down a fixed four-tier ladder, floored at
floor/0 (0.4):
turns_behind: 0 1 2 3+
prominence: 1.0 0.8 0.6 0.4nil or negative turns_behind -- an unknown or (defensively)
future-relative position -- always resolves to 1.0, never something
darker: an ungraded block is never demoted below full prominence by
this policy's own uncertainty.
Why these values (the ladder's provenance)
The four tiers are not new: they are the shipped salience ladder
already pinned by Raxol.UI.Harness.Prominence -- 0.6 is the
ordinary-context tier (Prominence.needs_input_floor/0 floors
awaiting-input content exactly there), the 1.0/0.6 pair is pinned
by regression to survive 256-color quantization as distinct palette
indices, and 0.4 is that ladder's own floor. This policy was
ratified with an explicit "no new tiers" fence: it maps recency onto
the EXISTING ladder, one uniform 0.2 step per turn behind, and any
retuning of the tier values themselves belongs to the solver-side
ladder (and its pending human-eye ratification pass, see the
Prominence moduledoc), never to this mapping.
Seal-time grading (the substrate law)
Prominence for a block is decided exactly once, at the moment the
block is painted/sealed, and is never re-graded afterward -- the
print-once history substrate this framework paints through cannot
repaint scrollback (see Raxol.Harness.Surface's own "seal / seal-once"
glossary entry). A block sealed during its own turn seals at 1.0 and
does NOT fade when the next turn starts; it is not re-visited once
painted.
The fade ladder is therefore visible wherever MULTIPLE turns are
painted in one pass -- a full re-render, a reattach rebuild, the live
region -- while incrementally sealed inline history keeps its
seal-time grade forever, unchanged by anything that happens
afterward. This is the honest, substrate-constrained reading of the
ratified "scoped by policy" clause: the policy grades a block once,
when it is painted; there is no retroactive re-grading, and no
mechanism in this module (or its one caller,
Raxol.Harness.Surface.render_block_lines/3) ever asks "what would
this block's prominence be now?" for an already-sealed block.
Composition with the needs-input floor
This policy only produces the RECENCY prominence -- it has no opinion
on, and never special-cases, content that is awaiting user input.
That promotion lives one layer below, in
Raxol.UI.Components.Harness.Block and
Raxol.UI.Harness.Prominence: a live :approval block auto-passes
needs_input: true into its render context, and
Prominence.resolve/3 floors the EFFECTIVE prominence at
Prominence.needs_input_floor/0 (0.6) before the fade runs. So a
pending approval this policy grades at, say, 0.4 (three-plus turns
behind) still renders no dimmer than the 0.6 ordinary-context tier --
an approval outranks its own ladder tier automatically, with no
needs-input branch anywhere in this module.
Scope note: on today's sole wiring point (the fixture-replay
surface's seal path) this composition is dormant -- the block builder
constructs every block already :sealed, and the seal frontier holds
a live approval back from painting at all, so a live approval never
reaches the graded path there; it lives in the repaintable footer
instead. The floor composition is real and tested at the Prominence
layer, and engages the moment a live-rendering surface grades live
blocks.
Coverage today (an honest scope note)
The grade is threaded through Raxol.UI.Components.Harness.BlockBody's
render context for EVERY block, but only Block.render/2 -- the folded
path, and every fallback -- resolves context[:prominence] into a fade
today. The expanded rich body components mounted via
Raxol.UI.Components.Harness.BodyProvider do not yet thread the key
(a pre-existing gap in those components, not in this policy or its
wiring); when they grow that support, the grade is already there in
their context, unchanged.
Turn identity is opaque
turn_ids/turn_id are opaque terms (atoms in tests, strings on the
wire) compared only with == -- this module has no notion of a turn's
internal shape, ordering by VALUE, or timestamps. "Recency" here means
purely POSITION in first-seen order among the turns actually present,
never wall-clock or ts distance.
Summary
Functions
The floor of the recency fade ladder (0.4) -- the prominence a
block three-or-more turns behind the current one renders at.
Grades turn_ids (the per-block list of turn identifiers, in
transcript order) against current_turn, returning a same-length list
of ladder prominences.
Convenience for the render path: grades block (matched loosely as
%{event_refs: refs} so both Raxol.UI.Components.Harness.Block.t()
and a plain map with that key work) against events -- typically
projection.source_events, the retained durable events a
Raxol.Harness.Projection was built from.
Batch form of grade_block/2: grades EVERY block in blocks against
events with a SINGLE walk over the event list -- O(events + total_refs) for the whole batch, where the per-block form re-derives
the (identical) turn order and current turn from the full event list
on every call.
Maps turns_behind (a non-negative integer distance from the current
turn) onto its ladder prominence: 0 -> 1.0, 1 -> 0.8, 2 -> 0.6,
3 or more -> 0.4 (the floor). nil or a negative integer --
an unknown or defensively-clamped position -- always resolves to
1.0: this policy's own uncertainty never reads as "darker."
Types
Functions
@spec floor() :: prominence()
The floor of the recency fade ladder (0.4) -- the prominence a
block three-or-more turns behind the current one renders at.
@spec grade([turn_id() | nil], turn_id() | nil) :: [prominence()]
Grades turn_ids (the per-block list of turn identifiers, in
transcript order) against current_turn, returning a same-length list
of ladder prominences.
Turn ORDER is the distinct non-nil turn ids in turn_ids, in
first-seen order (never wall-clock, never a separate turn list --
exactly the turns actually present in this transcript). A block's
turns_behind is position(current_turn) - position(block_turn) in
that order.
Caller contract (load-bearing): turn_ids must be in transcript
order. Positional recency is meaningless on a reordered list -- a
turn that appears first IS oldest to this function, whatever its id
looks like. This is a documented precondition, not something this
pure function can validate (turn ids are opaque; there is no
order-independent notion of "older" to check against).
Rules (each a documented guarantee, never darker than honest uncertainty warrants):
current_turnisnil-- no honest way to grade -- every block grades1.0.current_turnabsent from the order (a brand-new turn with no blocks yet inturn_ids) is treated as one position NEWER than the newest listed turn -- every existing block is graded one tier further behind than it would be if that new turn already had an entry.- A block's own turn id
nilgrades that block1.0(nothing to grade against). - A block's turn NEWER than
current_turn(shouldn't happen in a well-formed transcript; handled defensively) clampsturns_behindto0->1.0, rather than extrapolating a "prominence above 1.0" that has no meaning.
@spec grade_block(map(), [map()]) :: prominence()
Convenience for the render path: grades block (matched loosely as
%{event_refs: refs} so both Raxol.UI.Components.Harness.Block.t()
and a plain map with that key work) against events -- typically
projection.source_events, the retained durable events a
Raxol.Harness.Projection was built from.
Derivation (defensive throughout -- a non-map entry in events is
skipped, never raised on; the whole derivation is a SINGLE pass over
events, so grading one block costs one walk, never three):
- the block's own turn is the
:turn_idof the FIRST event ineventswhose:idis inblock.event_refs(nilifevent_refsis empty or nothing matches); - the current turn is the
:turn_idof the LAST event ineventsthat carries a non-nil:turn_id(nilif none do); - the turn order is the distinct non-nil
:turn_ids acrossevents, in first-seen order.
Empty events, an unresolvable block turn, or a nil current turn
all fall out of the same grade/2 core as 1.0 -- see that
function's own rule list.
Input contract (load-bearing, guaranteed upstream for the intended
feed): events must be journal/ingest-ordered and complete for the
blocks being graded. Both hold for projection.source_events by
construction: Raxol.Harness.Projection.Recovery.filter_ids/1 drops
duplicate/out-of-order ids BEFORE retention (so the retained list is
id-monotonic), and source_events retains EVERY durable event for
the session un-windowed (only :ephemeral tier -- item_delta
traffic, which never enters a block's event_refs -- is excluded),
so every block built by the projection resolves its turn here. A
caller feeding a reordered or windowed list violates the contract;
each violation degrades toward 1.0 (never darker), per the same
uncertainty rule as everything else in this module.
@spec grade_blocks([map()], [map()]) :: [prominence()]
Batch form of grade_block/2: grades EVERY block in blocks against
events with a SINGLE walk over the event list -- O(events + total_refs) for the whole batch, where the per-block form re-derives
the (identical) turn order and current turn from the full event list
on every call.
Equivalence law (tested): grade_blocks(blocks, events) is exactly
Enum.map(blocks, &grade_block(&1, events)) -- same input contract,
same defensive fallbacks, only the cost differs. This is the entry
point a bulk paint should use: a full re-render, a reattach rebuild,
or any pass that grades a whole projection at once. The incremental
seal path (Raxol.Harness.Surface.render_block_lines/3) keeps the
per-block form because its hold-back-one design seals a bounded
number of blocks per advance.
@spec prominence(integer() | nil) :: prominence()
Maps turns_behind (a non-negative integer distance from the current
turn) onto its ladder prominence: 0 -> 1.0, 1 -> 0.8, 2 -> 0.6,
3 or more -> 0.4 (the floor). nil or a negative integer --
an unknown or defensively-clamped position -- always resolves to
1.0: this policy's own uncertainty never reads as "darker."