ADR-0001: Riddler is one package: a dynamic content runtime that consumes the statifier family, and the content document is its contract
Copy Markdown View SourceStatus: accepted
Context
Riddler is a dynamic content runtime: a host authors content as JSON documents -
screens now; emails, images and feature flags forthcoming - and Riddler resolves
each against a visitor's context; the host renders, sends or serves what comes
back. Something has to decide what such a declaration means. The statifier
family already decides a different question - a chart decides what happens next,
and statifier-ex, statifier_blocks and their siblings own that. The two meet
in a host application, where a workflow reaches a screen and a screen has to be
resolved into the nodes a visitor sees.
Which way that arrow points is the question this record settles, and it has to
be settled before any module exists, because the answer decides what may appear
in mix.exs. A dependency added for convenience is very hard to remove once a
second package relies on the coupling it creates.
Two pieces of public evidence bound the answer. The fixture
priv/fixtures/signup_screens.json in statifier_examples (read at 9d288c9)
is a screen document written by hand: three screens of a signup wizard, each
a list of nodes with keys, carrying a schema_version and a metadata block,
and explicitly not a block document - the chart that drives it is a separate
fixture beside it. It is the shape a host already wants to author. The findings
document docs/spikes/SF040-element-editor.md in statifier_blocks (added by
that repository's PR 465; present at cdfbc6c) weighs, against two spikes,
whether the block editor should emit that shape itself, and separates the cost
of the emit seam from the cost of the element vocabulary it emits. Both are
public; neither is a decision, and this record does not treat them as one.
Prior generations of this product exist in history. They are not evidence for anything decided here, and nothing below is justified by them.
Decision
Riddler consumes the statifier family; the family does not consume Riddler. A workflow engine calls into this runtime when it reaches a screen, and this runtime never calls into a workflow engine. Charts stay in statifier: nothing in this package interprets, executes or re-implements a chart, and a need to do so is a decision to record before it is code to write.
Riddler ships as one hex package, riddler. The content kinds, the
template subset and the shared machinery are modules in that package rather
than packages of their own. The package is split along one of its seams only
when a consumer needs that piece without the others; a seam that no consumer
has asked to use alone is a module boundary, not a package boundary.
The content document is the contract. It is JSON, it carries a
schema_version, and it is what both sides of the boundary agree on - not a
struct, not a function signature, not a wire format negotiated per host. A
change to what the document admits is a change to the contract and is breaking
for every host holding one. A screen document is this version's instance of
that contract.
A content kind is a document shape, a resolved shape, a registry and a corpus capability. Those four are what makes a kind a kind: the shape a host authors, the shape resolution returns, the registry that says what may appear inside the document and refuses what it does not know, and the corpus capabilities a second runtime is held to. A proposal that supplies fewer than all four is not a kind yet.
v1 ships exactly one kind, screens. Emails, images and feature flags are
forthcoming kinds, and each is decided by its own record before it is code. A
kind is added by a record, never by a commit.
Kinds share the template subset, conditions, containers, diagnostics and findings, and never depend on one another's documents. The shared machinery is written once and each kind uses it; no kind's document, registry or resolved shape is reachable from another kind's. A kind that needs to read another kind's document is a coupling to record, not a module to write.
Each kind exposes admit and validate over a raw document, and resolve over a
document and a context, as pure functions, plus at most one kind-specific
check. The screens kind's one check is validate_responses, which is what a
host calls before it accepts what a visitor submitted. A kind that wants a
second check is a record, because a kind with an open-ended API is not a
contract a second runtime can be held to.
This package is the reference implementation of that contract, and it resolves
server-side. Resolution happens here, against a host-supplied context;
rendering, delivery and serving happen in the host. Nothing in this package
renders a view, sends a message, serves a byte, stores anything, or assumes a
browser. For a kind whose resolved document is itself the deliverable - an SVG,
an email's text part - resolution is the last step this package takes, and what
the host does with the result is still the host's. A second runtime in another
language is held to this package's behavior, not the reverse.
The public runtime of the screens kind is pure functions over decoded data.
Riddler.Screens.Document.admit/1 and Riddler.Screens.Document.validate/1
say what the vocabulary admits and why a document is refused;
Riddler.Screens.resolve/2 resolves a document against a context, and
Riddler.Screens.resolve_screen/3 resolves one named screen of it;
Riddler.Screens.validate_responses/3, and its arity-4 form naming the button
that was pressed, say what a set of responses has to satisfy before a host
accepts it. They are pure: same inputs, same outputs, no process state, no I/O.
Surface beyond those functions is added by a record, not by a commit.
A kind's modules sit under the kind's own namespace, and the shared machinery
does not. This kind is Riddler.Screens; the forthcoming kinds are
Riddler.Emails, Riddler.Images and Riddler.Flags. Riddler.Template,
Riddler.Finding and the condition and container machinery are shared and
carry no kind in their names. The first release of this package named the
screens kind Riddler.Elements, after the nodes inside a screen rather than
after the kind; the rename to Riddler.Screens lands with 0.1.0 and no
Riddler.Elements name survives it.
A corpus capability is named <kind>.<function>. This version emits
screens.admit, screens.resolve and screens.validate_responses for the one
kind, and templates.render for the shared template subset; forthcoming kinds
emit emails.*, images.* and flags.* under the same rule. A capability
name is how a second runtime dispatches a case, so it is part of the contract
and changes only by a record.
The conformance corpus is authored in this repository and emitted into
riddler_spec. The cases live beside the code that has to satisfy them; a
mix task emits them, with provenance naming the commit they were emitted from,
and a drift check in this repository's CI fails when the emitted corpus and the
cases disagree. A corpus file is never edited by hand in riddler_spec: the
emitted copy is an artifact, and an artifact that can be edited is not a
conformance corpus.
The core never depends on the block editor or on any statifier package.
No statifier package appears in this repository's mix.exs or mix.lock. An
editor that authors content documents is served by a later adapter package that
depends on both sides; it is not served by a dependency here.
predicator and solid are the runtime dependencies, and they are the only
ones. predicator evaluates the conditions a document declares; solid
parses the template subset. A third runtime dependency is a decision to record.
A container's winner replaces the container in the resolved output. A resolved document is the same shape as the document it came from, with the containers gone: a host that can render a document can render a resolved one, and nothing downstream needs to know a container was ever there.
Consequences
Renderers, delivery, persistence, scheduling and the admin are the host's, or later packages'. A feature this package could plausibly grow - a renderer, a mailer, a store, a transport - is out until a record puts it in.
Two records follow from this one. ADR-0002 decides the screen document v1 - the
document of the one kind this version ships: the envelope, what a screen is,
the node vocabulary, keys, conditions, writes, outcomes, the variant
container, and what a resolved screen is. ADR-0003 decides the template subset:
the allowlist, compile-time refusal, the output mode, and the render modes.
This record delegates every enumeration to them and to the tests of the code
halves that implement them; it enumerates nothing itself.
A further record, ADR-0004, decides the shared content machinery - what conditions, containers, diagnostics and findings mean for every kind rather than for screens alone - and it precedes the first non-screen kind's record. Until it exists those rules live in ADR-0002, where they were first written, and a kind added before ADR-0004 would be reading a screens record for shared rules, which is the reason ADR-0004 comes first.
Nothing here decides transport, authentication, streaming, the identity or durability of a visitor's execution, or the editor. Those remain open, and a commit that assumes an answer to one of them is out of scope until a record closes it.
The vocabulary is fixed by this record for everything downstream of it: a
document declares writes, a control names an outcome, what a visitor
submits is responses, and the host-supplied root is context. The spellings
payload, action and answers are named here once, to state that they are
not used.
Typespecs and worked example
This record names no types; it asserts rules about a package boundary and a contract, and the typespecs section this family's records carry does not apply to it. ADR-0002 and ADR-0003 carry the typespecs for the screen document and the template subset.
One sentence needs illustrating - that the content document, not a struct, is
the contract. The public fixture statifier_examples
priv/fixtures/signup_screens.json (at 9d288c9), in outline with its
metadata block and its node lists elided, and with the kind ADR-0002 adds
to the envelope written out rather than defaulted, is:
{
"schema_version": 1,
"kind": "screens",
"id": "edoc_signup_screens",
"screens": [{ "key": "account", "title": "Create your account", "nodes": [] }]
}That is what a host authors and what this package is given. It is JSON on both sides of the boundary: no host constructs an Elixir struct to talk to this package, and no runtime in another language is disadvantaged by the fact that the reference implementation is written in Elixir.
Recorded 2026-09-14, campaign RF049, bead rd-1dt. Rewritten in place while still proposed on 2026-09-15, campaign RF049, bead rd-9wd: the record now frames Riddler as a dynamic content runtime with content kinds, names the screens kind's modules and corpus capabilities, and delegates the screen document to ADR-0002 and the shared machinery to a forthcoming ADR-0004.
Accepted 2026-09-15, campaign RF049, bead rd-v1w, after a claim-by-claim
reading against main at 0d1979b (riddler 0.1.0, published). The code halves
that built what this record asserts: the package boundary and the two runtime
dependencies (PR 1), the template subset (PR 5), Riddler.Screens.Document
and its registry (PR 6), resolve/2 and resolve_screen/3 (PR 7),
validate_responses/3 and /4 (PR 8), the corpus and its four capabilities
(PR 9), the emitter and the drift check (PR 10), and the rename to
Riddler.Screens (PR 15). The paragraph above, recording a rewrite made
"while still proposed", describes the state this Note ends: the record is
accepted from this date, and a further change to what it decides is an
amendment, not an edit in place. Two sentences it states as rules are still
prospective at 0.1.0 and are carried as notes by addition rather than as
corrections: the shared condition and container machinery it says carries no
kind in its name still lives under Riddler.Screens (rd-2b2), and the
<kind>.<function> capability rule wants a clause for the shared
templates.render (rd-c6t).
Noted 2026-09-17, campaign RF051, beads rd-2b2, rd-c6t, rd-aya and rd-cpx, and
one item carried out of the direction review of ADR-0002's amendment of the same
date. Five notes by addition, each read against main at 27faac3. Nothing
above is changed; each paragraph below says what the text above means now.
The shared condition and container machinery still sits under the kind's
namespace. The module-layout rule above says that Riddler.Template,
Riddler.Finding and the condition and container machinery are shared and carry
no kind in their names. At 27faac3 the first two hold and the last two do not
yet: condition evaluation lives in Riddler.Screens (lib/riddler/screens.ex,
the private evaluate/4 that calls Predicator.evaluate/2) and in
Riddler.Screens.Document (lib/riddler/screens/document.ex,
compile_condition/2), and the one container this version ships is
Riddler.Screens.Type.Variant (lib/riddler/screens/type/variant.ex). Read that
sentence prospectively: it states where the machinery belongs once it is shared,
and lifting it out of the kind's namespace is work for the forthcoming ADR-0004,
which the Consequences section already names as the record that decides the
shared content machinery. Riddler.Template, Riddler.Template.Compiled and
Riddler.Finding are the parts of it that carry no kind today (rd-2b2).
The capability-naming rule covers shared machinery under its own prefix. The
rule above is written <kind>.<function> and is immediately followed by
templates.render, which is the shared template subset and not a content kind.
The rule in full: a capability is named <prefix>.<function>, where the prefix
is the kind for a kind's capability and the machinery's own name for shared
machinery. The names ship as they are - at 27faac3 this version emits
screens.admit, screens.resolve and screens.validate_responses under
corpus/screens/, and templates.render in corpus/templates/render.json - and
a shared-machinery prefix, like a kind's, is part of the contract and changes
only by a record (rd-c6t).
The packages the no-statifier rule excludes, named. "No statifier package
appears in this repository's mix.exs or mix.lock" excludes the statifier
family's own packages: statifier (the statifier-ex repository),
statifier_blocks, statifier_persistence, statifier_oban,
opentelemetry_statifier, statifier_ui, statifier_datamodel and
statifier_examples. predicator and solid are outside that set and are the
two runtime dependencies this record names; predicator is developed alongside
the statifier family but is not one of its packages, and the rule does not reach
it. At 27faac3 the rule holds literally: mix.exs names predicator and
solid as the runtime dependencies, its one occurrence of the word "statifier"
is the deps comment restating this prohibition, and mix.lock carries no package
of that set (rd-aya).
The worked example elides two of the fixture's three screens. The fixture is
a signup wizard of three screens, keyed account, plan and confirm (read at
9d288c9). The outline above shows account alone; plan and confirm are
elided along with the metadata block and the node lists (rd-cpx).
validate_responses/3 and its arity-4 form are removed by ADR-0002's
amendment of 2026-09-17. Two places above name them as current: the paragraph
on the public runtime of the screens kind, and the sentence naming
validate_responses as that kind's one kind-specific check. Both describe what
0.1.0 shipped. That amendment decides that a check on a visitor's responses
resolves the screen against the same root the host resolved with, names the
replacement validate_screen/3 and its arity-4 form naming the button that was
pressed, and removes validate_responses/3 and /4 rather than deprecating
them; it says in as many words that prose in this record naming
validate_responses describes what v1 did and stays as the historical record of
it. Read those two places here as history from that date. The removal is a
breaking change to a public function of 0.1.0 and ships in the next release cut
after its code half lands; at 27faac3 that code half has not landed and
Riddler.Screens.validate_responses/3 and /4 are still present in
lib/riddler/screens.ex.
Noted 2026-09-17, campaign RF051, bead rd-9j0. One note by addition, read
against main at 6658ae4. Nothing above is changed.
Riddler.Corpus and the mix riddler.corpus task ship in the Hex tarball,
and that is the decision, not an accident. Both are files under lib/
(lib/riddler/corpus.ex and lib/mix/tasks/riddler.corpus.ex, read at
6658ae4), and package/0's files: list in mix.exs names the whole lib
directory, so a mix hex.build of 0.1.0 at that commit puts both in the
package. They stay: they are small, they document to a reader of the published
package what this package's conformance corpus is and how it reaches
riddler_spec, and excluding individual lib/ files by enumerating paths in
files: is a list that has to be maintained against every future file under
lib/ (rd-9j0).
Noted 2026-09-17, campaign RF051, bead rd-pqf. One note by addition, read
against main at 0d51acc. Nothing above is changed.
The provenance an emitted case file carries names the source file and
nothing else: no version, no commit. The corpus paragraph above says that a
"mix task emits them, with provenance naming the commit they were emitted
from, and a drift check in this repository's CI fails when the emitted corpus
and the cases disagree." That clause was already inaccurate at 0.1.0 in the
noun it used: at 0d51acc the emitter stamped the package VERSION, not the
commit - Riddler.Corpus.generated_by/1 in lib/riddler/corpus.ex answered
the word riddler, the package version, the word from and the
repository-relative source path. From this date the header carries neither.
It is the word riddler, a space, the word from, a space and the source
path, so an emit is byte-identical across commits AND across releases, and a
difference the drift check reports is a difference in the corpus rather than
in when or from what version it was emitted. In the corpus, not in the cases
alone: Riddler.Corpus.drift/1 walks files/0, which is the case files and
the schemas together (lib/riddler/corpus.ex, read at 0d51acc), so a
schema that differs or is absent is reported exactly as a case file is - and
a missing schema is what that check reports on the request carrying this
note. The clause's second half holds unchanged: the drift check still fails
when the emitted corpus and the cases disagree. Read its first half as
history: what 0.1.0 emitted was the version, not the commit that clause
names.
Why no corpus_version field and why no commit stamp. Neither was taken,
and each fails for its own reason. A corpus_version key does not exist: no
case file, schema or module in either repository carries one, and the corpus
case schema's own description says the emitter "adds its own provenance header
beside them, which is why this schema does not forbid further keys at the top
level" - permission to add a key, not a key already there. Adding one would
be introducing a field to an emitted format, which is a decision about the
contract and not a fix to a header. A commit stamp fails harder: it would
rewrite every emitted file on every commit to this repository, where the
version stamp rewrote them only on a release. The property the header exists
to have is that re-emitting an unchanged corpus writes the same bytes, so
that the drift check reports a real difference rather than the passage of
time; stamping the commit would trade a drift-per-release for a
drift-per-commit and leave the drift check reporting the history of this
repository instead of the content of the corpus. The version and the commit
an emit ran from are recorded in the request that carries the emit, where
they do not travel into the artifact.
The key stays; only its value changes. riddler_spec's bin/lint
requires generated_by on every case file among its required top-level keys
(read at riddler_spec 84d7a3e), and it checks that the key is present and
not what its value says. Removing the key would turn that repository's gate
red; a corpus emitted under this note passes it unchanged. The package
version itself is not removed and is not hidden: Riddler.Corpus.version/0
survives, and mix riddler.corpus still names the version on the console for
whoever is running an emit. Console output is not an emitted byte, and a
person running the task is entitled to know which checkout is writing.
Why a note and not an amendment. The test docs/adr/README.md states is
that "an amendment changes what the record decides and a note does not: a
note records where something already decided renders, or what a sentence
already accepted was about." This entry does not change what this record
decides. The proposition the corpus paragraph asserts in bold - that the
conformance corpus is authored in this repository and emitted into
riddler_spec - is word for word what it was and is as true after this
change as before. What moves is a subordinate clause describing how the
emitter labels what it writes, and even that clause's truth value does not
move: it named the commit while the emitter stamped the version, so it was
inaccurate before this change and is inaccurate after it. What this record
DECIDED names no generated_by and states no header shape: at 0d51acc the
word does not occur in this file at all. Where it occurs as this note lands,
and where the header's shape is spelled out, is in the paragraphs of this
note, which report the header rather than decide it. And the record does not
put the header inside the contract it decides; the contract it decides is the
content document. The note above headed "Riddler.Corpus and the mix riddler.corpus task ship in the Hex tarball, and that is the decision, not
an accident." is the precedent this follows - a deliberate choice about the
emitter, locked in where a reader of the record will meet it, without
amending anything. By contrast the sibling record ADR-0002 took THREE
amendments the same day, and each of the three cleared the bar for a reason
this entry has no counterpart to. The first says of itself that "this
amendment changes what the record decides there", which is the test in the
record's own words. The second, because it "reverses the layer the Decision
assigns" and "refuses documents this version admitted". The third, because
"the rule stated above answers a call one way, and this entry answers the same
call another". All three read at 0d51acc, and all three name a call the
record answered one way and now answers another. This entry names none,
because it changes no answer the record gave. That a ruling was taken is not
the test; if it were, every commit made under one would amend a record.
This note is recorded with the code half that makes it true, in one request:
at 0d51acc the emitter still stamps the version, and the request carrying
this note is the one that stops it (rd-pqf).
Noted 2026-09-17, campaign RF051, bead rd-0pi. One note by addition, read
against main at 63c432f. Nothing above is changed.
Riddler.Finding carries a source position, and this record is where that
is decided. The struct gains a fifth field, :position, which is either
nil or a map %{line: line, column: column} with both numbers one-based and
counting bytes, as the parser's own locations do. Where a finding carries a
position its :message names it too and goes on naming it: a person reading a
finding reads one sentence, and the field is the same fact in the form an
editor can act on without parsing that sentence.
Which findings carry it. Three sites in lib/ set the field and no others:
the two clauses of Riddler.Template that build a template refusal, and the
document.invalid_template finding in lib/riddler/screens/document.ex that
re-reports a template refusal against the template a document node writes,
which carries the position of the refusal it wraps. Every other finding this
package builds leaves it nil: the document checks; the field refused for not
being template source at all, which carries the document.invalid_template
code as well, so that code alone does not tell a host whether a span is there;
and the four response findings, response.required, response.format,
response.out_of_range and response.undecidable.
Why some of those nils are the way they are is filed rather than
explained. Three of them are defects in this package and not decisions of
this record, and a record cannot be made true about behaviour that is wrong:
document.invalid_condition obtains a place for a condition the compiler
refused and does not carry it, and document.invalid_pattern's place is
discarded a layer below the finding (both rd-ai1); response.undecidable
fires for several unrelated causes and discards a position it is handed
(rd-d9n); and a template refusal whose parser metadata is malformed raises
before any finding is built rather than producing one without a place
(rd-9cc). That last one is why "every template refusal carries a position" is
worth stating carefully: it is what this package's checks and a fuzz of 19,683
templates and 43,253 adversarial inputs established, finding no template
finding with a nil position, and not a guarantee the parser makes. A
metadata carrying a line and no column would reach Riddler.Finding.position/2
and yield nil; no input has been found that produces one.
The field and the checks that pin it are added by this request and so are
citable at no earlier SHA; at 4208433 the struct carried four fields and the
line and column existed only inside the message, although
lib/riddler/template.ex already carried them as a pair through its refusal
tuples and formatted them in at the end.
Why this record and not ADR-0002 or ADR-0003. The rule on module layout
above names Riddler.Template, Riddler.Finding and the condition and
container machinery as the shared machinery that carries no kind in its names,
and the rule on the public runtime of the screens kind says that surface
beyond the functions it names is added by a record and not by a commit. A
field on the struct every refusal in this package returns is that surface, and
this is the record that owns the shared machinery it sits on. The two records
that decide behaviour producing findings do not own its shape: ADR-0003 names
Riddler.Finding nowhere, and ADR-0002 mentions it twice - once as an example
usage, and once to say in as many words that it does not introduce a
finding-code registry and that nothing there changes what the struct carries.
Neither enumerates the fields as a decision, so neither is amended by adding
one; the addition is recorded here, where the struct's ownership is stated.
Why a map and not a tuple, a nested struct or two flat fields. A host
matches %Riddler.Finding{position: %{line: line, column: column}} and reads
the two numbers by name. A {line, column} tuple would be positional, so a
reader has to know which number is which, and it has no JSON form, while this
package's conformance encoder turns every answer it emits into JSON for a
second runtime. Two flat fields would make "there is no source span" two facts
that can disagree, where one nullable field makes it one. A nested struct
would be a second public module for a pair of integers, and gains a host
nothing a map with those keys does not already give it.
Riddler.Finding.position/2, which is @doc false and no part of this
package's public surface, turns a line and a column into the map and answers
nil unless both are positive integers, so a parser error reported without a
place could not put half a span on a finding. It is the same device, in the same
module, and for the same reason, as
Riddler.Finding.node_key/1, the @doc false coercion this package's findings
already go through; each is def rather than defp because the checks that
build findings call it from another module. The map's shape carries a name of
its own: this addition exports the type Riddler.Finding.position/0, defined as
%{line: pos_integer(), column: pos_integer()}, and the struct's :position
is typed position() | nil (rd-0pi).
Amendment, 2026-09-18: a placeless parse refusal is a finding with a nil position
Status: proposed
This record decided that a finding's :position is nil or a map of a line
and a column, and it stated carefully that every template refusal carries a
position - carefully, because the statement rested on a fuzz rather than on a
guarantee, and because the one input class that would have falsified it raised
instead of producing a finding at all. That raise is now gone. This amendment
changes what the record decides about the shape a template refusal can take.
The change it records is the one riddler-ex pull request 45 makes. At writing
that request is open and held, and every cite below is read at its head
3ca3d745099636ee3e8f3468d467263fcaa3c34b; the amendment lands at proposed
and is flipped once both it and that request are on main.
What the record now decides
A template the parser refuses without saying where is a finding with no
position. The refusal reaches a host as the ordinary parse-failure finding,
template.parse_error with position: nil and a message that names no place,
from Riddler.Template.compile/1
(lib/riddler/template.ex:166, the {:error, :placeless} clause at :180);
and as document.invalid_template with position: nil at the document door,
where the wrapping finding carries the position of the refusal it wraps and
that position is now legitimately absent
(lib/riddler/screens/document.ex:586 and :590). Both are pinned by tests,
and the first by a corpus case in corpus/templates/render.json that expects a
refusal naming no place.
A nil position is a reachable shape, not only an unreached one.
Riddler.Finding.position/2 is where it is answered: it builds the map only
when both the line and the column are positive integers and answers nil
otherwise (lib/riddler/finding.ex:97 and the fallback clause at :101). It
is reached with no line at all, because the parser can refuse a source without
locating it and one call into the parser -
Riddler.Template's private parse/1 at lib/riddler/template.ex:210, which
wraps exactly Solid.parse(source) and names the two exceptions the parser
raises inside its own line arithmetic, ArithmeticError and CaseClauseError
(:213) - turns that into a refusal rather than an exception. The message and
the field cannot disagree about it: the span is appended from the position
rather than from the numbers it was built out of
(lib/riddler/template.ex:538 and :539).
The rule is restated. "Every template refusal carries a position" no longer holds and is replaced by: every template refusal is a finding, and a position is present when the parser gave one.
What v1 said, and why it is now history
Two sentences of this record are about the state before that change, and are read as history rather than as what the record decides.
The first named the raise as an open defect of this package:
and a template refusal whose parser metadata is malformed raises before any finding is built rather than producing one without a place (rd-9cc).
That defect is resolved by the request above, and the bead is no longer open against this record. The other defects that paragraph files - the condition and pattern places discarded a layer below the finding, and the undecidable response finding's several causes - are untouched by it and stand as written.
The second grounded the absence of a nil position in the fuzz rather than in
a guarantee:
it is what this package's checks and a fuzz of 19,683 templates and 43,253 adversarial inputs established, finding no template finding with a
nilposition, and not a guarantee the parser makes. A metadata carrying a line and no column would reachRiddler.Finding.position/2and yieldnil; no input has been found that produces one.
The care in that sentence was warranted and the sentence is now overtaken:
inputs that produce a template finding with a nil position are known, named
in the tests and in the corpus, and {% render %} is one of them. What the
fuzz established remains true of the code it ran against; it is not a
statement about the code this amendment records.
The count of sites is unchanged. "Which findings carry it" says three
sites in lib/ set the field and no others, and after the change it is still
three: the two clauses of Riddler.Template that build a template refusal
(lib/riddler/template.ex:518 and :529) and the document.invalid_template
finding that re-reports one (lib/riddler/screens/document.ex:590). The
guard added at the one call into the parser routes a placeless refusal through
the first of those clauses rather than adding a fourth site; what changed is
what that clause can produce, not how many places produce it.