The direct self-reference refusal: a document may not run itself
(ADR-0004's 2026-08-29 amendment on what core.subchart's src
resolves against).
A subchart names another document by its document id and the host's handler resolves that id to a chart at run time. When the id it names is the id of the document the block sits in, the run the handler starts is a fresh session of the same chart, which starts the same subchart, which starts another session. Nothing downstream can tell that apart from work the author meant to do, so the compiler refuses it here, where the two ids are both in hand and no session has been spent.
The criterion, and why it is not a block-type name
The pass never asks which block type emitted anything, for
StatifierBlocks.Compiler.SensitivePaths' reason: a hard-coded list of
core.* types would have to be edited every time the vocabulary grew,
and a type added without that edit would recurse silently. It walks the
emission and classifies by SCXML's own semantics: src on
<invoke> is the attribute that names the resource an invocation runs,
so an <invoke> whose src is this document's id is a document
invoking itself, whichever type wrote it.
srcexpr is deliberately not checked. It names a source computed at run
time out of the datamodel, so there is no id here to compare against the
document's, and refusing an expression because the document id appears
somewhere inside it would refuse charts that are correct.
Cross-document cycles are host-side, and named rather than built
A -> B -> A is the same defect one document further out, and this
package cannot see it. emit/2 is a pure function of one block and its
context (ADR-0004 decision 4); a compile is handed one document and a
palette, and neither carries another document. Deciding a cycle needs
the document graph - who references whom across the host's whole
library - which only the host has. So the host's resolver owns that
check, under the same st-ADR-0051 registry key that resolves a document
id to a chart, and this module owns exactly the half a single compile
can decide. Refusing the case in reach is worth more than declining both
because one of them is out of reach.
Stage and fault
The finding carries the :emit stage, like every other refusal that
reads the assembled emission, and it adds no stage to decision 10's
table. It anchors on the offending block with config_key: "chart" -
the author's verbatim value, stamped onto the attribute by
StatifierBlocks.Emission.attribute_from_config/3 - so decision 9's
split reads it as fault: :author, which is right: a one-field edit
fixes it.
Severity is :error. A warning would let the document through and
publish the recursion it exists to prevent.
Presenting one
StatifierBlocks.Finding.from_compiler/2's default derivation cannot
reach :lint for this finding, for the reason SensitivePaths records:
its rule 2 maps only a non-error to :lint, and an :emit stage at
:error severity maps to :compile under ADR-0005 amendment 11h, so
a bare adaptation presents it as a compile error rather than as the lint
it is. That is what opts[:source] exists for, and a caller adapting
these for the editor passes it:
{presentation, []} =
StatifierBlocks.Finding.from_compiler_all(findings, source: :lint)Widening the default derivation would mean switching on code, which
StatifierBlocks.Finding forbids by construction; the seam is named
here rather than bent, exactly as the sensitive-path mapping names it.
Summary
Functions
Every self-reference emission earns against document_id, in the
order the emission is walked.
Functions
@spec check(StatifierBlocks.Emission.t(), StatifierBlocks.Document.id()) :: [ StatifierBlocks.Compiler.Finding.t() ]
Every self-reference emission earns against document_id, in the
order the emission is walked.
Returns [] when nothing invokes the document itself, which is the
overwhelming majority of compiles.