The Chart stage: run the generated SCXML through statifier's own pipeline and route every finding back to a block (ADR-0004 decisions 9 and 10).
This package ships no reachability analysis, no transition-target
check, no id-uniqueness check and no expression well-formedness check.
Statifier's validator already runs those against a conformance corpus
with a regression ratchet behind it (st-ADR-0006), and a second semantic
validator written against block documents would be a second
implementation of the same rules that must agree with the first - every
disagreement surfacing as a document the editor accepts and the engine
then rejects. So: generate the SCXML, run it through
Statifier.compile/2, and map the findings back through provenance.
Adding a chart-semantic check here is a change to ADR-0004 decision 9.
How the routing works
Upstream findings carry no element reference. Every diagnostic in the
pipeline - Statifier.Parser.ParseError, Statifier.Lowering.Error,
Statifier.Validator.Error, Statifier.Validator.Warning,
Statifier.Compiler.Error - is {reason, message, location}, where
location is a %Statifier.Parser.Location{} over the source bytes.
So the route is location.start_offset through
StatifierBlocks.Provenance.owner_at/2, and decision 5's totality is
what makes it a total function rather than one with an :unmapped arm.
Upstream's message survives verbatim, and its reason tag becomes the
finding's code. Upstream's document-order sort survives as document
order over blocks, which StatifierBlocks.Compiler applies to every
stage's findings alike.
The fault split (decision 9)
Two kinds of chart-stage finding, distinguished by exactly one thing - whether the owning span carries a config key:
- Structural findings are bugs in this package or in a host's block
type, never the author's doing.
{:unresolved_target, id},{:initial_not_descendant, id, parent}, a malformed namespace: an author cannot express any of these, because the block vocabulary has no way to name them. Their owning span hasconfig_key: nil, andfault: :packageis the actionable part - "this cannot be fixed here" is the only honest message. - Content findings are the author's, and they carry a config key.
An
:expressionconfig field is a predicator source string passed verbatim into acond; if it does not parse, upstream returns{:expression_compile_error, owner_ref, source, parse_error}located at the attribute value - a span this package recorded with the config key the value came from. That is squarely the author's typo, and the config key is what lets an editor put the error on the field they typed into rather than on the block as a whole.
An upstream error of either kind fails the compile; a warning does
not, and rides on StatifierBlocks.Compiled.
The sub-expression span (decision 9's last refinement)
A content finding routes to a field. Decision 9 goes one step further:
the predicator parse error carries a span within the expression
string, so an editor can underline the offending sub-expression inside
the field rather than the whole field. That span is composed here and
lands on the finding as config_value_span.
The composition is three steps, and the middle one is upstream's:
- Upstream reports the failure against the attribute value's span
in the generated SCXML (
Statifier.Compiler.Error'slocation), and carries predicator's own{line, column}span over the expression string it compiled. Statifier.Parser.Location.resolve_span/4composes the two into an absolute span over the generated document, walking the raw bytes and the entity-expanded string in lockstep so a>earlier in the expression cannot shift the answer.- That absolute span is run backwards through the same correspondence the provenance map is built from: subtract the attribute value's own start, and unescape the prefix, which turns generated-document bytes back into bytes of the value the author typed.
Step 3's unescape is not decoration. A cond is emitted through
StatifierBlocks.Compiler.Serializer's XML escaping, so the canonical
amount > > 5000 reaches the document as amount > > 5000, where
the offending second > sits at byte 12 rather than at byte 9 - one
entity reference ahead of it is already enough to move it. The offsets
this field carries are into the author's value, so they are the ones an
editor can use without knowing that a serializer exists.
When a finding gets one
All four have to hold, and the field is nil otherwise:
- the finding's owning span carries a config key - the same test
the fault split above turns on, and the reason a canonicalised
attribute (
core.wait'sdelay, whose bytes the block type rebuilt rather than passed through) is never annotated and so never offered an in-value offset; - the diagnostic is an
:expression_compile_error, the only upstream reason that carries an expression string at all; - predicator supplied a span for the failure (
ParseError's:spanis optional, andnilon an error built throughnew/3); - the resolved span lies inside the attribute value's own span - the guard that keeps a degraded resolution honest rather than turning it into an offset into somebody else's bytes.
resolve_span/4 degrades rather than raising: an expression whose
expanded text does not describe the raw slice resolves to the value's
whole span, which arrives here as "underline the entire field" - the
same answer a consumer would have reached with no span at all, which is
why it is passed through rather than discarded.
Why "carries a config key" is enough
The first bullet reads "carries a config key", where decision 9's
annotation rule means "was written verbatim from that config value".
Those coincide because the annotation is only ever left on a verbatim
value: a block type that composes an attribute leaves it
unannotated, so its bytes have no config key and a finding inside them
is the package's. core.subchart was the one exception - it annotated
a cond it builds from an outcome name with the config key
outcomes - and that annotation was dropped rather than this
criterion widened.
Summary
Functions
Compiles scxml upstream and maps what comes back.
Functions
@spec validate(binary(), StatifierBlocks.Provenance.t(), StatifierBlocks.Document.t()) :: {:ok, [StatifierBlocks.Compiler.Finding.t()]} | {:error, [StatifierBlocks.Compiler.Finding.t()]}
Compiles scxml upstream and maps what comes back.
{:ok, warnings} when the chart is valid - upstream's warnings
(st-ADR-0033 made Machine.warnings/1 their only surfacing seam),
mapped the same way errors are. {:error, findings} otherwise.