The reason given was wrong. A dot path did reach an empty-named field:
Fake("a.") returned it, Fake("a.b.") returned a nested one, and {..a.} bound
it. What was true is narrower — List never offered it, because an empty name
is no path segment — so the engine accepted spellings it would not advertise.
The message, the test comment and the README say that instead.
Two things the rejection finished off:
A {} token still reported "no field \"\"", pointing at a fix the loader now
rejects — two errors for one rule. It is told the name can never exist, like
an option token already is.
addressable is dead: both halves of "not empty and no dot" are now rejected
where a name is authored. Mutating it to panic leaves the suite green here
and panics on main, so it was live and is not. Keeping it would preserve the
silent-hiding this change removes — List quietly omitting a name rather than
New refusing it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A field named "" was reachable by exactly one spelling, a bare {} token,
and by no dot path: List hid it and Fake could not ask for it. checkName
rejects it now, for the reason it rejects a dot — a name no path can reach.
That also makes the empty-segment rule unconditional. {a.} was rejected
because a field really named "" would otherwise have made it resolve; no
data can carry one, so the rule now guards a shape that cannot exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A field named "" loads, and a bare {} token renders it, but no dot path can
reach it — List hides it and Fake cannot ask for it. {a.} is already rejected
for the same reason, so the two spellings disagree about the same mistake.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sharing renders' seen set across heads left the suite green while silently
under-rejecting: the first head's walk marks the only route to the second
head's draw. This PR widens heads from the bound levels to every held name,
so two heads in one template is now the ordinary case; a rejected case whose
violation sits on the later head pins the scoping.
An operand rendering one field twice covers the revisit guard, taking
coverage to 97.8% against main's 97.4%.
The README said the two spellings are rejected alike without saying what the
hold follows, which is not true where the operand reaches its own field
through a reference. It now names the plain {field} tokens the walk follows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fence pinned only the operand node, so a reference reaching into it drew
that value again: {net.v} was a load error while {..cat.net.v} loaded and
disagreed in 20 of 40 renders. The hold now covers what rendering the operand
settles inside itself, so both spellings of that shape are rejected.
The walk stops at a {..path} edge, where the operand's own value ends and a
shared source begins. cover stops there too, through named, so both halves of
the fence end at the same boundary — and two names drawing from one referenced
category stay two draws.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
{net.v} is a load error because the operand is also a path head, but
{..cat.net.v} names the same node by another spelling and loads — and
disagrees in 20 of 40 renders. Same shape, opposite verdicts, which is the
thing this rule exists to remove.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The operand label had no test: every rejected case reported through a token
edge, so flipping the flag that distinguishes the two left the suite green
while the error went back to inventing a {b} the format never wrote. Each
case now asserts the route it names, and one reports through an operand edge,
which is the branch that was uncovered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>