StatifierUI.Trace.Diagnostic (StatifierUI v0.9.1)

Copy Markdown View Source

The wire error object: what docs/wire-format.md documents as an event object's error key.

It has two arms, discriminated on the wire by "class" (ADR-0014). object/4 renders an expression failure (class: "expression") and is the bulk of this module - spans, anchors, and the location_kind discipline below are all its. reason_object/4 renders a non-value reason term (class: "reason"), peeling {:nested_content, _, _} first so a wrapped expression failure comes back through object/4 with its span intact rather than as an opaque string.

This is the only caller of Statifier.Parser.Location.resolve_span/4 in this repo (ADR-0002 - never reimplement the engine's span composition locally). Every span this module emits, and every location it composes from one, follows that helper's convention: both the predicator span's end and the returned location's end are exclusive.

resolve_span/4 degrades rather than raising - a position past the end of the value clamps to the value location's end, and a raw/expanded desync returns the value location whole - so this module never rescues it. Per project convention (CLAUDE.md, "errors are values / never rescue-to-default at a leaf"), a call that could fail is not the shape the failure takes here: the helper's own degraded return already is the fallback.

A nil value location or a nil span means there is nothing to resolve. anchor/3 never calls resolve_span/4 in either case - it returns {:node, _} instead, so object/4 falls back to the owning node's own span. That branch is this module deciding not to call the helper, which is a different thing from catching what it raises.

location_kind reports what the producer did, not what the helper decided

"resolved" means resolve_span/4 was called with a value location and a span. "node" means it was not (no span, or no value location) and the owning node's own span was emitted instead. This module never inspects the result to guess which happened - resolve_span/4's own degraded return (falling back to the value location whole) is indistinguishable from a genuine resolution by looking at the location alone, and modeling that distinction here would be exactly the ADR-0002 failure this module exists to prevent. So "resolved" may still span the whole attribute value when the helper degraded internally.

Summary

Functions

Where to anchor error's span for origin: the value location of the failing expression ({:value, _}, resolved against error.span by the caller), the owning node's own span with nothing further to resolve ({:node, _}), or nothing at all (:none).

The wire error object for error, raised with cause origin.

The wire error object for a non-value reason term - ADR-0014's class: "reason" arm, and the terms it peels on the way there.

Functions

anchor(arg1, machine, error)

Where to anchor error's span for origin: the value location of the failing expression ({:value, _}, resolved against error.span by the caller), the owning node's own span with nothing further to resolve ({:node, _}), or nothing at all (:none).

Dispatches on origin and, for content, on the content struct itself - never on a generic Map.get(node, :location) || Map.get(node, :node_location) reflection, the same posture StatifierUI.Trace.Manifest's own content-location dispatch takes and for the same reason: that fallback is silently wrong for Statifier.Machine.Content.Assign, whose :location is a path-expression string, not a span.

A candidate value location is only ever returned as {:value, _} when error.span is non-nil - a nil span means resolve_span/4 would not be called, so the caller must not be handed a location that invites it.

error of nil is ADR-0014's class: "reason" arm: there is no expression and no span, so every origin that names a node at all returns {:node, _} and nothing ever returns {:value, _}.

object(error, origin, machine, source)

The wire error object for error, raised with cause origin.

"class" is always "expression" here - ADR-0014's discriminator, explicit on both arms rather than absent-means-expression, so a consumer never infers the class from a key's absence. "kind" and "expression" are always present. "span" is present only when error.span is non-nil. "location"/"location_kind" are present only when both machine and source are supplied and anchor/3 finds a location to anchor on - origin of nil, machine/source of nil, or an origin anchor/3 cannot resolve at all all omit both keys, key absence being the ADR-0005 discipline for "nothing here" rather than a null or a sentinel.

reason_object(term, origin, machine, source)

@spec reason_object(
  term(),
  Statifier.Event.Cause.origin() | nil,
  Statifier.Machine.t() | nil,
  String.t() | nil
) :: map()

The wire error object for a non-value reason term - ADR-0014's class: "reason" arm, and the terms it peels on the way there.

term is the unconstrained reason Statifier.Interpreter.Content raises an execution error with, which reaches an error.execution or error.communication event's data as a tagged tuple, a bare atom, or anything else at all. Three things happen to it here, in this order:

  1. {:nested_content, c_index, inner} is peeled, repeatedly, collecting each c_index in order into "content_path". The wrapper is what an <if> partition or a <foreach> body puts around a failure raised inside it, and treating it as opaque would leave every nested expression failure rendering as a string - ADR-0014 decision 4.
  2. An innermost %Statifier.Evaluator.Error{} routes to object/4, so a wrapped expression failure keeps its span and its location and renders as class: "expression", with "content_path" beside it.
  3. Anything else renders as class: "reason": "kind" derived from the term's shape by reason_kind/1, and "reason" the whole term's inspect/1.

"reason" is documented on the wire as human-readable text rather than structured data to branch on, the same wording session.terminated's reason carries. "kind" is the branchable half.

"location"/"location_kind" are present only when the producer could anchor, and on this arm the anchor is always the owning node's own span (location_kind: "node"): there is no expression span to resolve against, so anchor/3 is called with no error at all.