Cairnloop.Web.ToolProposalPresenter (cairnloop v0.1.0)

Copy Markdown View Source

Pure, total presenter for Cairnloop.Governance.ToolProposal structs.

Mirrors ReviewTaskPresenter and GapCandidatePresenter exactly:

  • Total functions with safe fallbacks — no crashes on unexpected input
  • Pattern-match on struct and bare atoms
  • Returns strings and atoms only — never markup, never raw Elixir terms
  • Never re-reads live config at render time (snapshots are truth for trust fields)

D-22 masking choke point: input_rows/1 is the ONLY place raw input_snapshot values are converted to display rows; it allowlists known scalar fields and applies the "Unsupported value" posture to nested/unknown/sensitive values.

D-14: reason_label/1 humanizes all reason shapes (including {:missing_scopes, _} tuples and atoms) — never passes inspect/1 output to operators.

D-13 / brand §7.5: risk_tier_tone/1 returns an atom (:info/:warning/:danger) that LiveView maps to color — always paired with text, never state-by-color-alone.

Summary

Functions

Human label for an approval mode atom.

Future-tense operator copy describing the approval gate — D-12 honesty seam.

Present-tense operator copy for an active approval record (D15-16 real copy).

Returns operator copy explaining why a proposal was blocked, or nil/empty for non-blocked.

Human-readable relative timestamp for an event datetime.

Returns a human-readable line for a ToolActionEvent.

Converts an input_snapshot map to a list of {label, value} display rows.

Returns a calm one-sentence explanation from the policy_snapshot.

Humanizes a reason value — never passes raw inspect output to operators (D-14).

Human label for a risk tier atom.

Semantic tone for the risk tier — returns an atom only (brand §7.5 / D-13). LiveView MUST pair this with a text label — never rely on color alone.

Human summary of required scopes from a scope_snapshot map.

Groups proposal status into display buckets.

Human label for the status group.

Human label for a proposal status atom or ToolProposal struct.

Status meaning — one calm sentence explaining what this state means for operators.

Returns a map of de-emphasized trace fields for a proposal. Safe for display; all values are strings. Shows a short idempotency key suffix.

Functions

approval_mode_label(arg1)

Human label for an approval mode atom.

approval_outlook(arg1)

Future-tense operator copy describing the approval gate — D-12 honesty seam.

Returns:

  • nil for :auto (no gate to describe)
  • String for :requires_approval (gate exists — future-tense)
  • String for :always_block (terminal — "cannot be approved or run")

approval_outlook_for_approval(approval)

Present-tense operator copy for an active approval record (D15-16 real copy).

Takes a %ToolApproval{} struct or a map with a :status key (and optionally :reason). Returns a calm, present-tense string for operators — or nil for unknown states.

Replaces the future-tense approval_outlook/1 honesty seam when an active approval exists. Never re-reads live config (brand §5.3/§5.6 reason-forward, no raw terms).

block_reason_copy(tool_proposal)

Returns operator copy explaining why a proposal was blocked, or nil/empty for non-blocked.

event_timestamp_label(dt)

Human-readable relative timestamp for an event datetime.

history_line(tool_action_event)

Returns a human-readable line for a ToolActionEvent.

Catch-all → "Workflow updated" for any unrecognized event_type (D-24).

input_rows(snapshot)

Converts an input_snapshot map to a list of {label, value} display rows.

MASKING RULES (D-22):

  • Iterates ALL keys (both atom and string-keyed JSONB snapshots are supported)
  • Scalar values (string, number, boolean) are humanized and displayed
  • Lists of scalars are joined with ", "
  • Nested maps, tuples, and other complex values → "Unsupported value" sentinel
  • Sensitive field names (containing "password", "token", "secret", "key") → "••••••"
  • Never dumps raw nested structures to operators

policy_explanation(snapshot)

Returns a calm one-sentence explanation from the policy_snapshot.

Uses dual-key lookup (atom + string) to survive the JSONB string-key round-trip (mirrors ReviewTaskPresenter.metadata_value/2). Raw map is NOT returned — only a humanized sentence. (D-14 / brand §5.6)

reason_label(reason)

Humanizes a reason value — never passes raw inspect output to operators (D-14).

Handles:

  • nil → nil
  • → "Missing scopes: scope1, scope2"
  • known atom → human-readable label from @reason_labels
  • unknown atom → humanize via capitalize-and-replace
  • string → pass through
  • any tuple → generic human fallback (no raw inspect)

risk_tier_label(arg1)

Human label for a risk tier atom.

risk_tier_tone(arg1)

Semantic tone for the risk tier — returns an atom only (brand §7.5 / D-13). LiveView MUST pair this with a text label — never rely on color alone.

:info — :read_only (low risk, informational) :warning — :low_write (moderate risk) :danger — :high_write, :destructive (high risk)

scope_summary(arg1)

Human summary of required scopes from a scope_snapshot map.

status_group(arg1)

Groups proposal status into display buckets.

  • :awaiting — :proposed, :needs_input (action needed but not yet blocked)
  • :blocked — :scope_invalid, :policy_denied (terminal blocked state in Phase 14)
  • :active — declared for future Phase 15/16 running/approved states (unreachable now)
  • :done — declared for future completed/executed states (unreachable now)

status_group_label(arg1)

Human label for the status group.

status_label(arg1)

Human label for a proposal status atom or ToolProposal struct.

status_meaning(arg1)

Status meaning — one calm sentence explaining what this state means for operators.

trace_metadata(proposal)

Returns a map of de-emphasized trace fields for a proposal. Safe for display; all values are strings. Shows a short idempotency key suffix.