Raxol. Harness. PanelProjection
(Raxol v2.6.1)
View Source
Pure read-model fold over extract meta events, for the harness's
worktracks/memory/plan overlay panels.
Frozen contract vs. contract-shape assumption
This module builds against the frozen meta-event contract shapes: the
extract entry in Raxol.Agent.Meta.Registry (required payload keys
[:class, :op, :item, :refs], scope :session), and against contract-
shape fixtures authored to that registry
(test/fixtures/harness/sessions/projection-panels.jsonl). The registry
entry is load-bearing and settled. The per-class item field shapes this
module reads (worktracks: id/lane/title/status; memory:
key/value; plan: id/title/status) are contract-shape
ASSUMPTIONS — they must be verified against real agent-emitted extract
events before this unit's PR merges. Nothing here should be read as ground
truth for the wire shape until that verification happens.
Tolerant reading
An unknown class, unknown op, unrecognized type, or a missing/
malformed field is skipped, never an error — a single unrecognized
meta event must not take down the whole fold. This is deliberate: a
malformed instance of a known shape found in a real journal (e.g. an
extract whose op is "add" but whose item is a string) is a defect
to report against the emitting codec, never something to silently work
around inside this module. "Skip" here only ever means "this class/op/
shape isn't one I know how to read," not "I read it and it was wrong."
Recompute, not incrementally cached
The read-model is recomputed on demand -- at panel summon and on each
footer repaint while a panel is open -- not incrementally maintained as
events arrive. fold/2 and render_lines/2 are both O(events) per
paint, matching the surface's existing per-advance full re-projection
(see Raxol.Harness.Projection). "Dismissed is not dead": the fold source
is the projection's retained durable events
(Raxol.Harness.Projection.source_events/1, itself already durable-only),
so re-summoning a panel folds current state without touching the block
projection at all.
Clamps
Two independent clamps guard against hostile or runaway meta-event content (meta payloads are agent-produced and untrusted):
@max_field_bytes-- every string entering a read-model is passed throughdisplay_string/1first: binaries longer than 512 bytes are clamped to at most 512 bytes (byte-sliced, then any trailing partial codepoint from a mid-codepoint split is dropped so the result stays valid UTF-8 -- this drops at most 3 bytes); non-binaries are rendered viainspect/2with bounded:limit/:printable_limit, never evaluated or atomized. Control bytes are not stripped here -- sanitizing control bytes for actual terminal rendering isRaxol.Harness.Surface.ViewText's trust boundary, downstream of this module; stripping them here too would just duplicate that seam without adding safety.@max_entries-- at most 500 distinct identities are retained per kind. A 501st distinct identity added evicts the oldest retained entry (first-seen order), bounding memory even against a runaway or adversarial extract stream.
Summary
Functions
Folds events (event-shaped maps or Raxol.Harness.Fixture.Event
structs, top-level fields read via Map.get/2 -- works for both) into
the kind read-model. Only family: :meta, type: :extract events with a
matching class contribute; everything else is silently skipped (see
moduledoc, "Tolerant reading"). Pure and deterministic: identical input
always yields an identical read-model.
The three panel kinds this module folds.
Folds then formats kind's read-model into footer-row lines. Every line
is newline-flattened (\r\n/\n/\r -> " ") so one read-model
entry is always exactly one footer row -- the same discipline
Raxol.UI.Harness.OverlayPicker applies to item labels, and load-bearing
for the same reason: a raw embedded newline here would silently overflow
a caller's fixed row budget. An empty read-model renders as ["(empty)"].
No width truncation happens here -- ViewText owns that, same as
OverlayPicker.
Types
@type kind() :: :worktracks | :memory | :plan
@type worktracks_lane() :: %{name: String.t(), items: [worktracks_item()]}
Functions
@spec fold(kind(), [map()]) :: [worktracks_lane()] | [memory_entry()] | [plan_entry()]
Folds events (event-shaped maps or Raxol.Harness.Fixture.Event
structs, top-level fields read via Map.get/2 -- works for both) into
the kind read-model. Only family: :meta, type: :extract events with a
matching class contribute; everything else is silently skipped (see
moduledoc, "Tolerant reading"). Pure and deterministic: identical input
always yields an identical read-model.
@spec kinds() :: [kind()]
The three panel kinds this module folds.
Folds then formats kind's read-model into footer-row lines. Every line
is newline-flattened (\r\n/\n/\r -> " ") so one read-model
entry is always exactly one footer row -- the same discipline
Raxol.UI.Harness.OverlayPicker applies to item labels, and load-bearing
for the same reason: a raw embedded newline here would silently overflow
a caller's fixed row budget. An empty read-model renders as ["(empty)"].
No width truncation happens here -- ViewText owns that, same as
OverlayPicker.