Statifier.Validator.Error (Statifier v2.0.0)

Copy Markdown View Source

The validator's own error shape: never raised, always collected into a list. Mirrors Statifier.Lowering.Error's shape (reason, message, location) with character-identical field names, so a future common diagnostic protocol can adopt both without either layer's reason union leaking into the other's. The two layers' error lists are never observed together - Statifier.Validator.validate/2 only ever receives a document lowering already accepted - so sharing the shape rather than the type is enough.

reason is a closed tagged-tuple union, declared once in full here even though initially only :duplicate_id has a constructor. Later additions add a constructor per reason rather than reopening the type, following Statifier.Lowering.Error's own precedent.

code/1 returns the reason tuple's tag atom - the stable error code the bead's acceptance criteria ask for - while reason itself keeps carrying the offending ids as data, the way the rest of the repo does.

Summary

Types

Which slot a shared transition-shape violation (:transition_count, :transition_missing_target, :transition_forbidden_attribute) came from: a state's <initial> element, or a :history state's own default transition. The id is nil exactly when the owning state's own id is.

t()

Functions

Spec 5.4.2: an <assign> element carries both an expr attribute and non-blank child content - "A conformant SCXML document MUST specify either 'expr' or children of <assign>, but not both." expr is the attribute's value as lowered, so the message quotes the expression rather than the text, which has no span of its own. Reported at the <assign>'s own element span (Statifier.Document.Assign.node_location, not the SCXML location attribute that struct also carries - Statifier.Document.Assign's moduledoc names the two spans apart).

Check 9 (spec 3.2.1): the root element's resolved namespace, document.namespace, is not the SCXML namespace. uri is document.namespace itself - nil for a fragment that declares no namespace at all, the boilerplate-free case that relaxed parsing left representable.

Check 10 (spec 3.2.1): the root element's version is not "1.0". version is nil when the attribute is absent.

Spec 6.3.1: "A conformant SCXML document MUST specify exactly one of sendid or sendidexpr." A <cancel> carrying neither. Reported at the <cancel> element's own location.

Spec 6.3.1: "sendid ... Must not occur with sendidexpr." A <cancel> carrying both. Reported at the <cancel> element's own location.

The reason tuple's tag - the stable error code.

Spec 5.6: a <content> element carries both an expr attribute and inline content, whether that content is text or markup (ADR-0041) - the spec's "MUST NOT specify both" that lib/statifier/document/content.ex deliberately leaves representable so this check can report it. expr is the attribute's value as lowered, and payload names which inline form was actually present, :text or :markup - markup wins when both are somehow present, since it is the more specific payload. The reason tuple stays {:content_expr_and_text, expr} regardless of payload; only the message varies. For :text, the message quotes the expression rather than the text, which has no span of its own (Statifier.Document.Content's moduledoc); for :markup, the message quotes the expression and names markup by kind, since markup DOES have a span of its own (markup_location) even though this diagnostic does not use it. Reported at the <content> element's own location, the only span covering both halves of the conflict.

Check 13 (spec 5.3.2): a <data> carries both an expr and a src attribute - "MAY have either a 'src' or an 'expr' attribute, but MUST NOT have both". id is the offending <data>'s own id.

Check 13 (spec 5.10): a <data id="..."> begins with _ - "A conformant SCXML document MUST NOT contain ids beginning with '_' in the <data> element".

Check 13 (spec 5.3.2): a <data> carries an expr or src attribute and non-blank child text - "if either attribute is present, the element MUST NOT have any children". Whitespace-only text does not count (Checks.Data.blank?/1).

Check 13 (spec 3.2.2 / 3.3.2 / 3.4.2): a <datamodel> sits on a :final or :history state - legal only under <scxml>, <state>, and <parallel>. kind is the offending state's own kind.

Check 7 (spec 3.3): id names a compound state with no initial attribute and no <initial> element, whose first child in document order has child_kind - a :history pseudo-state cannot be entered by the spec 3.3 default-entry fallback. Reported at the first child's own location.

Spec 5.5: a <donedata> carries both a <content> child and one or more <param> children - "either a single <content> element or one or more <param> elements as children of <donedata>, but not both". id is the owning :final state's own id (nil when unwritten, the same shape final_parent_missing_id/2 allows). Reported at the <donedata>'s own location, the only span covering both halves of the conflict.

Check 8 (spec 3.7, 5.7): id names a non-:final state carrying a non-nil donedata - <donedata> is only legal on :final. Reported at the donedata's own location.

Check 1 (spec 3.14): id names more than one state in the document. The location is the offending (non-first) occurrence's own span, never the canonical one.

Check 1 (spec 3.14): a state was written with id="". id is typed as an XML Schema ID, whose lexical space is an XML Name and therefore excludes the empty string, so an empty id is not a name a transition could ever target.

Check 6 (spec 3.7): id names a state child of a :final - only onentry, onexit, and donedata are legal there. Reported once per offending child, at the child's own location (not the <final>'s), so the caret lands on the element that should not be there.

Check 6 (spec 3.7): id names a :final carrying a <transition>. A <final> is where a region stops, so it has no outgoing transitions to take - spec 3.7's content model is onentry, onexit, and donedata only. Reported once per offending transition, at the transition's own location rather than the <final>'s, matching final_has_states/2.

Check 12 (spec 3.7, Appendix D enterStates): a <final> state's parent carries no id. Entering a non-top-level <final> raises done.state.{parent.id}, so a parent with no written id has a completion event the spec names after a name it does not have - and no transition could match "done.state." even if it were raised. final_id is the offending <final> child's own id, which names the completion the document loses; it rides in reason and stays out of message, because it is itself optionally nil and interpolating that reads as a bug ("containing <final> nil") in the one case the check exists for. The location is the parent's span, since the parent is the element that has to change.

Check 5 (spec 3.10): a :history state id is a child of parent_kind, which is not a compound <state> or <parallel> - the only two legal parents for a history pseudo-state. parent_kind is :scxml for the document root itself.

Check 5 (spec 3.10): a <history type="..."> value that is neither "shallow" nor "deep". raw is the source text as written (Location.slice/2), so the message quotes what the author actually typed rather than the default it silently lowered to.

Spec 4.3.2: an <if> with more than one <else> branch (each an attribute-less, cond-less partition per Statifier.Document.If.Branch). 4.3 caps <else> at "0 or 1 times", so a second one is malformed regardless of position. Reported at the second <else>'s own location, never the first's.

Spec 4.3.2: "In a conformant SCXML document, <else> MUST occur after all <elseif> tags." An <elseif> branch that follows the <if>'s <else> branch violates that ordering. Reported at the offending <elseif>'s own location (Statifier.Document.If.Branch.location), not the enclosing <if>'s.

Check 4 (spec 3.3, 3.6): a state carries both an initial attribute and an <initial> element - at most one is legal. Reported at the <initial> element's own span.

Check 3 (spec 3.3, 3.6): a resolved initial target id is not a descendant of the state it initializes, parent_id - ancestry, not direct-child membership (v1's bug).

Check 3 (spec 3.3): id names a state carrying an initial attribute or <initial> element, and the spec MUST NOT: an atomic state (no states children, or a :parallel, :final, or :history kind) has nothing to default into. Fires ahead of, and suppresses, the descendancy check for the same state.

Spec 6.4.1: an <invoke autoforward="..."> value that is neither "true" nor "false". Same slice-back substrate as transition_bad_type/2 and Statifier.Validator.Checks.Enums's other three enumerated attributes - Statifier.Lowering.Attributes.atom/4 maps an unrecognised value onto the false default, so the atom alone cannot tell autoforward="false" from autoforward="sideways". raw is the source text as written (Location.slice/2).

Spec 6.4.1: "id: Must not occur with the 'idlocation' attribute." An <invoke> carrying both. Reported at the <invoke> element's own location.

Spec 6.4.1: "namelist: Must not occur with the <param> element." An <invoke> carrying a written namelist alongside one or more <param> children. Reported at the <invoke> element's own location.

Spec 6.4.1: the other half of src's "Must not occur with ... the <content> element" constraint - an <invoke> carrying a <content> child alongside src or srcexpr. Reported at the <invoke> element's own location.

Spec 6.4.1: "src: Must not occur with the 'srcexpr' attribute or the <content> element." The src/srcexpr half of that constraint; the <content> half is invoke_src_and_content/1. An <invoke> carrying both src and srcexpr. Reported at the <invoke> element's own location.

Spec 6.4.1: "type: Must not occur with the 'typeexpr' attribute." An <invoke> carrying both. Reported at the <invoke> element's own location - the reason carries no payload, matching {:if_duplicate_else} and {:script_no_src_or_text}'s own nullary shape for a violation with no natural id to name it by.

Spec 5.7: a <param> element carries both an expr attribute and a location attribute - "A conformant SCXML document MUST specify either the 'expr' attribute of <param> or the 'location' attribute, but MUST NOT specify both." name is the offending <param>'s own name attribute. Reported at the <param> element's own location.

Spec 5.7: a <param> element carries neither an expr attribute nor a location attribute - the same "MUST specify either ... but MUST NOT specify both" rule as param_expr_and_location/2, its other half. name is the offending <param>'s own name attribute. Reported at the <param> element's own location.

Spec 5.8.2: "A conformant SCXML document MUST specify either the 'src' attribute or child content, but not both." src is unreachable in a struct lowering ever builds (Statifier.Lowering.Builders.build_script/2 already refuses to build one when src is written - ADR-0026 decision 2), so the only way this document-layer check can still observe the MUST's violation is the other missing half: a <script> with neither. The reason carries no payload - a <script> has no id and no attribute-value to name it by, matching {:empty_id} and {:if_duplicate_else}'s own nullary shape.

Spec 3.2.1: an <scxml binding="..."> value that is neither "early" nor "late". Same slice-back substrate as transition_bad_type/2, against the :early default.

Check 13 (spec 3.2.1): a datamodel="..." value outside ["predicator", "elixir", "null", "ecmascript", "xpath"] - the spec's own three named values ("null", "ecmascript", "xpath") plus this platform's two ("predicator", its default; "elixir", an alias for v1-converted documents). Accepting "ecmascript" and "xpath" does not claim this engine implements either datamodel - spec 3.2.1's Valid Values clause names "other platform-defined values" as legal too, and Appendix B's intro states no rule for what a processor does with a datamodel it does not implement. What this check actually catches is a typo (datamodel="predicater", datamodel="javascript"); which datamodel this engine runs is docs/datamodel.md's answer, not this attribute's. raw is the source text as written (Location.slice/2), the same substrate as scxml_bad_binding/2.

Spec 6.2.2: "delay ... Must not occur with 'delayexpr' or when the attribute 'target' has the value "_internal"." The delay/delayexpr half of that constraint; the target="_internal" half is send_delay_and_internal_target/1. A <send> carrying both delay and delayexpr. Reported at the <send> element's own location.

Spec 6.2.2: the other half of delay's "Must not occur with ... when the attribute 'target' has the value "_internal"" constraint - a <send> carrying delay or delayexpr alongside a literal target="_internal". Detectable only when target is a literal attribute; with targetexpr the value is not known until execute time, so this check stays silent there rather than guessing (Statifier.Validator.Checks.Send's moduledoc). Reported at the <send> element's own location.

Spec 6.2.2: "event ... Must not occur with 'eventexpr'." A <send> carrying both. Reported at the <send> element's own location.

Spec 6.2.2: "id ... Must not occur with 'idlocation'." A <send> carrying both. Reported at the <send> element's own location.

Spec 6.2.3: "A conformant document MUST NOT specify 'namelist' or <param> with <content>." The namelist half of that constraint; the <param> half is send_param_and_content/1. A <send> carrying a written namelist alongside a <content> child. Reported at the <send> element's own location.

Spec 6.2.3: the other half of "A conformant document MUST NOT specify 'namelist' or <param> with <content>" - a <send> carrying a <param> child alongside a <content> child. Reported at the <send> element's own location.

Spec 6.2.2: "target ... Must not occur with 'targetexpr'." A <send> carrying both. Reported at the <send> element's own location.

Spec 6.2.2: "type ... Must not occur with 'typeexpr'." A <send> carrying both. Reported at the <send> element's own location.

Spec 3.5: a <transition type="..."> value that is neither "internal" nor "external". raw is the source text as written (Location.slice/2) - lowering maps any out-of-range value onto the :external default, so the atom alone cannot tell type="external" from type="sideways".

Checks 4 and 5 (spec 3.6, 3.10): owner's content model is exactly one <transition>, and it holds count instead. Fires for both zero (nothing to default into) and two-or-more (which one wins is undefined).

Checks 4 and 5 (spec 3.6, 3.10): owner's one transition carries attr (:event or :cond), which neither an <initial> nor a <history> default transition may specify.

Checks 4 and 5 (spec 3.6, 3.10): owner's one transition carries no usable target - target="" and an absent target are the same failure here.

Check 3 (spec 3.3, 3.6): an initial attribute or Document.initial names id, and no state in the document carries that id. An <initial> element's own transition targets are never reported by this constructor - check 2 already owns their existence.

Check 2 (spec 3.5): a <transition>'s target names id, and no state in the document carries that id. Reported once per unresolved id, at the transition's own target span (or the transition's, when unwritten).

Types

owner()

@type owner() :: {:initial, String.t() | nil} | {:history, String.t() | nil}

Which slot a shared transition-shape violation (:transition_count, :transition_missing_target, :transition_forbidden_attribute) came from: a state's <initial> element, or a :history state's own default transition. The id is nil exactly when the owning state's own id is.

reason()

@type reason() ::
  {:duplicate_id, id :: binary()}
  | {:empty_id}
  | {:unresolved_target, id :: binary()}
  | {:unresolved_initial, id :: binary()}
  | {:initial_not_descendant, id :: binary(), parent_id :: binary()}
  | {:initial_on_atomic_state, id :: binary()}
  | {:initial_attribute_and_element, id :: binary()}
  | {:transition_count, owner :: owner(), count :: non_neg_integer()}
  | {:transition_missing_target, owner :: owner()}
  | {:transition_forbidden_attribute, owner :: owner(), attr :: :event | :cond}
  | {:history_bad_parent, id :: binary(), parent_kind :: atom()}
  | {:history_bad_type, raw :: binary()}
  | {:transition_bad_type, raw :: binary()}
  | {:scxml_bad_binding, raw :: binary()}
  | {:final_has_states, id :: binary()}
  | {:final_has_transitions, id :: binary() | nil}
  | {:final_parent_missing_id, final_id :: binary() | nil}
  | {:default_entry_not_enterable, id :: binary(), child_kind :: atom()}
  | {:donedata_not_on_final, id :: binary()}
  | {:donedata_content_and_params, id :: binary() | nil}
  | {:content_expr_and_text, expr :: binary()}
  | {:param_expr_and_location, name :: binary()}
  | {:param_no_value, name :: binary()}
  | {:bad_namespace, uri :: binary() | nil}
  | {:bad_version, version :: binary() | nil}
  | {:scxml_bad_datamodel, raw :: binary()}
  | {:data_expr_and_src, id :: binary()}
  | {:data_value_and_children, id :: binary()}
  | {:data_reserved_id, id :: binary()}
  | {:datamodel_bad_parent, kind :: atom()}
  | {:assign_expr_and_text, expr :: binary()}
  | {:if_elseif_after_else}
  | {:if_duplicate_else}
  | {:script_no_src_or_text}
  | {:invoke_type_and_typeexpr}
  | {:invoke_src_and_srcexpr}
  | {:invoke_src_and_content}
  | {:invoke_id_and_idlocation}
  | {:invoke_namelist_and_param}
  | {:invoke_bad_autoforward, raw :: binary()}
  | {:send_event_and_eventexpr}
  | {:send_target_and_targetexpr}
  | {:send_type_and_typeexpr}
  | {:send_id_and_idlocation}
  | {:send_delay_and_delayexpr}
  | {:send_delay_and_internal_target}
  | {:send_namelist_and_content}
  | {:send_param_and_content}
  | {:cancel_sendid_and_sendidexpr}
  | {:cancel_no_sendid}

t()

@type t() :: %Statifier.Validator.Error{
  location: Statifier.Parser.Location.t(),
  message: binary(),
  reason: reason()
}

Functions

assign_expr_and_text(expr, location)

@spec assign_expr_and_text(
  expr :: binary(),
  location :: Statifier.Parser.Location.t()
) :: t()

Spec 5.4.2: an <assign> element carries both an expr attribute and non-blank child content - "A conformant SCXML document MUST specify either 'expr' or children of <assign>, but not both." expr is the attribute's value as lowered, so the message quotes the expression rather than the text, which has no span of its own. Reported at the <assign>'s own element span (Statifier.Document.Assign.node_location, not the SCXML location attribute that struct also carries - Statifier.Document.Assign's moduledoc names the two spans apart).

bad_namespace(uri, location)

@spec bad_namespace(uri :: binary() | nil, location :: Statifier.Parser.Location.t()) ::
  t()

Check 9 (spec 3.2.1): the root element's resolved namespace, document.namespace, is not the SCXML namespace. uri is document.namespace itself - nil for a fragment that declares no namespace at all, the boilerplate-free case that relaxed parsing left representable.

bad_version(version, location)

@spec bad_version(
  version :: binary() | nil,
  location :: Statifier.Parser.Location.t()
) :: t()

Check 10 (spec 3.2.1): the root element's version is not "1.0". version is nil when the attribute is absent.

cancel_no_sendid(location)

@spec cancel_no_sendid(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.3.1: "A conformant SCXML document MUST specify exactly one of sendid or sendidexpr." A <cancel> carrying neither. Reported at the <cancel> element's own location.

cancel_sendid_and_sendidexpr(location)

@spec cancel_sendid_and_sendidexpr(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.3.1: "sendid ... Must not occur with sendidexpr." A <cancel> carrying both. Reported at the <cancel> element's own location.

code(reason)

@spec code(reason :: reason()) :: atom()

The reason tuple's tag - the stable error code.

content_expr_and_text(expr, location, payload)

@spec content_expr_and_text(
  expr :: binary(),
  location :: Statifier.Parser.Location.t(),
  payload :: :text | :markup
) :: t()

Spec 5.6: a <content> element carries both an expr attribute and inline content, whether that content is text or markup (ADR-0041) - the spec's "MUST NOT specify both" that lib/statifier/document/content.ex deliberately leaves representable so this check can report it. expr is the attribute's value as lowered, and payload names which inline form was actually present, :text or :markup - markup wins when both are somehow present, since it is the more specific payload. The reason tuple stays {:content_expr_and_text, expr} regardless of payload; only the message varies. For :text, the message quotes the expression rather than the text, which has no span of its own (Statifier.Document.Content's moduledoc); for :markup, the message quotes the expression and names markup by kind, since markup DOES have a span of its own (markup_location) even though this diagnostic does not use it. Reported at the <content> element's own location, the only span covering both halves of the conflict.

data_expr_and_src(id, location)

@spec data_expr_and_src(id :: binary(), location :: Statifier.Parser.Location.t()) ::
  t()

Check 13 (spec 5.3.2): a <data> carries both an expr and a src attribute - "MAY have either a 'src' or an 'expr' attribute, but MUST NOT have both". id is the offending <data>'s own id.

data_reserved_id(id, location)

@spec data_reserved_id(id :: binary(), location :: Statifier.Parser.Location.t()) ::
  t()

Check 13 (spec 5.10): a <data id="..."> begins with _ - "A conformant SCXML document MUST NOT contain ids beginning with '_' in the <data> element".

data_value_and_children(id, location)

@spec data_value_and_children(
  id :: binary(),
  location :: Statifier.Parser.Location.t()
) :: t()

Check 13 (spec 5.3.2): a <data> carries an expr or src attribute and non-blank child text - "if either attribute is present, the element MUST NOT have any children". Whitespace-only text does not count (Checks.Data.blank?/1).

datamodel_bad_parent(kind, location)

@spec datamodel_bad_parent(kind :: atom(), location :: Statifier.Parser.Location.t()) ::
  t()

Check 13 (spec 3.2.2 / 3.3.2 / 3.4.2): a <datamodel> sits on a :final or :history state - legal only under <scxml>, <state>, and <parallel>. kind is the offending state's own kind.

default_entry_not_enterable(id, child_kind, location)

@spec default_entry_not_enterable(
  id :: binary() | nil,
  child_kind :: atom(),
  location :: Statifier.Parser.Location.t()
) :: t()

Check 7 (spec 3.3): id names a compound state with no initial attribute and no <initial> element, whose first child in document order has child_kind - a :history pseudo-state cannot be entered by the spec 3.3 default-entry fallback. Reported at the first child's own location.

donedata_content_and_params(id, location)

@spec donedata_content_and_params(
  id :: binary() | nil,
  location :: Statifier.Parser.Location.t()
) ::
  t()

Spec 5.5: a <donedata> carries both a <content> child and one or more <param> children - "either a single <content> element or one or more <param> elements as children of <donedata>, but not both". id is the owning :final state's own id (nil when unwritten, the same shape final_parent_missing_id/2 allows). Reported at the <donedata>'s own location, the only span covering both halves of the conflict.

donedata_not_on_final(id, location)

@spec donedata_not_on_final(
  id :: binary() | nil,
  location :: Statifier.Parser.Location.t()
) :: t()

Check 8 (spec 3.7, 5.7): id names a non-:final state carrying a non-nil donedata - <donedata> is only legal on :final. Reported at the donedata's own location.

duplicate_id(id, location)

@spec duplicate_id(id :: binary(), location :: Statifier.Parser.Location.t()) :: t()

Check 1 (spec 3.14): id names more than one state in the document. The location is the offending (non-first) occurrence's own span, never the canonical one.

empty_id(location)

@spec empty_id(location :: Statifier.Parser.Location.t()) :: t()

Check 1 (spec 3.14): a state was written with id="". id is typed as an XML Schema ID, whose lexical space is an XML Name and therefore excludes the empty string, so an empty id is not a name a transition could ever target.

This is a different case from an absent id, which is legal and reported by nothing (lib/statifier/document/state.ex): the two are told apart by the attribute_locations[:id] key, never by the value alone. The reason carries no payload - the offending value is the empty string, and the location is the span the author wrote it at.

final_has_states(id, location)

@spec final_has_states(
  id :: binary() | nil,
  location :: Statifier.Parser.Location.t()
) :: t()

Check 6 (spec 3.7): id names a state child of a :final - only onentry, onexit, and donedata are legal there. Reported once per offending child, at the child's own location (not the <final>'s), so the caret lands on the element that should not be there.

final_has_transitions(id, location)

@spec final_has_transitions(
  id :: binary() | nil,
  location :: Statifier.Parser.Location.t()
) :: t()

Check 6 (spec 3.7): id names a :final carrying a <transition>. A <final> is where a region stops, so it has no outgoing transitions to take - spec 3.7's content model is onentry, onexit, and donedata only. Reported once per offending transition, at the transition's own location rather than the <final>'s, matching final_has_states/2.

final_parent_missing_id(final_id, location)

@spec final_parent_missing_id(
  final_id :: binary() | nil,
  location :: Statifier.Parser.Location.t()
) ::
  t()

Check 12 (spec 3.7, Appendix D enterStates): a <final> state's parent carries no id. Entering a non-top-level <final> raises done.state.{parent.id}, so a parent with no written id has a completion event the spec names after a name it does not have - and no transition could match "done.state." even if it were raised. final_id is the offending <final> child's own id, which names the completion the document loses; it rides in reason and stays out of message, because it is itself optionally nil and interpolating that reads as a bug ("containing <final> nil") in the one case the check exists for. The location is the parent's span, since the parent is the element that has to change.

history_bad_parent(id, parent_kind, location)

@spec history_bad_parent(
  id :: binary() | nil,
  parent_kind :: atom(),
  location :: Statifier.Parser.Location.t()
) :: t()

Check 5 (spec 3.10): a :history state id is a child of parent_kind, which is not a compound <state> or <parallel> - the only two legal parents for a history pseudo-state. parent_kind is :scxml for the document root itself.

history_bad_type(raw, location)

@spec history_bad_type(raw :: binary(), location :: Statifier.Parser.Location.t()) ::
  t()

Check 5 (spec 3.10): a <history type="..."> value that is neither "shallow" nor "deep". raw is the source text as written (Location.slice/2), so the message quotes what the author actually typed rather than the default it silently lowered to.

if_duplicate_else(location)

@spec if_duplicate_else(location :: Statifier.Parser.Location.t()) :: t()

Spec 4.3.2: an <if> with more than one <else> branch (each an attribute-less, cond-less partition per Statifier.Document.If.Branch). 4.3 caps <else> at "0 or 1 times", so a second one is malformed regardless of position. Reported at the second <else>'s own location, never the first's.

if_elseif_after_else(location)

@spec if_elseif_after_else(location :: Statifier.Parser.Location.t()) :: t()

Spec 4.3.2: "In a conformant SCXML document, <else> MUST occur after all <elseif> tags." An <elseif> branch that follows the <if>'s <else> branch violates that ordering. Reported at the offending <elseif>'s own location (Statifier.Document.If.Branch.location), not the enclosing <if>'s.

initial_attribute_and_element(id, location)

@spec initial_attribute_and_element(
  id :: binary() | nil,
  location :: Statifier.Parser.Location.t()
) ::
  t()

Check 4 (spec 3.3, 3.6): a state carries both an initial attribute and an <initial> element - at most one is legal. Reported at the <initial> element's own span.

initial_not_descendant(id, parent_id, location)

@spec initial_not_descendant(
  id :: binary(),
  parent_id :: binary(),
  location :: Statifier.Parser.Location.t()
) :: t()

Check 3 (spec 3.3, 3.6): a resolved initial target id is not a descendant of the state it initializes, parent_id - ancestry, not direct-child membership (v1's bug).

initial_on_atomic_state(id, location)

@spec initial_on_atomic_state(
  id :: binary(),
  location :: Statifier.Parser.Location.t()
) :: t()

Check 3 (spec 3.3): id names a state carrying an initial attribute or <initial> element, and the spec MUST NOT: an atomic state (no states children, or a :parallel, :final, or :history kind) has nothing to default into. Fires ahead of, and suppresses, the descendancy check for the same state.

invoke_bad_autoforward(raw, location)

@spec invoke_bad_autoforward(
  raw :: binary(),
  location :: Statifier.Parser.Location.t()
) :: t()

Spec 6.4.1: an <invoke autoforward="..."> value that is neither "true" nor "false". Same slice-back substrate as transition_bad_type/2 and Statifier.Validator.Checks.Enums's other three enumerated attributes - Statifier.Lowering.Attributes.atom/4 maps an unrecognised value onto the false default, so the atom alone cannot tell autoforward="false" from autoforward="sideways". raw is the source text as written (Location.slice/2).

invoke_id_and_idlocation(location)

@spec invoke_id_and_idlocation(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.4.1: "id: Must not occur with the 'idlocation' attribute." An <invoke> carrying both. Reported at the <invoke> element's own location.

invoke_namelist_and_param(location)

@spec invoke_namelist_and_param(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.4.1: "namelist: Must not occur with the <param> element." An <invoke> carrying a written namelist alongside one or more <param> children. Reported at the <invoke> element's own location.

invoke_src_and_content(location)

@spec invoke_src_and_content(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.4.1: the other half of src's "Must not occur with ... the <content> element" constraint - an <invoke> carrying a <content> child alongside src or srcexpr. Reported at the <invoke> element's own location.

invoke_src_and_srcexpr(location)

@spec invoke_src_and_srcexpr(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.4.1: "src: Must not occur with the 'srcexpr' attribute or the <content> element." The src/srcexpr half of that constraint; the <content> half is invoke_src_and_content/1. An <invoke> carrying both src and srcexpr. Reported at the <invoke> element's own location.

invoke_type_and_typeexpr(location)

@spec invoke_type_and_typeexpr(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.4.1: "type: Must not occur with the 'typeexpr' attribute." An <invoke> carrying both. Reported at the <invoke> element's own location - the reason carries no payload, matching {:if_duplicate_else} and {:script_no_src_or_text}'s own nullary shape for a violation with no natural id to name it by.

param_expr_and_location(name, location)

@spec param_expr_and_location(
  name :: binary(),
  location :: Statifier.Parser.Location.t()
) :: t()

Spec 5.7: a <param> element carries both an expr attribute and a location attribute - "A conformant SCXML document MUST specify either the 'expr' attribute of <param> or the 'location' attribute, but MUST NOT specify both." name is the offending <param>'s own name attribute. Reported at the <param> element's own location.

param_no_value(name, location)

@spec param_no_value(name :: binary(), location :: Statifier.Parser.Location.t()) ::
  t()

Spec 5.7: a <param> element carries neither an expr attribute nor a location attribute - the same "MUST specify either ... but MUST NOT specify both" rule as param_expr_and_location/2, its other half. name is the offending <param>'s own name attribute. Reported at the <param> element's own location.

script_no_src_or_text(location)

@spec script_no_src_or_text(location :: Statifier.Parser.Location.t()) :: t()

Spec 5.8.2: "A conformant SCXML document MUST specify either the 'src' attribute or child content, but not both." src is unreachable in a struct lowering ever builds (Statifier.Lowering.Builders.build_script/2 already refuses to build one when src is written - ADR-0026 decision 2), so the only way this document-layer check can still observe the MUST's violation is the other missing half: a <script> with neither. The reason carries no payload - a <script> has no id and no attribute-value to name it by, matching {:empty_id} and {:if_duplicate_else}'s own nullary shape.

scxml_bad_binding(raw, location)

@spec scxml_bad_binding(raw :: binary(), location :: Statifier.Parser.Location.t()) ::
  t()

Spec 3.2.1: an <scxml binding="..."> value that is neither "early" nor "late". Same slice-back substrate as transition_bad_type/2, against the :early default.

scxml_bad_datamodel(raw, location)

@spec scxml_bad_datamodel(raw :: binary(), location :: Statifier.Parser.Location.t()) ::
  t()

Check 13 (spec 3.2.1): a datamodel="..." value outside ["predicator", "elixir", "null", "ecmascript", "xpath"] - the spec's own three named values ("null", "ecmascript", "xpath") plus this platform's two ("predicator", its default; "elixir", an alias for v1-converted documents). Accepting "ecmascript" and "xpath" does not claim this engine implements either datamodel - spec 3.2.1's Valid Values clause names "other platform-defined values" as legal too, and Appendix B's intro states no rule for what a processor does with a datamodel it does not implement. What this check actually catches is a typo (datamodel="predicater", datamodel="javascript"); which datamodel this engine runs is docs/datamodel.md's answer, not this attribute's. raw is the source text as written (Location.slice/2), the same substrate as scxml_bad_binding/2.

send_delay_and_delayexpr(location)

@spec send_delay_and_delayexpr(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.2.2: "delay ... Must not occur with 'delayexpr' or when the attribute 'target' has the value "_internal"." The delay/delayexpr half of that constraint; the target="_internal" half is send_delay_and_internal_target/1. A <send> carrying both delay and delayexpr. Reported at the <send> element's own location.

send_delay_and_internal_target(location)

@spec send_delay_and_internal_target(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.2.2: the other half of delay's "Must not occur with ... when the attribute 'target' has the value "_internal"" constraint - a <send> carrying delay or delayexpr alongside a literal target="_internal". Detectable only when target is a literal attribute; with targetexpr the value is not known until execute time, so this check stays silent there rather than guessing (Statifier.Validator.Checks.Send's moduledoc). Reported at the <send> element's own location.

send_event_and_eventexpr(location)

@spec send_event_and_eventexpr(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.2.2: "event ... Must not occur with 'eventexpr'." A <send> carrying both. Reported at the <send> element's own location.

send_id_and_idlocation(location)

@spec send_id_and_idlocation(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.2.2: "id ... Must not occur with 'idlocation'." A <send> carrying both. Reported at the <send> element's own location.

send_namelist_and_content(location)

@spec send_namelist_and_content(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.2.3: "A conformant document MUST NOT specify 'namelist' or <param> with <content>." The namelist half of that constraint; the <param> half is send_param_and_content/1. A <send> carrying a written namelist alongside a <content> child. Reported at the <send> element's own location.

send_param_and_content(location)

@spec send_param_and_content(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.2.3: the other half of "A conformant document MUST NOT specify 'namelist' or <param> with <content>" - a <send> carrying a <param> child alongside a <content> child. Reported at the <send> element's own location.

send_target_and_targetexpr(location)

@spec send_target_and_targetexpr(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.2.2: "target ... Must not occur with 'targetexpr'." A <send> carrying both. Reported at the <send> element's own location.

send_type_and_typeexpr(location)

@spec send_type_and_typeexpr(location :: Statifier.Parser.Location.t()) :: t()

Spec 6.2.2: "type ... Must not occur with 'typeexpr'." A <send> carrying both. Reported at the <send> element's own location.

transition_bad_type(raw, location)

@spec transition_bad_type(raw :: binary(), location :: Statifier.Parser.Location.t()) ::
  t()

Spec 3.5: a <transition type="..."> value that is neither "internal" nor "external". raw is the source text as written (Location.slice/2) - lowering maps any out-of-range value onto the :external default, so the atom alone cannot tell type="external" from type="sideways".

transition_count(owner, count, location)

@spec transition_count(
  owner :: owner(),
  count :: non_neg_integer(),
  location :: Statifier.Parser.Location.t()
) :: t()

Checks 4 and 5 (spec 3.6, 3.10): owner's content model is exactly one <transition>, and it holds count instead. Fires for both zero (nothing to default into) and two-or-more (which one wins is undefined).

transition_forbidden_attribute(owner, attr, location)

@spec transition_forbidden_attribute(
  owner :: owner(),
  attr :: :event | :cond,
  location :: Statifier.Parser.Location.t()
) :: t()

Checks 4 and 5 (spec 3.6, 3.10): owner's one transition carries attr (:event or :cond), which neither an <initial> nor a <history> default transition may specify.

transition_missing_target(owner, location)

@spec transition_missing_target(
  owner :: owner(),
  location :: Statifier.Parser.Location.t()
) :: t()

Checks 4 and 5 (spec 3.6, 3.10): owner's one transition carries no usable target - target="" and an absent target are the same failure here.

unresolved_initial(id, location)

@spec unresolved_initial(id :: binary(), location :: Statifier.Parser.Location.t()) ::
  t()

Check 3 (spec 3.3, 3.6): an initial attribute or Document.initial names id, and no state in the document carries that id. An <initial> element's own transition targets are never reported by this constructor - check 2 already owns their existence.

unresolved_target(id, location)

@spec unresolved_target(id :: binary(), location :: Statifier.Parser.Location.t()) ::
  t()

Check 2 (spec 3.5): a <transition>'s target names id, and no state in the document carries that id. Reported once per unresolved id, at the transition's own target span (or the transition's, when unwritten).