test: pin the guard's whole shape and the card region's kinds #19
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "test/close-surviving-mutations"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes every mutation that survived
ReviewSuite's battery on #18, plus the four thereview of this branch's first push turned up. Test-only:
git diff main --statistest.luau | 83 +++--, nothing undertimew/, so there are no behaviour changes. Everyone of them is a pre-existing gap rather than a defect in shipped code — each is a place
where the suite could not have noticed one.
1 — the guard's whole shape, not just its anchors
The guard is
^%d+:%d%d:%d%d$. Onmain, dropping either anchor survived the entiresuite, and so did tightening or loosening every single field:
Why the anchors survived: no input carried a clock with junk after it, so the trailing
$was doing nothing any test could see. The one input with junk before a clock is theempty-range prose,
... - 2026-09-02T00:00:00., and it ends in a period — so$rejected it and the leading
^was never what did the work. The accident of that periodwas carrying the assert.
Why
S1matters most: it is not synthetic.timew summary :allprints a two-digit hourfor anyone past ten hours logged, and this file already feeds the parser
"45:00:00"atthe 45-match state. Under
S1that renders as the reject arm's"0:00"—indistinguishable from a real zero, in the panel's headline all-time total.
Nine inputs now pin the guard end to end — both anchors, the hour's width, and the minute
and second fields from both sides:
The failure mode is one story for all of them. Anything that reaches the accept arm through
a loosened guard is a string
Parse.clockalso refuses, so it floors to0— andFmt.span(0)is"0s", not the reject arm's literal"0:00". The panel would publish0sas the all-time scope's header total for a range it failed to parse(
allTotalText→scopeTotalText), indistinguishable from a real zero. (The first pushsaid "into the day header"; that was loose — the in-code comment was always accurate.)
2 —
Parse.clock's own anchorsThe guard comment leans on that flooring: "one of them reaches
Parse.clock, which floorsto 0". Nothing in the suite held it —
S12/S13, dropping either anchor of^(%d+):(%d%d):(%d%d)$, both survived.The realistic input pins neither, for the same reason as above: the prose field
"2026-09-02T00:00:00."carries junk on both sides, so either anchor alone alreadyrejects it and dropping one still floors it. Two inputs that discriminate:
The first is the
SUMMARYfixture's own data line — a line that ends in a clock is not aclock.
3 — the card region, as child kinds rather than child counts
#18pinned#info.children == 2, which catches aToday:label coming back under theclock. Both counts are pinned now, so the row's shape is as closed as the column's.
That was wrong on two counts, and the review demonstrated both.
Child count is not shape.
X1—buildCardreturningui.columninstead ofui.row—survived. The card silently restacks from side-by-side to vertical, the button lands
under the clock, nothing fails.
Two positions is not four. Reinstating the exact historical bug,
ui.label({ text = "Today: 2026-09-02T00:00:00." }), at every position the card regionoffers, against the first push:
chips = left.children[4].childrenreads position 4 positionally, so an append at position5 disturbed nothing.
The fix is a change of spelling, not of kind. A count is the right kind of contract —
what is defended is an absence, so there is no value to assert and counting is the cheapest
way to say "nothing here". It is the weakest spelling of it:
-- 3 vs 2names acardinality, not what arrived, so a legitimate new node and this regression are the same
failure, and bumping
2to3is the locally rational move that silently disarms thedefence.
One function-scoped helper turns the two counts into four kinds-in-order comparisons:
All four positions die, and so does the card's kind, and each failure names the node that
arrived:
and
X1:"column,button,label"says a label came back beside the button.3 vs 2says a numbermoved. A maintainer adding a legitimate node now has to write down what it is, and the diff
reads
"column,button"→"column,button,badge", which is legible in review.The helper is
localinsidecompare: this file sits at Luau's 200-register file-scopeceiling and one more top-level non-constant local fails to compile with
Out of local registers ... exceeded limit 200.4 — the all-time total's parsed string
compare()checkedsame(m.scopeTotalText, if m.scope == "all" then m.allTotalText else m.dayTotalText). In all-time scope that is the same field twice, so it cannot fail on awrong parse — the suite fed
"45:00:00"through the parser and never asserted the result.One line at the fixture:
It kills
S1on its own, through the whole pipeline (stdout →parseRangeTotal→state.allTotal→ model), with the wrong value in the message:got 0:00.Mutation results
Re-run in
cp -rcopies at/tmp— never the live checkout, since the running shellhot-reloads this directory. "Before" is this branch's first push (
e33744c), the tree thereview saw.
S1hour%d+→%dtimew.luau:174:397S2hour%d+→%d*timew.luau:174:406S3minute%d%d→%d%d?timew.luau:174:407S4second%d%d→%d%d?timew.luau:174:408S5minute%d%d→%d+timew.luau:174:407S5bminute%d%d→%d%d%d?timew.luau:174:409S5csecond%d%d→%d%d%d?timew.luau:174:410S12Parse.clockdrop^parse.luau:77:116S13Parse.clockdrop$parse.luau:77:117L1label as the card's siblingpanel.luau:1104:1904RT1label appended to the root rowpanel.luau:1120:1903X1buildCardreturnsui.columnpanel.luau:764:1904G3drop trailing$timew.luau:174:382:389G4drop leading^timew.luau:174:381:388S6guard →"%d"timew.luau:174:366:373S7reject arm"0:00"→"0:0"timew.luau:174:366:373P7Fmt.span→Fmt.clocktimew.luau:174:372:379S14Parse.clockhour%d+→%dparse.luau:77:109:109S15Fmt.spans < 60→s < 59fmt.luau:27:118:125S16Fmt.spans < 60→s <= 60fmt.luau:27:119:126C1third child, card columnpanel.luau:772:1857:1906C4bsibling, card rowpanel.luau:782:1858:1905C2prepend, card columnpanel.luau:765:1844:1872C3prepend, card rowpanel.luau:764:1538:1566L2prepend, left columnpanel.luau:1100:1366:1394R1append, right columnpanel.luau:1119:1349:137726 applied, 26 killed, zero survivors. The twelve that were open on the reviewed tree
are the top block.
Gates
Field comparisons
1751 → 1775: two child-count comparisons became four kind comparisons,across twelve states. The nine guard asserts, the two
Parse.clockasserts and theallTotalTextassert are file-scopeasserts, notsame()calls, so they do not move thecount.
What the review changed
S1survives; add"45:00:00"X1survivesX1dies on the left column's kind stringParse.clock's anchors are unpinnedscopeTotalText == allTotalTextis a tautologyFor a reviewer
:00:00,1:2:03,1:02:3,1:123:03,1:02:034— five, with1:02:03x). The review flagged that the first push wasinconsistent about which standard it applied. It is now consistent in the closing
direction: the guard is pinned as a shape contract, exhaustively, and the
realistic/synthetic distinction applies to which inputs model production output, not to
which boundaries get defended. If you would rather see
S2–S5cdeclared equivalentmutants and documented as such, that is a coherent alternative and it is one comment
instead of five asserts.
kinds(right)is deliberately absent: the right column's mutations (R1,L2) werealready dead, and
#rightKids == 6attest.luau:1574stays a count because upgrading itwould need a file-scope helper, which does not fit under the register ceiling. So the
file now mixes both spellings, by force rather than by choice.
kindshelper is rebuilt on each of the twelvecompare()calls. Trivial, but it is aclosure allocation in a hot-ish loop if this file grows.
Verdict: REQUEST_CHANGES. (Posted as a
COMMENTreview because Forgejo refusesREQUEST_CHANGESon your own PR:reject your own pull is not allowed.)Reviewed by re-running everything from scratch in
/tmpcopies ofmain(7f2f419) and thishead (
e33744c). Every claim in the description is true and every claimed kill lands at theclaimed line with the right message. I am still requesting changes, for two reasons: the guard
this PR exists to pin still has one boundary open, and that boundary is the realistic one; and
the new comment states the card hole is closed when it demonstrably is not.
Verified
mainG3(^%d+:%d%d:%d%d) andG4(%d+:%d%d:%d%d$) applied totimew/timew.luau:174on7f2f419both SURVIVED the whole suiteC4bwas unpinned onmainui.labelappended aftertimew/panel.luau:782SURVIVED on7f2f419C1andP7were already killed onmainC1→test.luau:1842,P7→test.luau:372on7f2f419G3→test.luau:382,G4→test.luau:381C4b→test.luau:1858,C1→test.luau:1857,P7→test.luau:372:382leavesG3SURVIVING; deleting:381leavesG4SURVIVING. Each input pins exactly one anchor — the mapping in the description is exact, not approximateFmt.span(0)is"0s"and the reject arm is"0:00"Fmt.span(0) = "0s",Fmt.span(59) = "59s",Fmt.span(60) = "0:01", reject arm literal"0:00". DistinguishableParse.clockand floorParse.clock("2026-09-02T00:00:00") = 0,Parse.clock("1:02:03x") = 01739 → 1751, twelve states, one comparison eachmainprints1739, this head prints1751,12 statesbothNote
Fmt.clock(0)is also"0:00", so the two new asserts do not overlapP7's kill at:372—they are independent of it, which is correct.
Gates
nix shell nixpkgs#luau -c luau test.luau, run in a copy of this head:noctalia plugins lint timew:git diff main...HEAD --statistest.luau | 24 ++++--and nothing undertimew/, so lint isunchanged from
mainby construction.Mutations
Everything below was applied to a copy of this head and run against the suite as submitted.
G3drop trailing$timew.luau:174test.luau:382G4drop leading^timew.luau:174test.luau:381P7Fmt.span→Fmt.clocktimew.luau:174test.luau:372S6guard →"%d"timew.luau:174test.luau:366S7reject arm"0:00"→"0:0"timew.luau:174test.luau:366C1third child, card columnpanel.luau:772test.luau:1857C4bsibling, card rowpanel.luau:782test.luau:1858C2prepend, card columnpanel.luau:765test.luau:1844C3prepend, card rowpanel.luau:764test.luau:1538L2prepend, left columnpanel.luau:1100test.luau:1366R1append, right columnpanel.luau:1119test.luau:1349S14Parse.clockhour%d+→%dparse.luau:77test.luau:109S15Fmt.spans < 60→s < 59fmt.luau:27test.luau:118S16Fmt.spans < 60→s <= 60fmt.luau:27test.luau:119S1guard hour%d+→%dtimew.luau:174L1label as the card's SIBLING in the left columnpanel.luau:1104X1buildCardreturnsui.column, notui.rowpanel.luau:764RT1label appended to the root rowpanel.luau:1120S2guard hour%d+→%d*timew.luau:174S3guard minutes%d%d→%d%d?timew.luau:174S4guard seconds%d%d→%d%d?timew.luau:174S5guard minutes%d%d→%d+timew.luau:174S12Parse.clockdrop^parse.luau:77S13Parse.clockdrop$parse.luau:77Ten survivors. Four are actionable;
S1andL1are the ones that matter.The invariant question
You asked whether
#childrenis the right contract or whether it trains a maintainer to bump thenumber. My answer is: it is the right kind of contract and the weakest available spelling
of it, and the evidence for that is in the table above.
Right kind. What is being defended is an absence. There is no value to assert, because the
correct state is "no node here". Counting is the cheapest way to assert absence at a position, and
it is honest about defending a product decision (the day header already carries that number)
rather than a correctness property. A "stronger" assertion about the content of a node that is
supposed to be gone would be asserting about nothing. So I do not think reaching for a count was
wrong in principle.
Weakest spelling, on three counts.
It does not say what arrived.
-- 3 vs 2names a cardinality. Swapping both lines for thechild kinds in order costs one helper and zero extra field comparisons (still
1751), and thesame two mutations then fail like this:
"column,button,label"tells the maintainer a label came back beside the button.3 vs 2tells them a number moved. That difference is the whole of your question: the first is read,
the second is bumped. (The helper must be
localinsidecompare—test.luauis at Luau's200 file-scope register ceiling and one more top-level non-constant local fails to compile.)
It is bound to a position, and the label has more than two. See
L1andRT1below. Thein-diff comment says "only the pair closes the hole". The pair closes two of at least four.
The file already has the stronger idiom and this PR reached past it.
test.luau:1567isassert(not onScreen("1-8 / 20"), "the range label the panel used to build is gone")— thesame problem (a deleted label must not come back), solved position-independently, one screen
away. And
test.luau:1574pins#rightColumn().children == 6for the right column whilenothing pins the left column's four.
Would a legitimate layout change break these for an unrelated reason? Yes, and cheaply: a
"paused" badge under the clock, a second glyph button, a divider in the card. Under
#childrenthat failure is indistinguishable from the regression and the message ("nothing under them") reads
as a rule rather than a reason — so bumping
2to3is the locally rational move, and itsilently disarms the defence. Under kinds-in-order the maintainer has to write down what the new
node is, and the diff then reads
"column,button"→"column,button,badge", which is legible inreview. That is the difference between an invariant and a magic number.
Findings
Blocking
1.
test.luau:1850-1858— "only the pair closes the hole" is false; the label still has two openpositions.
The comment added by this PR says the two counts together close the hole because a third node
could otherwise come back beside the button. It can also come back beneath the card, in the left
column, and there it is addressed by nothing. Reinstating the exact historical bug —
ui.label({ text = "Today: 2026-09-02T00:00:00." })— at all four positions the card regionoffers, against the suite as submitted:
chips = left.children[4].childrenreads position 4 positionally, so an append at position 5disturbs nothing. This is not a hypothetical shape — it is the deleted label, with the deleted
label's buggy text, back on screen, suite green.
The cheapest fix is the file's own convention, one line below
test.luau:1574:Verified: suite stays green (
1751), andL1then dies withtest.luau:1575: the left column is title, card, recent label, chips: got 5.A position-independent scan closes
RT1as well, using theallTextswalker already in the file,and names the defect instead of a count — one line inside
compare, all four positions dead:Either fix is fine. What is not fine is shipping the sentence "only the pair closes the hole"
while the hole is open — that is precisely the "trains a maintainer to trust the number" failure
you asked me to hunt for, and here it is in the comment rather than in the assert.
2.
test.luau:381-382— the guard's realistic boundary is still unpinned, and the suite alreadycarries the fixture that would pin it.
S1, tightening the hour field from%d+to%d, SURVIVES the suite as submitted. Thatmutation is not synthetic:
timew summary :allprints a two-digit hour for anyone with ten hourslogged, and under
S1the panel's headline all-time total renders"0:00"— the sameindistinguishable-from-real-zero failure this PR was written to defend against, one field to the
left of the anchors it pins.
The suite does feed two-digit hours through this parser —
"20:00:00"attest.luau:1564and"45:00:00"attest.luau:2229— butallTotalTextis only ever asserted against itself(
test.luau:1896comparesscopeTotalTexttoallTotalText), so the parsed string is neverpinned. One line, next to the two new asserts:
Verified: suite stays green (
1751), andS1then dies at that assert.This is also my answer to your question 3 about
"1:02:03x". Pinning a boundary with an input theupstream tool cannot emit is defensible on its own — a guard is a contract about a shape, and
timew's output growing a suffix is exactly the change you cannot see coming. But the PR isinconsistent about which standard it applies. By the synthetic standard, four more boundaries of
the same guard are unpinned (
S2,S3,S4,S5all survive). By the "model real output"standard,
"1:02:03x"does not qualify and"45:00:00"does. Keep"1:02:03x"— it is doing realwork, and I confirmed it pins
G3alone — but adding"45:00:00"is what makes the guard'scoverage principled rather than accidental: after it, every field of
^%d+:%d%d:%d%d$thattimewcan actually exercise is pinned by an inputtimewcan actually produce.Advisory
3.
test.luau:1858— the card row's kind is not part of "the row's shape".The description says "Both counts are pinned now, so the row's shape is as closed as the column's."
X1—buildCardreturningui.columninstead ofui.rowatpanel.luau:764— SURVIVES.The card silently restacks from side-by-side to vertical, the button lands under the clock, and
nothing fails. Child count is not shape;
same(left.children[2].kind, "row", …)or thekinds-in-order form above would cover it. Pre-existing rather than introduced here, so advisory —
but it undercuts the sentence, and the kinds-in-order rewrite gets it nearly for free.
4.
timew/parse.luau:77—Parse.clock's own anchors are unpinned (S12,S13both survive).Worth flagging only because this PR's stated failure mode leans on them: "one of them reaches
Parse.clock, which floors to0". That story is correct today, and I confirmed it empirically,but it is load-bearing on a guard the suite does not defend. Outside this PR's region; not a
change request.
5. Description wording. "The panel would publish
0sinto the day header" — the range totallands in
allTotalText→scopeTotalTextand is shown for the all-time scope. The in-codecomment (
"0s" is published where "0:00" belongs) is accurate; only the PR body is loose.Everything this PR claims, it does.
G3,G4andC4bwere genuinely open onmain, the fivekills land where stated, the anchor-to-assert mapping is exact, and the
"0s"story is real.Add the
"45:00:00"assert and either close the left column or correct the comment, and I wouldapprove without further comment.
e33744cf2dto9b7a420159Actioned. Both blocking findings accepted and closed, both advisories accepted, the
tautology closed, and the description rewritten where the review proved it wrong. Amended
onto
9b7a420rather than stacked. 26 mutations applied, 26 killed, zero survivors.Accepted
1 (blocking) —
S1, the guard's hour field. Added your suggestion verbatim:S1: SURVIVED before →killed test.luau:397after, with that message. You were rightthat this is the realistic boundary and the anchors were the cosmetic ones — the fixture
that would have caught it was already in the file, two thousand lines down, feeding a value
nothing asserted.
2 (blocking) — "only the pair closes the hole". Closed rather than corrected, and
closed at all four positions rather than the three your suggestion reaches. Reinstating
ui.label({ text = "Today: 2026-09-02T00:00:00." }):panel.luau:772:1857:1906panel.luau:782:1858:1905panel.luau:1104:1904panel.luau:1120:19033 (advisory) — kinds over counts. Accepted in full; the counts are gone. The two
#childrencomparisons became four kinds-in-order comparisons behind one helper that islocalinsidecompare, as you specified — I reproduced the ceiling first, and a top-levellocal functiondoes fail with./test.luau:3366: Out of local registers when trying to allocate tag: exceeded limit 200.X1(buildCardreturningui.column) is closed as a side effect, so the sentence youcalled out is no longer merely corrected, it is true:
4 (advisory) —
Parse.clock's anchors. Added, as a suite assert only;parse.luauisuntouched. Your point that the guard's comment is load-bearing on an undefended property is
exactly why it belongs in this PR. Worth recording why it was undefended, because it is the
same accident this PR started from: the realistic input,
"2026-09-02T00:00:00.", carriesjunk on both sides, so either anchor alone already rejects it and dropping one still
floors it to
0. Two inputs that actually discriminate — theSUMMARYfixture's own dataline, which ends in a clock, and
"1:02:03x":5 — "day header". Corrected in the description; it lands in
allTotalText→scopeTotalTextand shows for the all-time scope.The tautology. Cheap, and done: one line at the 45-match fixture, no restructuring.
It kills
S1independently of the unit assert, through the whole pipeline, and puts thewrong value in the message (
got 0:00). Verified both ways: delete the unit assert andS1still dies at this one; delete this one and
S1still dies at the unit assert.Pushed back
On the spelling of fix 2, not the finding. Your suggested
assert(#leftColumn().children == 4, ...)at:1575closesL1but leavesRT1— the rootrow's fourth child — open, and it reintroduces the count idiom one line after your own
argument against it. I also skipped the
allTextsscan you offered as the alternative: oncekinds cover all four positions it is a second mechanism defending the same defect, and it
only catches that one text. So: neither of your two suggested fixes, but the finding closed
more completely than either.
I went past finding 1. You listed
S2–S5as survivors and framed them as anobservation about inconsistent standards rather than a change request. I closed them, plus
two widening twins I found while checking (
S5bminute →%d%d%d?,S5csecond →%d%d%d?, both SURVIVED on the reviewed tree). Reasoning: this PR exists to close guardsurvivors, so four left in the guard is the same finding recurring at the next sweep; the
failure mode is identical to the one already defended (
"0s"where"0:00"belongs); andit costs five file-scope asserts and zero field comparisons.
This is the thing to challenge hardest and I want to name it rather than bury it. It
resolves your standards inconsistency in the closing direction, which means the file now
carries five synthetic guard inputs. The coherent alternative is to declare
S2–S5cequivalent mutants —
timewcannot emit an empty hour, a one-digit minute or a three-digitsecond — and document that in one comment instead of five asserts. I took the asserts
because "upstream will not emit that" is the assumption that made the trailing
$unpinnedin the first place, but I do not think the other call is wrong.
Also worth a second look
kinds(right)is deliberately absent —R1/L2were already dead, so it would closenothing. That leaves
#rightKids == 6at:1574as a count next to four kind asserts. Itis forced, not chosen: upgrading it needs a file-scope helper, which the register ceiling
refuses. The file mixes both spellings now.
"every anchor and every field", and
S5cproved that false — the second field was pinnednarrowing but not widening. Fixed by adding
"1:02:034"rather than by softening thecomment, which is the same correction you asked for on "only the pair closes the hole". If
a comment in this diff still claims more than the asserts hold, that is the finding.
Gates
1751 → 1775: two child-count comparisons became four kind comparisons, across twelvestates. Everything else added is a file-scope
assert, which does not move the count.git diff main --statistest.luau | 83 +++--, nothing undertimew/.test: pin the guard's anchors and the card row's shapeto test: pin the guard's whole shape and the card region's kinds