Current section
Files
Jump to
Current section
Files
lib/scoria_web/copy.ex
defmodule ScoriaWeb.Copy do
@moduledoc """
Strings-only operator copy.
`ScoriaWeb.Copy` returns strings / keyword data ONLY and renders nothing —
zero `~H`. It owns the canonical D-25 action-verb set, the D-25 status-label
map, and the empty/error/loading copy strings (`empty_title/1`,
`empty_cta/1`, `error_line/1`, `loading_label/1`).
Hard line (D-24b): `Copy` is data, `ScoriaWeb.UI.empty_state/1` and
`skeleton/1` stay the renderers that receive these strings.
Per-domain copy that branches on record data (a `case`/`cond` on
`status`/`tool_name`/`kind`) lives in the sibling per-domain modules
(`IncidentCopy`, `DatasetCopy`, `ReviewCopy`, `ConnectorCopy`), each
templated on `ScoriaWeb.ApprovalCopy` (D-24c).
Voice: calm + exact + useful (brandbook §6). Never emits a banned word
("magic", "seamless", "Nothing here").
"""
@action_verbs [
approve: "Approve",
deny: "Deny",
promote: "Promote",
add_manually: "Add manually",
resolve: "Resolve",
open: "Open",
select: "Select",
review: "Review",
dismiss: "Dismiss",
grant: "Grant",
revoke: "Revoke",
open_trace: "Open trace",
retry: "Retry",
pin: "Pin",
compare: "Compare",
edit: "Edit",
run: "Run"
]
@status_labels [
{"pending", "Pending"},
{"approved", "Approved"},
{"expired", "Expired"},
{"not_scored", "Not scored"},
{"inconclusive", "Inconclusive"},
{"passed", "Passed"},
{"failed", "Failed"},
{"regressed", "Regressed"},
{"running", "Running"},
{"promoted", "Promoted"},
{"draft", "Draft"},
{"published", "Published"},
{"connected", "Connected"},
{"disconnected", "Disconnected"},
{"idle", "Idle"}
]
@doc "The full canonical D-25 action-verb keyword list."
def action_verbs, do: @action_verbs
@doc "The full canonical D-25 status-label vocabulary (excludes approval-domain 'Denied', D-24d)."
def status_vocabulary, do: @status_labels
@doc """
Operator label for a D-25 action-verb key.
Falls back to a humanized transform of the key for anything not curated
(never raises — safe for unseen keys).
"""
def action_verb(key) when is_atom(key) do
Keyword.get(@action_verbs, key) || humanize(Atom.to_string(key))
end
def action_verb(key) when is_binary(key) do
key
|> String.to_atom()
|> action_verb()
end
@doc """
Operator label for a status value (D-25 canonical vocabulary).
ADDITIVE-style lookup with a safe generic fallback — never raises on an
unseen status. Does NOT curate `"rejected"` — "Denied" is approval-domain
only (D-24d, `ApprovalCopy.decision_outcome/1`).
"""
def status_label(nil), do: "Unknown"
def status_label(status) when is_atom(status), do: status |> Atom.to_string() |> status_label()
def status_label(status) when is_binary(status) do
case List.keyfind(@status_labels, status, 0) do
{_status, label} -> label
nil -> humanize(status)
end
end
def status_label(_status), do: "Unknown"
@doc "Empty-state title for a domain/context key. Says what's missing, in domain nouns."
def empty_title(:incidents), do: "No incidents match this view"
def empty_title(:datasets), do: "No datasets match this view"
def empty_title(:reviews), do: "No candidates need review"
def empty_title(:connectors), do: "No runtimes connected"
def empty_title(:approvals), do: "No approvals waiting"
def empty_title(:runs), do: "No runs recorded yet"
def empty_title(:evals), do: "No eval datasets yet"
def empty_title(_domain), do: "No matches for these filters"
@doc "Empty-state next-action copy for a domain/context key."
def empty_cta(:incidents),
do: "Incidents appear here once a connector or workflow reports one."
def empty_cta(:datasets), do: "Promote a production trace to start a regression suite."
def empty_cta(:reviews), do: "Review candidates appear here once a run needs a decision."
def empty_cta(:connectors),
do: "Runtime activity appears here once a host session connects for this tenant."
def empty_cta(:approvals),
do: "Approval requests appear here once a tool call needs a decision."
def empty_cta(:runs), do: "Runs appear here once a workflow executes for this tenant."
def empty_cta(:evals), do: "Promote a production trace to start a regression suite."
def empty_cta(_domain), do: "Adjust your filters or check back once new data arrives."
@doc "Error line for a failure reason key. Says what happened, not a vague apology."
def error_line(:connection),
do: "Could not reach the server. Check the connection and try again."
def error_line(:not_found), do: "This record no longer exists or was removed."
def error_line(:stale),
do: "This record changed since it loaded. Refresh to see the latest state."
def error_line(:unauthorized), do: "You don't have access to this action."
def error_line(_reason), do: "Something interrupted this request. Try again."
@doc "Loading label for a domain/context key."
def loading_label(:incidents), do: "Loading incidents…"
def loading_label(:datasets), do: "Loading datasets…"
def loading_label(:reviews), do: "Loading review candidates…"
def loading_label(:connectors), do: "Loading runtime and connector posture…"
def loading_label(:approvals), do: "Loading approval requests…"
def loading_label(:runs), do: "Loading runs…"
def loading_label(:evals), do: "Loading eval datasets…"
def loading_label(_domain), do: "Loading…"
defp humanize(value) when is_binary(value) do
value |> String.replace("_", " ") |> String.capitalize()
end
end