StatifierBlocks.Provenance (StatifierBlocks v0.1.0)

Copy Markdown View Source

Which generated element came from which block (ADR-0004 decision 5).

The compile runs one way and nothing decompiles a chart back into blocks. What has to survive the trip is correspondence, and this is it.

Keyed two ways, because it answers two different questions

Runtime highlighting starts from a running session, whose active configuration is a list of state ids (Statifier.active_leaf_states/1). That is by_state_id, and StatifierBlocks.Compiler.StateId's totality is what makes it complete.

Error routing starts from an upstream finding, and upstream findings carry no element reference at all: every diagnostic in statifier's pipeline is {reason, message, location} where location is a %Statifier.Parser.Location{}, and the offending ids ride inside the closed reason tuple as data. So the second key is a byte span over the generated SCXML, and mapping a finding is owner_at/2 on location.start_offset.

A span key is strictly more general than an id-based one. It routes findings about transitions, which have no id of their own in SCXML, and findings whose reason tuple carries no id at all - and because the compiler records the span of every attribute value as well as every element, it routes a bad expression to the config field the author typed it into.

The map is total over the emission

Every span the compiler emits has an owner; there is no unowned scaffolding. Chart-level elements belonging to no particular block - the <scxml> element itself - are attributed to the root block, which ADR-0001 decision 1 guarantees exists. Totality is what makes decision 9's mapping a total function rather than one with an :unmapped arm every consumer must handle and none can act on. An unmapped offset inside the generated bytes is a compiler bug, and owner_at/2 still returns an error tuple for it rather than raising, because a caller may hand it an offset from a different chart.

Serialization

to_json/1 writes the map under ADR-0001 decision 8's canonical rules, so a host can store it beside the SCXML, diff it, and read it from non-Elixir tooling. It is not part of the document and carries no schema_version of its own; it is versioned by the compilation record's compiler version.

Summary

Types

role is nil for the block's own state and a role name for one it minted (ADR-0004 decision 3). config_key is set when the span was emitted verbatim from that config field, which is what makes a finding the author's rather than a bug in this package.

Byte offsets into the generated SCXML. Exclusive end, as upstream.

t()

Functions

Reads back what to_json/1 wrote.

Builds an owner. opts carries :role and :config_key, both nil by default.

The owner of the innermost span containing offset.

The owner of the generated state state_id, for mapping a running session's Statifier.active_leaf_states/1 back onto blocks.

The blocks a running configuration is inside, in the order the configuration was given, with any state this map does not know dropped.

Canonical JSON, ADR-0001 decision 8's rules. Deterministic.

Types

owner()

@type owner() :: %{
  block_id: StatifierBlocks.Block.id(),
  role: String.t() | nil,
  config_key: String.t() | nil
}

role is nil for the block's own state and a role name for one it minted (ADR-0004 decision 3). config_key is set when the span was emitted verbatim from that config field, which is what makes a finding the author's rather than a bug in this package.

span()

@type span() :: {start_offset :: non_neg_integer(), end_offset :: non_neg_integer()}

Byte offsets into the generated SCXML. Exclusive end, as upstream.

t()

@type t() :: %StatifierBlocks.Provenance{
  by_state_id: %{optional(String.t()) => owner()},
  spans: [{span(), owner()}]
}

Functions

from_json(json)

@spec from_json(binary()) :: {:ok, t()} | {:error, term()}

Reads back what to_json/1 wrote.

owner(block_id, opts \\ [])

@spec owner(
  StatifierBlocks.Block.id(),
  keyword()
) :: owner()

Builds an owner. opts carries :role and :config_key, both nil by default.

owner_at(provenance, offset)

@spec owner_at(t(), non_neg_integer()) ::
  {:ok, owner()} | {:error, {:unmapped_offset, non_neg_integer()}}

The owner of the innermost span containing offset.

Innermost is by width: spans nest, so the narrowest span containing an offset is unique, and an attribute value's span is always strictly inside its element's.

owner_of_state(provenance, state_id)

@spec owner_of_state(t(), String.t()) :: {:ok, owner()} | :error

The owner of the generated state state_id, for mapping a running session's Statifier.active_leaf_states/1 back onto blocks.

owners_of_states(provenance, state_ids)

@spec owners_of_states(t(), [String.t()]) :: [owner()]

The blocks a running configuration is inside, in the order the configuration was given, with any state this map does not know dropped.

The drop is deliberate rather than lenient: a state id this map has never heard of is a state from a different chart, and a highlighting caller wants the blocks it can highlight rather than an error over the whole list.

to_json(provenance)

@spec to_json(t()) :: binary()

Canonical JSON, ADR-0001 decision 8's rules. Deterministic.