refactor(panel): collapse the module state into one table behind a single commit #10

Merged
yuris merged 2 commits from refactor/t6-state into main 2026-09-01 08:32:56 +00:00
Owner

Wave 6, the last structural one: the panel's scattered mutable module locals
become one state table, and every repaint goes through a single commit.

Review #47 fixup (cd3395b). Four findings, all applied; the panel change is
comments only. Two of them were claims this description and test.luau's comments
made that the code did not back: the bypass invariant was grep-only where the
suite said it was test-enforced, and PR #8's globals snapshot never caught the
dropped local its comment advertised (it was taken after the model had already
created the global, so the loop compared the table against itself). Both are
closed and mutation-proved. adoptState's apply — the one site the repaint
table never reached — is now measured and driven in the suite. The invariant awk
block below is re-quoted at the current commit.

What moved

One state table. The twenty locals named in the ticket, plus elapsedText
— it is view state by every test that matters (mutated by a watch, read by the
model, shown on the tree), and leaving it outside would have meant viewModel
reading state and one stray local. poke and watching stay module locals
and are commented as such: neither reaches the view model, so neither is a reason
to repaint, and putting them in state would create fields commit must never
be handed.

Constants are untouched: ROWS_MAX, DAYS, RECENT_MAX, PAGE_BUTTONS, the
widths and heights, UNTAGGED, TAG_COLORS.

Two functions own the table.

  • apply(patch) writes state and never paints. Six call sites, each one a path
    that must not paint where it writes: loadDay (the day's walk lands the
    repaint — a bar click painted once before this change and paints once now),
    fetchDays (applies the totals, reloads the day, commits once),
    adoptState (inside refresh, whose callers paint later or not at all —
    measured separately, since the repaint table below never reaches it),
    onOpen (defaults applied so the refetches are issued against day scope and
    page 1, then one commit at the end), and onSearchChange / onDraftChange
    (the input widget is already showing the keystroke; neither painted before).
  • commit(patch) applies and paints exactly once. Eighteen call sites. It is the
    only caller of render in the file.

One semantic narrowing against the twenty assignments apply replaces, now
written on its comment: it iterates pairs(patch), so a patch field whose value
is nil is not written where the old assignment would have cleared the field.
No producer can emit one today (parseActive returns literals or
Parse.unquote, which falls back to its argument; every other parser returns a
table or a string), and the closed-field-set assert covers misspelling rather
than this. A field that needs clearing gets its zero value in the patch, the way
watchState's idle branch does (patch.startEpoch, patch.tags = 0, "").

Two functions rather than one because the alternative is worse in the direction
the ticket cares about: forcing those six paths through commit adds repaints (a
keystroke would rebuild the whole tree per character). The invariant the tests
and the gate pin is the one that matters — state.<field> = appears inside
apply and commit and nowhere else
:

$ awk '/state\.[A-Za-z_]+ *,? *(state\.[A-Za-z_]+ *)?= *[^=]/ || /state\[[a-z]+\] *= *[^=]/' timew/panel.luau
345:		state[field] = value                                           -- apply
367:	state.shown, state.matching = m.windowRows, m.pager.total       -- commit
373:	state.focusOnce = false                                        -- commit

That grep sees the source shape and nothing else, and one bypass shape does not
have that shape: commit publishes state.shown as the model's own
windowRows table rather than a copy, so a region builder that mutated the model
in place would move state without ever writing state.<field>. Nothing does
(grep -nE '\bm\.[A-Za-z_]+ *=[^=]|table\.(insert|remove|move|sort)\(state': no
matches, and main aliased the same table identically), and it is now written on
the state table's own comment. The complementary check — that every path
publishes what it painted — is in the suite; see "New asserts" below.

render writes nothing. It takes the model as an argument. commit builds
one model per repaint, publishes state.shown/state.matching off it at the
same point and in the same order render used to, hands the model down, and
clears state.focusOnce after the paint.

focusOnce belongs to commit because the flag's contract is "the repaint
that carries it to the tree consumes it" — that was render's job only because
render was the repaint. Now that repaints are explicit and singular, the
repainter consumes it, which is the same point in the same order. The observable
asymmetry test.luau already pinned (focus == true on onOpen's tree,
focusOnce == false by the time the model can be called) is unchanged and still
passes; only the assert's message changed.

Call-site count

The ticket says 45. That is grep -c render — every line mentioning render,
comments included. Actual render() call statements on main: 19. After:
1 (inside commit), reached through 18 commit and 6 apply call sites.

Repaints, per path, before and after

Measured, not reasoned about: main's panel.luau and this branch's are loaded in
one luau process, each with its own global table and identical stubs, driven
through the same path, with panel.render counted. cmd is commands queued or
landed (negative = landed). Harness: /tmp/pr9/h/repaints.luau.

path                                                             main         branch
--------------------------------------------------------------------------------------

== panel open
onOpen (defaults set, every fetch issued)                   1 paint      4 cmd      1 paint      4 cmd
  fetchActive lands                                         1 paint     -1 cmd      1 paint     -1 cmd
  fetchTotal lands                                          1 paint     -1 cmd      1 paint     -1 cmd
  fetchTags lands                                           1 paint     -1 cmd      1 paint     -1 cmd
  fetchDays lands (reloads the day)                         1 paint      0 cmd      1 paint      0 cmd
  the day walk lands                                        1 paint     -1 cmd      1 paint     -1 cmd

== tick and bar state
second tick while tracking (update)                         1 paint      0 cmd      1 paint      0 cmd
bar publishes active + startEpoch + tags                    3 paint      0 cmd      3 paint      0 cmd
bar publishes idle (clears the interval)                    1 paint      0 cmd      1 paint      0 cmd
bar publishes elapsedText                                   1 paint      0 cmd      1 paint      0 cmd

== clicks
click a day bar (onDay3)                                    0 paint      1 cmd      0 paint      1 cmd
  its day walk lands                                        1 paint     -1 cmd      1 paint     -1 cmd
click a day the chart does not hold (day 99)                0 paint      0 cmd      0 paint      0 cmd
click a row's tag (onRow2 -> setFilter)                     1 paint      1 cmd      1 paint      1 cmd
  dayTotals lands (bars follow the tag)                     1 paint      0 cmd      1 paint      0 cmd
  its day walk lands                                        1 paint     -1 cmd      1 paint     -1 cmd
clear the chip (onClearFilter)                              1 paint      1 cmd      1 paint      1 cmd

== scope switch
scope -> all time (onScopeAll)                              1 paint      2 cmd      1 paint      2 cmd
  the all-time walk and range total land                    2 paint     -2 cmd      2 paint     -2 cmd
scope -> this day (onScopeDay)                              1 paint      0 cmd      1 paint      0 cmd

== pager
page number (onIpc page 4)                                  1 paint      0 cmd      1 paint      0 cmd
next page (>)                                               1 paint      0 cmd      1 paint      0 cmd
previous page (<)                                           1 paint      0 cmd      1 paint      0 cmd
a named strip button (onPage1)                              1 paint      0 cmd      1 paint      0 cmd

== search box
keystroke in the search field (onSearchChange)              0 paint      0 cmd      0 paint      0 cmd
Enter in the search field, day scope (onSearchSubmit)       2 paint      3 cmd      1 paint      3 cmd  <-- fewer
  its dayTotals and all-time landings                       4 paint     -3 cmd      4 paint     -3 cmd
Enter in the search field, already all-time                 2 paint      5 cmd      1 paint      3 cmd  <-- fewer

== tag field and the two mutating commands
keystroke in the tag field (onDraftChange)                  0 paint      0 cmd      0 paint      0 cmd
Enter in the tag field (onDraftSubmit -> timew start)       1 paint      1 cmd      1 paint      1 cmd
  timew start lands -> refresh issues every fetch           0 paint      3 cmd      0 paint      3 cmd
  refresh's five landings                                   5 paint     -4 cmd      5 paint     -4 cmd
the stop button (onStop -> timew stop)                      0 paint      1 cmd      0 paint      1 cmd
  timew stop lands -> refresh issues every fetch            0 paint      3 cmd      0 paint      3 cmd
--------------------------------------------------------------------------------------
TOTAL                                                      39 paint     10 cmd     37 paint      8 cmd
repaints: main 39 -> branch 37 (-2); paths that got worse: 0

The two paths that were paying twice, named:

  1. Enter in the search field, day scope. main: setFilter committed the tag
    and painted under the old scope, then onSearchSubmit moved the scope to
    all-time and painted again — the first of those two trees showed the new chip
    over a day-scope list nobody asked for. Now the scope rides in setFilter's
    patch: 2 → 1.
  2. Enter in the search field, already in all-time scope. Same two paints, and
    main also issued the all-time walk twice: setFilter issued it (scope was
    already all), then onSearchSubmit issued it again for the same tag.
    2 paints → 1, and 5 commands → 3 — the command dropped is a 200-id DOM
    walk, the most expensive thing this panel asks timew for.

Everything else is identical, including the deliberate zeros: a bar click still
paints when its intervals arrive rather than before (loadDay applies, the walk
commits), a keystroke still paints nothing, and day 99 — the route to the
deferred backwards-range bug — still returns without painting or fetching.

The async landings are not merged and must not be: refresh's five fetches land
at five different times, so they are five logical changes and five repaints, on
both sides. Merging them means holding a paint until the slowest walk returns.

One apply site this table does not cover, and its own measurement. The
harness's noctalia.state.get returns nil, so adoptState takes its a == nil
early return on all 38 steps above and the apply under it never runs — "0 paths
worse" is true but says nothing about that site. Measured with a bar widget that
actually publishes:

== the bar widget is publishing, so adoptState succeeds and fetchActive is skipped ==
                       onOpen (defaults + refresh + commit)   stop lands -> refresh
main                   1 paint  [C:dayTotal C:tags C:dayTotals P]   0 paint
branch                 1 paint  [C:dayTotal C:tags C:dayTotals P]   0 paint
adoptState commits     2 paint  [P C:dayTotal C:tags C:dayTotals P] 1 paint

Committing there costs +1 paint per open and +1 per mutating command, and the
first tree is the same either way. So the site is justified by measurement, not by
argument — and that path is now driven in the suite rather than only here (see
"New asserts").

Timing

Same harness as PR #9's review (/tmp/pr9/h/cost3.luau), worst state: 200
all-time rows, page 4, min of 9 interleaved rounds x 500 calls, three runs.

main branch
repaint 0.0512 / 0.0513 / 0.0515 ms 0.0509 / 0.0518 / 0.0510 ms
viewModel() 0.0180 ms 0.0179 ms
pure-core calls per repaint 19 (= 19 per model) 19 (= 19 per model)
heap per repaint 57 KiB min / 58 median 57 KiB min / 58 median
ui nodes / props tables 104 / 104 104 / 104

Per-repaint cost is unchanged: the delta is ±1% and changes sign between runs
(-1.0%, +0.9%, -0.6%), i.e. noise. 0.20–0.21% of the 25 ms per-handler budget on
both sides. The patch table commit allocates does not register against a tree
of 104 nodes.

The path that lost a repaint, timed end to end (Enter in the search field from
all-time scope, min of 9 x 500, three runs):

main branch
per submit 0.0658 / 0.0669 / 0.0662 ms 0.0347 / 0.0345 / 0.0346 ms
of the 25 ms budget 0.26% 0.14%

-47% on that handler, and one fewer 200-id shell walk behind it.

Tree identity: 32 states, 0 differences

PR #9's harness (/tmp/pr9/h/treediff.luau), unchanged except for the branch
name in its banner. Both panels in one process, own global tables, identical
stubs, props key-sorted, flattened one line per node, compared byte for byte.

tree identity: main vs refactor/t6-state
states: 32
bytes: main 244419, branch 244419
node lines compared: 2372 per side
differences: 0
tree-identical

The async guards

PR #6's reproduction, re-run: issue the all-time walk under focus, change the
tag to email mid-flight, land the fresh walk, then land the stale one. The
guards moved from above the assignment to above the commit, which is the same
place semantically — but a refused patch is now also a repaint that never
happens, so there is a second, sharper thing to assert:

paintsFor("a walk issued under a tag the panel has left", 0, function()
	raceWalk.cb({ stdout = DOM })
end)
paintsFor("and that walk's all-time total", 0, function()
	raceTotal.cb({ stdout = "9:11:00" })
end)

The pre-existing content asserts (test.luau:659, :663) still pass unchanged. The
new count catches a failure mode they cannot see: a guard that drops the patch
but paints anyway. Mutation-proved below.

How the test suite still reaches state

The ticket's premise needs one correction, and it is load-bearing for reviewing
this PR: test.luau never drove state by assignment, because it never could.
panelEnv = getfenv(panel.Fmt.clock) is the module's globals table. The state
locals were local, i.e. upvalues, and Luau ships no debug.getupvalue — the
suite says so itself (:1030 on main). It drives state through the handlers the
shell calls (onOpen, onIpc, onRowN, onPageN, update, the four
noctalia.state.watch callbacks) and reads it back off the rendered tree and
panel.viewModel().

So the mechanism needs no adaptation to keep working, and the 14 states and 1485
field comparisons pass unchanged. What did change:

  • state is now exported (return { ..., state = state }, the table already
    documented "exported for test.luau only"). One value, so for the first time the
    suite can read the panel's whole mutable surface directly. It is read there,
    never written: writing it would be the one bypass of commit in the repo.

  • PR #8's env-key snapshot was moved, because it never caught what it claimed
    to.
    It is supposed to catch a local dropped inside viewModel landing a
    new global in the module's globals table — a class the state export cannot see
    (a name that never reached the table is not a field of it). It did not, on this
    branch or on main: panelEnv is the module's own globals table, taken at
    test.luau:492, so a global the model writes exists from the model's first call
    onward, and a snapshot taken further down (beside the viewModel section, after
    the panel has been driven) had already recorded the name. The loop at the bottom
    compared the table against itself. Dropping local from chartPeak inside
    viewModel printed ok on both sides.

    It now snapshots immediately after the panelEnv.panel stub, before the first
    handler runs, with os — the only key the table legitimately gains afterwards,
    from the frozen-clock stub — seeded by hand. Drop that seed and the pristine
    suite fails on os, which is what keeps the snapshot honest about being early.
    state is a local created at load time, so it still adds no key.

    The hole was PR #8's rather than this wave's, but this wave rewrote the comment
    that asserted it, so it is fixed here.

  • The other half is now asserted by value, which is new capability rather
    than a replacement: dump state, call viewModel() five times, dump again.
    Plus the landed values by name (#state.recent == 4, state.total,
    state.tags, ...), because a before/after comparison alone passes a model that
    flattens a field — the first call flattens it, every later call finds it
    already flat, and idempotence is not purity. That hole was real: a
    state.recent = {} planted in viewModel passed the whole suite until those
    asserts existed.

Asserts whose shape changed

assert change why
test.luau:1070 focus one-shot message only: "its own render cleared it" → "the commit that painted it cleared it" the consumer moved from the end of render to the end of commit; both asserts still pin the same observable
test.luau env snapshot moved, and seeded with os it never caught the dropped local its comment claimed; see above

No assert was weakened or removed. 354 lines added.

New asserts

  • commit paints exactly once per call, path by path, wrapping the stub's
    panel.render with a counter: 1 for a page click, a scope click, a filter set,
    Enter in either field, each fetch landing, each tick while tracking; 0 for a
    keystroke, a bar click (its walk pays), day 99, and a tick while idle.

  • Nothing bypasses commit, in the two halves that are reachable from here.
    A path that mutates without committing paints 0 and the tree falls behind the
    state — the counter above sees that. A path that writes state directly and
    paints anyway
    renders a correct tree, so neither the counter nor any tree
    comparison in the file can see it; what it drops is the rest of commit. So
    after every counted paint, paintsFor also asserts that state.shown and
    state.matching are the painted model's own window and total and that
    state.focusOnce was cleared. That makes all 23 counted paths bypass checks.
    The mutant that motivated this — setFilter's commit(patch) replaced by the
    four state.<field> = writes plus render(viewModel()) — used to print ok,
    and is not benign: it leaves the previous window published, so clicking the one
    row left after filtering to admin moves the chip to focus, and 'next page'
    reports "page 2 of 1, showing 9-8 of 5".

    What stays out of the suite is the source shape (state.<field> = only inside
    apply and commit). The luau CLI ships no io library — test.luau notes the
    same at :743 for en.json — so that grep runs in the gate; its output is at the
    top of this description, and the shape it cannot see is noted there too.

  • adoptState's success path is driven, so the sixth apply is counted too.
    It was the one panel path the counter never reached: noctalia.state.get
    returns nil in this harness, so the a == nil early return fired on every
    open. With the bar widget publishing, refresh adopts instead of issuing
    fetchActive, and the assert is that adopting costs no tree of its own — one
    paint for the open, the adopted interval already on it (the action button reads
    onStop and the subject line reads the adopted tag, both before any fetch has
    landed), then the four landings one paint each. adoptState committing instead
    makes that open paint twice.

  • commit publishes the model's own window and match count — asserted
    against viewModel() rather than a literal, since the claim is that they are
    not a second derivation.

  • The field set of state is closed. apply writes state[field], so a
    patch with a misspelled field would grow a silent twenty-second field while the
    model went on reading the old one. This is the hazard the refactor introduces
    in exchange for a greppable state surface, so it gets its own assert.

  • A repaint of unchanged state moves no field, and a repaint after the open
    one does not re-request focus
    (read off the second tree after a keystroke,
    since a flag set on the way out of one paint only reaches the next).

  • The stale-landing guards cost zero repaints (above).

  • Four commands for one Enter in the search field, not six.

Mutation proof

Every new assert broken, run, restored, sha256 compared (all restores
byte-identical; the suite is green on the pristine tree before and after).

Line numbers below are at 84d4201, the commit these were measured at —
git checkout 84d4201 reproduces them verbatim. The review fixup that follows
adds ~104 lines to test.luau and moves them; the assert message in each row is
the stable identifier. The fixup's own mutations are in the table after this one.

mutation first failure
commit calls render twice test.luau:1391: Enter in the search field painted 2 times, not 1
onScopeDay applies instead of committing test.luau:1391: the scope switch back to this day painted 0 times, not 1
onSearchSubmit restored to main's two-paint shape test.luau:1391: Enter in the search field painted 2 times, not 1
stale landing drops the patch but paints test.luau:1391: a walk issued under a tag the panel has left painted 1 times, not 0
a patch misspells searchKey (nothing else reads it) test.luau:1509: a patch grew state a field no declaration has: searchKeyy
commit publishes a stale match count test.luau:1545: the published match count is not the model's: 4 vs 3
fetchTotal's patch names allTotal test.luau:1534: the day total and the live interval are the fetches' own, after five model calls
viewModel clears recent after building the chips test.luau:1529: a fetch landing lost its rows: recent 0, days 3, dayRows 3
a repaint re-arms focusOnce test.luau:1574: a repaint after the open one must not re-request focus
apply silently drops the draft field test.luau:1573: the keystroke reached state without a repaint of its own
render writes state again (draftKey) test.luau:869: idle, nothing fetched: draftKey -- "tag-26" vs "tag-27" (pre-existing comparison)
the stale-walk guard removed test.luau:636 (PR #6's assert)
the stale-total guard removed test.luau:640 (PR #6's assert)
viewModel writes state.matching test.luau:1207 (pre-existing pager assert)
commit stops publishing the window test.luau:692 (pre-existing row-click assert)

Review fixup #47, at cd3395b:

mutation first failure before the fixup
setFilter bypasses commit (four state.<field> = writes + render(viewModel())) test.luau:1424: Enter in the search field: the published match count is not the painted model's printed ok, exit 0 — the gate's awk was the only thing that saw it
local dropped from chartPeak inside viewModel (panel.luau:1111) test.luau:1687: viewModel() wrote a module global: chartPeak printed ok, exit 0, on this branch and on main
the envBefore.os seed removed, pristine panel test.luau:1686: viewModel() wrote a module global: os n/a — this is what pins the snapshot as genuinely early
adoptState commits instead of applying (panel.luau:685) test.luau:1415: an open with the bar widget publishing painted 2 times, not 1 not reachable: the harness's state.get returned nil on every open
adoptState drops the adopted tag (panel.luau:688) test.luau:1650: the interval adopted off the bar widget reached state without a tree of its own same
state.recent = {} planted at viewModel's tail test.luau:1568: a fetch landing lost its rows: recent 0, days 3, dayRows 3 unchanged — the kept half of the purity pair, re-verified live

Honest note on that split: five of these are caught by the pre-existing 1485
comparisons before the new asserts run. That is belt and braces, not redundancy —
the counter catches what the comparisons structurally cannot (a paint that
happens or does not happen with the correct tree either way), and the comparisons
catch what the counter cannot.

Gate

$ nix shell nixpkgs#luau -c luau test.luau
viewModel: 14 states, 1485 field comparisons
ok

$ noctalia plugins lint timew          # from .worktrees/t6-state
yuris/timew
  ok

0 errors, 0 warnings

$ git diff --stat main...HEAD --name-only
test.luau
timew/panel.luau

$ grep -c 'getfenv\|setfenv' timew/panel.luau
0

$ luau treediff.luau
states: 32   differences: 0   tree-identical

$ luau repaints.luau
TOTAL   39 paint  10 cmd     37 paint   8 cmd
repaints: main 39 -> branch 37 (-2); paths that got worse: 0

$ git status --porcelain     # both worktrees

Non-goals held

bar.luau untouched. No layout, geometry, handler name, settings key, IPC event
or translation key changed — the 32-state byte-for-byte tree identity is the
proof. The deferred listOffset backwards-range bug is still reproduced, not
fixed: state 13 of the suite still reads 17-16 / 12, and the day 99 route
into it still paints nothing.

What to screenshot after merge

The two paths whose intermediate frame changed are the only visual difference
in the PR, and both are improvements you can only see live:

  1. Type a tag in the search box and press Enter, from day scope. main flashed
    a day-scope list under the new chip before switching to all-time. Should now
    go straight to the all-time list, one frame.
  2. Same, but already in all-time scope. Same single frame, and only one DOM
    walk fired.

Then the states where nothing should have changed at all:

  1. Panel open while idle, before any fetch lands — --:--, --, focus in the
    tag field (the focus request is the one thing commit now consumes).
  2. Panel open while tracking a multi-word tag, ticking — the clock must still
    move once a second and match the bar widget.
  3. Click a day bar with nothing tracked on it — Nothing tracked, dated scope
    button, and the selection should move when the list does, not before.
  4. All-time scope with a tag chip, page 4 of 6 — chip, strip base 2, range label.
  5. Clear the chip from all-time scope — list widens, scope stays all-time.
  6. noctalia msg plugin yuris/timew:panel all day 99 from a painted page 4, then
    click the button painted "2" — the deferred bug, unchanged.
  7. Filter to a tag in day scope, then click the one row left on screen — the
    state.shown publication finding 1 is about. On a correct build the chip does
    not move; the bypass mutant moved it to focus and made 'next page' report
    "page 2 of 1, showing 9-8 of 5".
  8. Panel open, idle, with the bar widget publishing — onOpen's apply and
    adoptState's apply both cashing in on the same commit: the caret lands in
    the tag field exactly once and the adopted interval is on that first tree.
Wave 6, the last structural one: the panel's scattered mutable module locals become one `state` table, and every repaint goes through a single `commit`. **Review #47 fixup (`cd3395b`).** Four findings, all applied; the panel change is comments only. Two of them were claims this description and `test.luau`'s comments made that the code did not back: the bypass invariant was grep-only where the suite said it was test-enforced, and PR #8's globals snapshot never caught the dropped `local` its comment advertised (it was taken after the model had already created the global, so the loop compared the table against itself). Both are closed and mutation-proved. `adoptState`'s `apply` — the one site the repaint table never reached — is now measured and driven in the suite. The invariant awk block below is re-quoted at the current commit. ## What moved **One `state` table.** The twenty locals named in the ticket, plus `elapsedText` — it is view state by every test that matters (mutated by a watch, read by the model, shown on the tree), and leaving it outside would have meant `viewModel` reading `state` *and* one stray local. `poke` and `watching` stay module locals and are commented as such: neither reaches the view model, so neither is a reason to repaint, and putting them in `state` would create fields `commit` must never be handed. Constants are untouched: `ROWS_MAX`, `DAYS`, `RECENT_MAX`, `PAGE_BUTTONS`, the widths and heights, `UNTAGGED`, `TAG_COLORS`. **Two functions own the table.** - `apply(patch)` writes `state` and never paints. Six call sites, each one a path that must not paint where it writes: `loadDay` (the day's walk lands the repaint — a bar click painted once before this change and paints once now), `fetchDays` (applies the totals, reloads the day, commits once), `adoptState` (inside `refresh`, whose callers paint later or not at all — measured separately, since the repaint table below never reaches it), `onOpen` (defaults applied so the refetches are issued against day scope and page 1, then one commit at the end), and `onSearchChange` / `onDraftChange` (the input widget is already showing the keystroke; neither painted before). - `commit(patch)` applies and paints exactly once. Eighteen call sites. It is the only caller of `render` in the file. One semantic narrowing against the twenty assignments `apply` replaces, now written on its comment: it iterates `pairs(patch)`, so a patch field whose value is `nil` is *not written* where the old assignment would have cleared the field. No producer can emit one today (`parseActive` returns literals or `Parse.unquote`, which falls back to its argument; every other parser returns a table or a string), and the closed-field-set assert covers misspelling rather than this. A field that needs clearing gets its zero value in the patch, the way `watchState`'s idle branch does (`patch.startEpoch, patch.tags = 0, ""`). Two functions rather than one because the alternative is worse in the direction the ticket cares about: forcing those six paths through `commit` adds repaints (a keystroke would rebuild the whole tree per character). The invariant the tests and the gate pin is the one that matters — **`state.<field> =` appears inside `apply` and `commit` and nowhere else**: ``` $ awk '/state\.[A-Za-z_]+ *,? *(state\.[A-Za-z_]+ *)?= *[^=]/ || /state\[[a-z]+\] *= *[^=]/' timew/panel.luau 345: state[field] = value -- apply 367: state.shown, state.matching = m.windowRows, m.pager.total -- commit 373: state.focusOnce = false -- commit ``` That grep sees the *source* shape and nothing else, and one bypass shape does not have that shape: `commit` publishes `state.shown` as the model's own `windowRows` table rather than a copy, so a region builder that mutated the model in place would move state without ever writing `state.<field>`. Nothing does (`grep -nE '\bm\.[A-Za-z_]+ *=[^=]|table\.(insert|remove|move|sort)\(state'`: no matches, and main aliased the same table identically), and it is now written on the `state` table's own comment. The complementary check — that every path *publishes what it painted* — is in the suite; see "New asserts" below. **`render` writes nothing.** It takes the model as an argument. `commit` builds one model per repaint, publishes `state.shown`/`state.matching` off it at the same point and in the same order `render` used to, hands the model down, and clears `state.focusOnce` after the paint. `focusOnce` belongs to `commit` because the flag's contract is "the *repaint* that carries it to the tree consumes it" — that was `render`'s job only because `render` was the repaint. Now that repaints are explicit and singular, the repainter consumes it, which is the same point in the same order. The observable asymmetry test.luau already pinned (`focus == true` on onOpen's tree, `focusOnce == false` by the time the model can be called) is unchanged and still passes; only the assert's message changed. ## Call-site count The ticket says 45. That is `grep -c render` — every line mentioning `render`, comments included. Actual `render()` call *statements* on main: **19**. After: **1** (inside `commit`), reached through 18 `commit` and 6 `apply` call sites. ## Repaints, per path, before and after Measured, not reasoned about: main's `panel.luau` and this branch's are loaded in one luau process, each with its own global table and identical stubs, driven through the same path, with `panel.render` counted. `cmd` is commands queued or landed (negative = landed). Harness: `/tmp/pr9/h/repaints.luau`. ``` path main branch -------------------------------------------------------------------------------------- == panel open onOpen (defaults set, every fetch issued) 1 paint 4 cmd 1 paint 4 cmd fetchActive lands 1 paint -1 cmd 1 paint -1 cmd fetchTotal lands 1 paint -1 cmd 1 paint -1 cmd fetchTags lands 1 paint -1 cmd 1 paint -1 cmd fetchDays lands (reloads the day) 1 paint 0 cmd 1 paint 0 cmd the day walk lands 1 paint -1 cmd 1 paint -1 cmd == tick and bar state second tick while tracking (update) 1 paint 0 cmd 1 paint 0 cmd bar publishes active + startEpoch + tags 3 paint 0 cmd 3 paint 0 cmd bar publishes idle (clears the interval) 1 paint 0 cmd 1 paint 0 cmd bar publishes elapsedText 1 paint 0 cmd 1 paint 0 cmd == clicks click a day bar (onDay3) 0 paint 1 cmd 0 paint 1 cmd its day walk lands 1 paint -1 cmd 1 paint -1 cmd click a day the chart does not hold (day 99) 0 paint 0 cmd 0 paint 0 cmd click a row's tag (onRow2 -> setFilter) 1 paint 1 cmd 1 paint 1 cmd dayTotals lands (bars follow the tag) 1 paint 0 cmd 1 paint 0 cmd its day walk lands 1 paint -1 cmd 1 paint -1 cmd clear the chip (onClearFilter) 1 paint 1 cmd 1 paint 1 cmd == scope switch scope -> all time (onScopeAll) 1 paint 2 cmd 1 paint 2 cmd the all-time walk and range total land 2 paint -2 cmd 2 paint -2 cmd scope -> this day (onScopeDay) 1 paint 0 cmd 1 paint 0 cmd == pager page number (onIpc page 4) 1 paint 0 cmd 1 paint 0 cmd next page (>) 1 paint 0 cmd 1 paint 0 cmd previous page (<) 1 paint 0 cmd 1 paint 0 cmd a named strip button (onPage1) 1 paint 0 cmd 1 paint 0 cmd == search box keystroke in the search field (onSearchChange) 0 paint 0 cmd 0 paint 0 cmd Enter in the search field, day scope (onSearchSubmit) 2 paint 3 cmd 1 paint 3 cmd <-- fewer its dayTotals and all-time landings 4 paint -3 cmd 4 paint -3 cmd Enter in the search field, already all-time 2 paint 5 cmd 1 paint 3 cmd <-- fewer == tag field and the two mutating commands keystroke in the tag field (onDraftChange) 0 paint 0 cmd 0 paint 0 cmd Enter in the tag field (onDraftSubmit -> timew start) 1 paint 1 cmd 1 paint 1 cmd timew start lands -> refresh issues every fetch 0 paint 3 cmd 0 paint 3 cmd refresh's five landings 5 paint -4 cmd 5 paint -4 cmd the stop button (onStop -> timew stop) 0 paint 1 cmd 0 paint 1 cmd timew stop lands -> refresh issues every fetch 0 paint 3 cmd 0 paint 3 cmd -------------------------------------------------------------------------------------- TOTAL 39 paint 10 cmd 37 paint 8 cmd repaints: main 39 -> branch 37 (-2); paths that got worse: 0 ``` **The two paths that were paying twice, named:** 1. **Enter in the search field, day scope.** main: `setFilter` committed the tag and painted under the old scope, then `onSearchSubmit` moved the scope to all-time and painted again — the first of those two trees showed the new chip over a day-scope list nobody asked for. Now the scope rides in `setFilter`'s patch: **2 → 1**. 2. **Enter in the search field, already in all-time scope.** Same two paints, and main also issued the all-time walk twice: `setFilter` issued it (scope was already `all`), then `onSearchSubmit` issued it again for the same tag. **2 paints → 1, and 5 commands → 3** — the command dropped is a 200-id DOM walk, the most expensive thing this panel asks timew for. Everything else is identical, including the deliberate zeros: a bar click still paints when its intervals arrive rather than before (`loadDay` applies, the walk commits), a keystroke still paints nothing, and `day 99` — the route to the deferred backwards-range bug — still returns without painting or fetching. The async landings are not merged and must not be: `refresh`'s five fetches land at five different times, so they are five logical changes and five repaints, on both sides. Merging them means holding a paint until the slowest walk returns. **One `apply` site this table does not cover, and its own measurement.** The harness's `noctalia.state.get` returns `nil`, so `adoptState` takes its `a == nil` early return on all 38 steps above and the `apply` under it never runs — "0 paths worse" is true but says nothing about that site. Measured with a bar widget that actually publishes: ``` == the bar widget is publishing, so adoptState succeeds and fetchActive is skipped == onOpen (defaults + refresh + commit) stop lands -> refresh main 1 paint [C:dayTotal C:tags C:dayTotals P] 0 paint branch 1 paint [C:dayTotal C:tags C:dayTotals P] 0 paint adoptState commits 2 paint [P C:dayTotal C:tags C:dayTotals P] 1 paint ``` Committing there costs **+1 paint per open and +1 per mutating command**, and the first tree is the same either way. So the site is justified by measurement, not by argument — and that path is now driven in the suite rather than only here (see "New asserts"). ## Timing Same harness as PR #9's review (`/tmp/pr9/h/cost3.luau`), worst state: 200 all-time rows, page 4, min of 9 interleaved rounds x 500 calls, three runs. | | main | branch | |---|---|---| | repaint | 0.0512 / 0.0513 / 0.0515 ms | 0.0509 / 0.0518 / 0.0510 ms | | `viewModel()` | 0.0180 ms | 0.0179 ms | | pure-core calls per repaint | 19 (= 19 per model) | 19 (= 19 per model) | | heap per repaint | 57 KiB min / 58 median | 57 KiB min / 58 median | | ui nodes / props tables | 104 / 104 | 104 / 104 | Per-repaint cost is unchanged: the delta is ±1% and changes sign between runs (-1.0%, +0.9%, -0.6%), i.e. noise. 0.20–0.21% of the 25 ms per-handler budget on both sides. The patch table `commit` allocates does not register against a tree of 104 nodes. The path that lost a repaint, timed end to end (Enter in the search field from all-time scope, min of 9 x 500, three runs): | | main | branch | |---|---|---| | per submit | 0.0658 / 0.0669 / 0.0662 ms | 0.0347 / 0.0345 / 0.0346 ms | | of the 25 ms budget | 0.26% | 0.14% | **-47%** on that handler, and one fewer 200-id shell walk behind it. ## Tree identity: 32 states, 0 differences PR #9's harness (`/tmp/pr9/h/treediff.luau`), unchanged except for the branch name in its banner. Both panels in one process, own global tables, identical stubs, props key-sorted, flattened one line per node, compared byte for byte. ``` tree identity: main vs refactor/t6-state states: 32 bytes: main 244419, branch 244419 node lines compared: 2372 per side differences: 0 tree-identical ``` ## The async guards PR #6's reproduction, re-run: issue the all-time walk under `focus`, change the tag to `email` mid-flight, land the fresh walk, then land the stale one. The guards moved from above the assignment to above the `commit`, which is the same place semantically — but a refused *patch* is now also a repaint that never happens, so there is a second, sharper thing to assert: ```lua paintsFor("a walk issued under a tag the panel has left", 0, function() raceWalk.cb({ stdout = DOM }) end) paintsFor("and that walk's all-time total", 0, function() raceTotal.cb({ stdout = "9:11:00" }) end) ``` The pre-existing content asserts (test.luau:659, :663) still pass unchanged. The new count catches a failure mode they cannot see: a guard that drops the patch but paints anyway. Mutation-proved below. ## How the test suite still reaches state The ticket's premise needs one correction, and it is load-bearing for reviewing this PR: **test.luau never drove state by assignment, because it never could.** `panelEnv = getfenv(panel.Fmt.clock)` is the module's *globals* table. The state locals were `local`, i.e. upvalues, and Luau ships no `debug.getupvalue` — the suite says so itself (:1030 on main). It drives state through the handlers the shell calls (`onOpen`, `onIpc`, `onRowN`, `onPageN`, `update`, the four `noctalia.state.watch` callbacks) and reads it back off the rendered tree and `panel.viewModel()`. So the mechanism needs no adaptation to keep working, and the 14 states and 1485 field comparisons pass unchanged. What *did* change: - **`state` is now exported** (`return { ..., state = state }`, the table already documented "exported for test.luau only"). One value, so for the first time the suite can read the panel's whole mutable surface directly. It is read there, never written: writing it would be the one bypass of `commit` in the repo. - **PR #8's env-key snapshot was moved, because it never caught what it claimed to.** It is supposed to catch a `local` dropped inside `viewModel` landing a *new global* in the module's globals table — a class the state export cannot see (a name that never reached the table is not a field of it). It did not, on this branch or on main: `panelEnv` **is** the module's own globals table, taken at `test.luau:492`, so a global the model writes exists from the model's first call onward, and a snapshot taken further down (beside the viewModel section, after the panel has been driven) had already recorded the name. The loop at the bottom compared the table against itself. Dropping `local` from `chartPeak` inside `viewModel` printed `ok` on both sides. It now snapshots immediately after the `panelEnv.panel` stub, before the first handler runs, with `os` — the only key the table legitimately gains afterwards, from the frozen-clock stub — seeded by hand. Drop that seed and the pristine suite fails on `os`, which is what keeps the snapshot honest about being early. `state` is a `local` created at load time, so it still adds no key. The hole was PR #8's rather than this wave's, but this wave rewrote the comment that asserted it, so it is fixed here. - **The other half is now asserted by value**, which is new capability rather than a replacement: dump `state`, call `viewModel()` five times, dump again. Plus the landed values by name (`#state.recent == 4`, `state.total`, `state.tags`, ...), because a before/after comparison alone passes a model that *flattens* a field — the first call flattens it, every later call finds it already flat, and idempotence is not purity. That hole was real: a `state.recent = {}` planted in `viewModel` passed the whole suite until those asserts existed. ### Asserts whose shape changed | assert | change | why | |---|---|---| | test.luau:1070 focus one-shot | message only: "its own render cleared it" → "the commit that painted it cleared it" | the consumer moved from the end of `render` to the end of `commit`; both asserts still pin the same observable | | test.luau env snapshot | moved, and seeded with `os` | it never caught the dropped `local` its comment claimed; see above | No assert was weakened or removed. 354 lines added. ### New asserts - **`commit` paints exactly once per call**, path by path, wrapping the stub's `panel.render` with a counter: 1 for a page click, a scope click, a filter set, Enter in either field, each fetch landing, each tick while tracking; 0 for a keystroke, a bar click (its walk pays), `day 99`, and a tick while idle. - **Nothing bypasses `commit`, in the two halves that are reachable from here.** A path that mutates without committing paints 0 and the tree falls behind the state — the counter above sees that. A path that writes `state` directly *and paints anyway* renders a correct tree, so neither the counter nor any tree comparison in the file can see it; what it drops is the rest of `commit`. So after every counted paint, `paintsFor` also asserts that `state.shown` and `state.matching` are the painted model's own window and total and that `state.focusOnce` was cleared. That makes all 23 counted paths bypass checks. The mutant that motivated this — `setFilter`'s `commit(patch)` replaced by the four `state.<field> =` writes plus `render(viewModel())` — used to print `ok`, and is not benign: it leaves the previous window published, so clicking the one row left after filtering to `admin` moves the chip to `focus`, and 'next page' reports "page 2 of 1, showing 9-8 of 5". What stays out of the suite is the *source* shape (`state.<field> =` only inside `apply` and `commit`). The luau CLI ships no `io` library — test.luau notes the same at `:743` for en.json — so that grep runs in the gate; its output is at the top of this description, and the shape it cannot see is noted there too. - **`adoptState`'s success path is driven, so the sixth `apply` is counted too.** It was the one panel path the counter never reached: `noctalia.state.get` returns `nil` in this harness, so the `a == nil` early return fired on every open. With the bar widget publishing, `refresh` adopts instead of issuing `fetchActive`, and the assert is that adopting costs no tree of its own — one paint for the open, the adopted interval already on it (the action button reads `onStop` and the subject line reads the adopted tag, both before any fetch has landed), then the four landings one paint each. `adoptState` committing instead makes that open paint twice. - **`commit` publishes the model's own window and match count** — asserted against `viewModel()` rather than a literal, since the claim is that they are not a second derivation. - **The field set of `state` is closed.** `apply` writes `state[field]`, so a patch with a misspelled field would grow a silent twenty-second field while the model went on reading the old one. This is the hazard the refactor introduces in exchange for a greppable state surface, so it gets its own assert. - **A repaint of unchanged state moves no field**, and **a repaint after the open one does not re-request focus** (read off the *second* tree after a keystroke, since a flag set on the way out of one paint only reaches the next). - **The stale-landing guards cost zero repaints** (above). - Four commands for one Enter in the search field, not six. ### Mutation proof Every new assert broken, run, restored, `sha256` compared (all restores byte-identical; the suite is green on the pristine tree before and after). **Line numbers below are at `84d4201`**, the commit these were measured at — `git checkout 84d4201` reproduces them verbatim. The review fixup that follows adds ~104 lines to `test.luau` and moves them; the assert message in each row is the stable identifier. The fixup's own mutations are in the table after this one. | mutation | first failure | |---|---| | `commit` calls `render` twice | `test.luau:1391: Enter in the search field painted 2 times, not 1` | | `onScopeDay` applies instead of committing | `test.luau:1391: the scope switch back to this day painted 0 times, not 1` | | `onSearchSubmit` restored to main's two-paint shape | `test.luau:1391: Enter in the search field painted 2 times, not 1` | | stale landing drops the patch but paints | `test.luau:1391: a walk issued under a tag the panel has left painted 1 times, not 0` | | a patch misspells `searchKey` (nothing else reads it) | `test.luau:1509: a patch grew state a field no declaration has: searchKeyy` | | `commit` publishes a stale match count | `test.luau:1545: the published match count is not the model's: 4 vs 3` | | `fetchTotal`'s patch names `allTotal` | `test.luau:1534: the day total and the live interval are the fetches' own, after five model calls` | | `viewModel` clears `recent` after building the chips | `test.luau:1529: a fetch landing lost its rows: recent 0, days 3, dayRows 3` | | a repaint re-arms `focusOnce` | `test.luau:1574: a repaint after the open one must not re-request focus` | | `apply` silently drops the `draft` field | `test.luau:1573: the keystroke reached state without a repaint of its own` | | `render` writes state again (`draftKey`) | `test.luau:869: idle, nothing fetched: draftKey -- "tag-26" vs "tag-27"` (pre-existing comparison) | | the stale-walk guard removed | `test.luau:636` (PR #6's assert) | | the stale-total guard removed | `test.luau:640` (PR #6's assert) | | `viewModel` writes `state.matching` | `test.luau:1207` (pre-existing pager assert) | | `commit` stops publishing the window | `test.luau:692` (pre-existing row-click assert) | Review fixup #47, at `cd3395b`: | mutation | first failure | before the fixup | |---|---|---| | `setFilter` bypasses `commit` (four `state.<field> =` writes + `render(viewModel())`) | `test.luau:1424: Enter in the search field: the published match count is not the painted model's` | printed `ok`, exit 0 — the gate's awk was the only thing that saw it | | `local` dropped from `chartPeak` inside `viewModel` (`panel.luau:1111`) | `test.luau:1687: viewModel() wrote a module global: chartPeak` | printed `ok`, exit 0, on this branch *and* on main | | the `envBefore.os` seed removed, pristine panel | `test.luau:1686: viewModel() wrote a module global: os` | n/a — this is what pins the snapshot as genuinely early | | `adoptState` commits instead of applying (`panel.luau:685`) | `test.luau:1415: an open with the bar widget publishing painted 2 times, not 1` | not reachable: the harness's `state.get` returned `nil` on every open | | `adoptState` drops the adopted tag (`panel.luau:688`) | `test.luau:1650: the interval adopted off the bar widget reached state without a tree of its own` | same | | `state.recent = {}` planted at `viewModel`'s tail | `test.luau:1568: a fetch landing lost its rows: recent 0, days 3, dayRows 3` | unchanged — the kept half of the purity pair, re-verified live | Honest note on that split: five of these are caught by the pre-existing 1485 comparisons before the new asserts run. That is belt and braces, not redundancy — the counter catches what the comparisons structurally cannot (a paint that happens or does not happen with the correct tree either way), and the comparisons catch what the counter cannot. ## Gate ``` $ nix shell nixpkgs#luau -c luau test.luau viewModel: 14 states, 1485 field comparisons ok $ noctalia plugins lint timew # from .worktrees/t6-state yuris/timew ok 0 errors, 0 warnings $ git diff --stat main...HEAD --name-only test.luau timew/panel.luau $ grep -c 'getfenv\|setfenv' timew/panel.luau 0 $ luau treediff.luau states: 32 differences: 0 tree-identical $ luau repaints.luau TOTAL 39 paint 10 cmd 37 paint 8 cmd repaints: main 39 -> branch 37 (-2); paths that got worse: 0 $ git status --porcelain # both worktrees ``` ## Non-goals held `bar.luau` untouched. No layout, geometry, handler name, settings key, IPC event or translation key changed — the 32-state byte-for-byte tree identity is the proof. The deferred `listOffset` backwards-range bug is still reproduced, not fixed: `state 13` of the suite still reads `17-16 / 12`, and the `day 99` route into it still paints nothing. ## What to screenshot after merge The two paths whose *intermediate* frame changed are the only visual difference in the PR, and both are improvements you can only see live: 1. **Type a tag in the search box and press Enter, from day scope.** main flashed a day-scope list under the new chip before switching to all-time. Should now go straight to the all-time list, one frame. 2. **Same, but already in all-time scope.** Same single frame, and only one DOM walk fired. Then the states where nothing should have changed at all: 3. Panel open while idle, before any fetch lands — `--:--`, `--`, focus in the tag field (the focus request is the one thing `commit` now consumes). 4. Panel open while tracking a multi-word tag, ticking — the clock must still move once a second and match the bar widget. 5. Click a day bar with nothing tracked on it — `Nothing tracked`, dated scope button, and the selection should move when the list does, not before. 6. All-time scope with a tag chip, page 4 of 6 — chip, strip base 2, range label. 7. Clear the chip from all-time scope — list widens, scope stays all-time. 8. `noctalia msg plugin yuris/timew:panel all day 99` from a painted page 4, then click the button painted "2" — the deferred bug, unchanged. 9. **Filter to a tag in day scope, then click the one row left on screen** — the `state.shown` publication finding 1 is about. On a correct build the chip does not move; the bypass mutant moved it to `focus` and made 'next page' report "page 2 of 1, showing 9-8 of 5". 10. **Panel open, idle, with the bar widget publishing** — `onOpen`'s `apply` and `adoptState`'s `apply` both cashing in on the same `commit`: the caret lands in the tag field exactly once and the adopted interval is on that first tree.
The panel kept twenty mutable module locals and called render() from
nineteen places. A bare local is ungreppable -- `filter` matched a
parameter, a builder argument and a comment as readily as the state --
and several paths painted twice for one logical change.

State is now one `state` table with those twenty fields plus
`elapsedText`, which the view model already read. Two functions own it:
`apply` writes it and never paints, `commit` applies a patch and paints
exactly once. Eighteen commits and six applies replace the nineteen
render() calls, and `render` takes the model as an argument, so it
writes nothing at all: the window, the match count and the one-shot
focus flag are `commit`'s.

`poke` and `watching` stay module locals: neither reaches the view
model, so neither is a reason to repaint.

Search-submit paid two repaints and issued the 200-id DOM walk twice
whenever the panel was already in all-time scope; the scope now rides in
setFilter's own patch, so it is one of each. Every other path keeps the
repaint count it had. PR #6's stale-walk guards move to the patch rather
than the assignment, and refuse a late landing with no repaint at all.

`state` is exported for test.luau, which makes viewModel's purity
checkable by value rather than only through the globals table.
yuris left a comment

Verdict: APPROVE

Re-derived rather than re-read: the invariant grep, the PR #6 race, the 32-state
tree diff, the repaint counter, the timing, and every one of the six apply
sites individually. The panel's behaviour is unchanged, the guard survived the
move, focusOnce lands on the same tree, and one of the two "wins" is a real
bug fix. Four findings, none blocking; two are about what the tests and the
description claim rather than about the panel.

Harnesses: /tmp/pr10/h/ (common.luau mount + order.luau, race.luau,
bypass.luau, focus.luau, adopt.luau), driven against pmain.luau /
pbranch.luau rebuilt from git show main: and git show refactor/t6-state: --
cmp says both are byte-identical to the copies your harness used.


Findings

1. The bypass invariant is grep-enforced, not test-enforced -- and the suite says otherwise (P2)

test.luau:1388 -- paintsFor

The new section's second claim is "Nothing bypasses commit: same counter,
from the other side". The counter sees only the half where a path mutates and
fails to paint. The other half -- a path that writes state directly and
paints anyway -- passes the whole suite, because the tree it renders is correct.
What it silently drops is the other two things commit does at :348 and
:354.

My mutant, setFilter's commit(patch) replaced by the writes it stands for:

local function setFilter(tag, extra)
	state.filter = Parse.trim(tag)
	state.search = state.filter
	state.searchKey = state.searchKey + 1
	state.listOffset = 0
	if extra then
		for field, value in pairs(extra) do
			state[field] = value
		end
	end
	render(viewModel())
	fetchDays()
	...

The gate catches it. The suite does not:

$ awk '/state\.[A-Za-z_]+ *,? *(state\.[A-Za-z_]+ *)?= *[^=]/ || /state\[[a-z]+\] *= *[^=]/ { print NR": "$0 }' nb_bypass.luau   # your gate's own awk
326: 		state[field] = value
348: 	state.shown, state.matching = m.windowRows, m.pager.total
354: 	state.focusOnce = false
843: 	state.filter = Parse.trim(tag)
844: 	state.search = state.filter
845: 	state.searchKey = state.searchKey + 1
846: 	state.listOffset = 0
849: 			state[field] = value

$ luau test.luau
viewModel: 14 states, 1485 field comparisons
ok

It is not a benign mutant. commit also publishes the window, and without that
the slice onRowN indexes is the previous one (/tmp/pr10/h/bypass.luau):

== filter to 'admin' in day scope, then click the one row on screen ==
  branch  tree shows rows[admin]  pager.total=1
  branch  after clicking row 1, the chip reads: "admin"
  bypass  tree shows rows[admin]  pager.total=1
  bypass  after clicking row 1, the chip reads: "focus"

== the pager clamp reads the same stale publication ==
  branch  after filter->admin then 'next page': page 1 of 1, showing 1-5 of 5
  bypass  after filter->admin then 'next page': page 2 of 1, showing 9-8 of 5

The tree diff cannot see it either -- I ran treediff.luau against this exact
mutant and it reports tree-identical, correctly: the trees are identical, the
state the next click reads is not.

I accept that the suite cannot self-grep; I verified the reason rather than
taking test.luau:743 on trust:

$ luau -e 'print(type(io), type(os.getenv), type(loadstring), type(require))'
nil	nil	function	function
$ luau -e 'local t={} for k in pairs(debug) do t[#t+1]=k end print(table.concat(t,","))'
info,traceback

No io, no os.getenv, and debug is info/traceback only, so there is no
route to the file's text. Fine. But then the claim in the section header has to
shrink to what the counter actually proves.

Two ways to close it, and the second is cheap enough that I would rather have it
than the disclaimer. Fold commit's own three writes into paintsFor, which
already holds the painted state at exactly the right moment:

local function paintsFor(what, expected, fn)
	paints = 0
	fn()
	assert(paints == expected, what .. " painted " .. paints .. " times, not " .. expected)
	-- A path that wrote state and painted without going through `commit` leaves
	-- the published window behind: the tree is right, the slice onRowN indexes is
	-- not. Nothing else in this file can see that.
	if paints > 0 then
		local m = panel.viewModel()
		assert(panel.state.matching == m.pager.total, what .. ": the published match count is not the painted model's")
		assert(panel.state.focusOnce == false, what .. ": a paint left the focus request armed")
		assert(#panel.state.shown == #m.windowRows, what .. ": the published window is not the painted model's")
		for i, row in ipairs(m.windowRows) do
			assert(panel.state.shown[i] == row, what .. ": published window row " .. i .. " is not the model's")
		end
	end
end

Applied, on the pristine tree and on the mutant:

pristine: viewModel: 14 states, 1485 field comparisons / ok
bypass:   ./test.luau:1397 function paintsFor  <- "Enter in the search field: the published match count is not the painted model's"

That turns your 23 counted paths into 23 bypass checks for ten lines and no new
mechanism. Whatever you do with it, the grep in the gate is still the only thing
that sees the source shape, and the section comment should say so rather than
say "nothing bypasses commit".

One bypass shape neither the grep nor the suite covers, worth a sentence in the
state comment since the whole design rests on that grep: state.shown is
m.windowRows, the same table, so a builder that mutated the model would mutate
state without ever matching state.<field> =. Nothing does today (grep -nE '\bm\.[A-Za-z_]+ *=[^=]|table\.(insert|remove|sort)\(state' on the branch: no
matches), and main aliased it identically. Not introduced here; just the one
hole in the invariant's shape.

2. The rewritten globals-snapshot comment claims coverage the snapshot does not have (P2)

test.luau:1030-1041

The comment this PR rewrites now says the table "answers half of that -- an
accidental global, from a local dropped inside the model, lands the name in
here", and the description repeats it as "PR #8's env-key snapshot is untouched
and still meaningful.
It catches a dropped local inside viewModel". It does
not catch it, on this branch or on main.

$ sed -i '1092s/local chartPeak = /chartPeak = /' timew/panel.luau   # branch
$ luau test.luau
viewModel: 14 states, 1485 field comparisons
ok

$ sed -i '1011s/local chartPeak = /chartPeak = /' timew/panel.luau   # main, same mutation
$ luau test.luau
ok

The global is real -- it just predates the snapshot. panelEnv is taken at
:492 and the panel is driven from there on, so viewModel has run some
hundreds of lines before envBefore is filled at :1038:

[probe] chartPeak already a module global at snapshot time: true

(probe = one print inserted immediately above local envBefore = {}, against
the mutant.) So the loop at the bottom can only ever catch a global whose first
write happens after :1038, which for viewModel is none of them.

The fix is to snapshot before the first handler runs rather than before the first
open(). The only key the panel's globals table legitimately gains after the
stubs is os, added by the panelEnv.os stub further down, so it is seeded by
hand:

local envBefore = {}
for k in pairs(panelEnv) do
	envBefore[k] = true
end
-- The os stub below is installed later, at the viewModel section; it is a key
-- this table gains legitimately.
envBefore.os = true

moved to sit immediately after panelEnv.panel = { ... } (:535), with the
original block at :1038-1041 deleted. Verified both directions:

pristine: viewModel: 14 states, 1485 field comparisons / ok
dropped `local chartPeak`: ./test.luau:1597  "viewModel() wrote a module global: chartPeak"

The hole is PR #8's, not this wave's -- but this wave rewrites the comment that
asserts it and repeats the claim in the description, and comments are
load-bearing here. Worth noting that the other half is genuinely new and
genuinely works: dump(panel.state) either side of five calls fails as advertised
(state.recent = {} planted in viewModel -> test.luau:1529), so after this
fix both halves would actually be covered.

3. The repaint harness never reaches adoptState's apply (P3)

timew/panel.luau:663-672

repaints.luau's noctalia.state.get returns nil, so adoptState takes the
a == nil early return on all 38 measured steps and the apply under review is
never executed. "0 paths worse" is true, but it does not cover this site --
routing it through commit changes nothing in that table:

apply@666 -> commit:  repaints: main 39 -> branch 37 (-2); paths that got worse: 0

The conclusion survives; I re-measured it with a bar widget that actually
publishes (/tmp/pr10/h/adopt.luau):

== the bar widget is publishing, so adoptState succeeds and fetchActive is skipped ==
                       onOpen (defaults + refresh + commit)   stop lands -> refresh
main                   1 paint  [C:dayTotal C:tags C:dayTotals P]   0 paint
branch                 1 paint  [C:dayTotal C:tags C:dayTotals P]   0 paint
adoptState commits     2 paint  [P C:dayTotal C:tags C:dayTotals P] 1 paint

+1 paint per open and +1 per mutating command. So the site is justified -- but by
this measurement, not by the one in the description. Either give repaints.luau
a publishing state.get, or say in the PR body that adoptState is argued
rather than measured.

4. The quoted awk output in the description is two lines off (P3)

timew/panel.luau:322-328

The description's invariant block reads 324 / 346 / 352. At 84d4201 the
lines are 326 / 348 / 354:

$ awk '/state\.[A-Za-z_]+ *,? *(state\.[A-Za-z_]+ *)?= *[^=]/ || /state\[[a-z]+\] *= *[^=]/ { print NR": "$0 }' timew/panel.luau
326: 		state[field] = value
348: 	state.shown, state.matching = m.windowRows, m.pager.total
354: 	state.focusOnce = false

Same three sites, same conclusion; it is the one piece of output in the
description that is not reproducible verbatim, and it happens to be the PR's
central evidence. It is not a stale commit either -- the branch is one commit,
and state[field] = value is at 326 in it.


Verified

The invariant holds, including the forms a naive grep misses. state[field] =
only at :326; state.<field> = only at :348 and :354. No rawset
anywhere in the repo; no state["literal"] =; no state.X[...] =; no
table.insert/remove/move/sort(state...); no alias binding the table to another
name (state = state at :1689 is the export); one render( call statement, at
:349. grep -c 'commit(' = 19 minus the definition = 18 commit sites, and
six apply sites at 666 / 734 / 761 / 985 / 999 / 1614. Your "19 on main" is
right too: awk over non-comment lines on pmain.luau gives exactly 19 render()
statements at 618 622 626 633 642 649 659 671 696 715 722 767 852 859 870 916 927 1547 1555.

Async ordering: the PR #6 race does not come back, and the split adds no new
one.
Reproduction re-run per PR #6 (/tmp/pr10/h/race.luau), all-time under
focus, filter to email mid-flight, stale walk last:

== R1: PR #6's stale-walk race, re-run on this branch ==
  main     stale walk landed:   rows[email] allTotal=3:22 chip=email scope=all   paints=0
  branch   stale walk landed:   rows[email] allTotal=3:22 chip=email scope=all   paints=0

== NEGATIVE CONTROL: rows guard removed from the branch ==
  branch   stale walk landed:   rows[focus,focus] allTotal=3:22 chip=email scope=all   paints=1

R2 lands them in the other order, R3 races two day walks, R4 lands a stale
dayTotals, R5 checks a keystroke reaching the tree on the next unrelated
repaint, R6 checks day 99 -- every one identical on both sides, including
the two that are wrong on both (R3 leaves day 2 selected showing day 3's rows;
R4 lands the stale bars, fetchDays taking no wanted guard). Pre-existing,
byte-for-byte the same shape as main, not this PR's to fix -- but R3 is the
sharper of the two and is the same class PR #6 closed for fetchAll.

No apply site leaves state unpainted longer than main did. I compared the
full paint | command interleaving per handler rather than the state after it
(/tmp/pr10/h/order.luau, 24 scenarios). Nine streams differ; on eight of them
the commands are identical and the branch's paint lands earlier:

scenarios: 24   full-stream differences: 9   COMMAND-STREAM differences: 1
paint moved EARLIER on 8 paths, LATER on 0

The one command-stream difference is the win, and it is a bug fix, not a
refactor -- worth saying so plainly in the description:

onSearchSubmit, all-time scope
  main   C:dayTotals C:ALLWALK C:allTotal P C:ALLWALK C:allTotal P
  branch P C:dayTotals C:ALLWALK C:allTotal

main issues the 200-id DOM walk and its range total twice for one Enter.
That is a duplicated ALL_WALK_MAX-bounded shell walk on the panel's most
expensive query, on a keypress. Independent of the repaint story.

The six apply sites, ruled on individually. Each converted to commit in
turn and re-run through repaints.luau (baseline: branch 37 paints, 0 worse):

site as commit verdict
loadDay :761 43 paints, 6 paths worse apply. A bar click would paint before its intervals exist, and day 99 would paint a day the chart has no entry for.
fetchDays :734 41 paints, 4 paths worse apply. The totals must land before loadDay can index them; committing splits one landing into two trees.
onSearchChange :985 39 paints, 1 worse apply. One whole-tree rebuild per character.
onDraftChange :999 38 paints, 1 worse apply. Same.
onOpen :1614 38 paints, 1 worse apply. Two trees on every open.
adoptState :666 37 paints, 0 worse in that harness apply -- but see finding 3; measured properly it is +1 per open and +1 per mutating command.

6/6 genuinely justified, so the two-function split earns itself: this is not
apply existing because commit felt too blunt once. refresh's five landings
staying five repaints is right for the same reason -- merging them means holding
the tree until the slowest walk returns. The one thing I would still write down
is that apply iterates pairs(patch), so a patch field whose value is nil is
silently not written where the twenty assignments it replaces would have
cleared the field. No producer can emit nil today (parseActive returns
literals or Parse.unquote, which falls back to its argument; every other
parser returns a table or a string), so there is nothing to fix -- but the
closed-field-set assert covers misspelling and not this, and it is the one
semantic narrowing the rewrite makes.

focusOnce is carried by the same tree and consumed once
(/tmp/pr10/h/focus.luau, focus-per-paint recorded across the whole open):

F1 idle open + five landings + tick + two scope clicks
  main    [1,0,0,0,0,0,0,0]   trees carrying focus: 1
  branch  [1,0,0,0,0,0,0,0]   trees carrying focus: 1
F2 reopened while already open
  main / branch  [1,0,0,0,0,0,0,0,1,0]   2
F3 host replays watch values at registration (a paint *inside* onOpen)
  main / branch  [1,0]   1
F4 host calls runAsync back inline (every fetch lands inside onOpen)
  main / branch  [1,0,0,0,0]   1

focus sequence differences: 0

F3 and F4 are the two hosts that could paint between apply({focusOnce = true})
and onOpen's commit(); both sides move the flag to the same tree at the same
index. And the asserts that pin it are not vacuous -- onOpen applying
focusOnce = false fails test.luau:1059, commit not clearing it fails
test.luau:1060.

Tree identity and cost reproduce. pmain.luau/pbranch.luau rebuilt from
git show, cmp-identical to yours:

tree identity: main vs refactor/t6-state
states: 32   bytes: main 244419, branch 244419   node lines compared: 2372 per side
differences: 0
tree-identical

TOTAL   39 paint  10 cmd     37 paint   8 cmd
repaints: main 39 -> branch 37 (-2); paths that got worse: 0
  main    0.0659 ms per submit   branch  0.0345 ms   (-47.7%)

Not vacuous either: ROWS_MAX 8 -> 6 reports node COUNT main 103 branch 91,
and deleting commit's state.focusOnce = false reports the input's
focus=boolean:true against main's false. Timing over three runs is noise with
a sign flip, matching your claim: -1.3% / -0.5% / +1.1%, 0.21% -> 0.21% of
the 25 ms budget.

The suite still drives everything it drove. The panelEnv.* handler set is a
strict superset -- nothing dropped, five added directly (onClearFilter,
onDraftChange, onScopeDay, onSearchChange, onSearchSubmit); one assert
line changed and it is the focus message. And with apply/commit instrumented
to log every field write over a full run, all 21 declared fields move:

active 19  tags 19  startEpoch 19  total 19  recent 20  filter 29  mode 34
days 22  selected 45  dayRows 23  allRows 10  allTotal 10  shown 201
matching 201  listOffset 77  draft 21  draftKey 20  search 30  searchKey 29
focusOnce 221  elapsedText 1
every declared field is driven by the suite

elapsedText at 1 is thin, but it is the watch's only writer on both sides.

Your new asserts fire where you say. Independently re-run: viewModel
clearing recent -> :1529; a patch misspelling a field -> :1509; commit
painting twice -> :1391.

Gate, from the branch worktree. luau test.luau -> ok. noctalia plugins lint timew -> 0 errors, 0 warnings. No getfenv/setfenv in timew/. Every
onClick is still a name off the model, no closures. git status --porcelain
empty in both worktrees; everything I broke was broken in /tmp/pr10/work and
/tmp/pr10/h, and each restore was cmp-checked.


Not checked

  • The live shell. Everything above is under stubs, so runAsync's real
    scheduling, the real noctalia.state.watch replay semantics and the real
    widget's handling of focus/key are inferred. F3/F4 in focus.luau are my
    attempt to bracket the two that matter, but they are models of a host, not the
    host.
  • The deferred backwards-range bug behind day 99. Reproduced identically on
    both sides (R6: 0 paints on the click, dayTitle -- on the next tick), not
    investigated.
  • Parse/Query/Timew/the region builders. Waves 1-5 territory; I only
    confirmed viewModel and render read state and write nothing.
  • Whether poke/watching staying outside the table is right beyond confirming
    neither appears in viewModel.

Panel states worth screenshotting

The two that changed, and three the harness can only assert about:

  1. Enter in the search field while already in all-time scope. The path that
    drops a paint and a 200-id walk. Watch for a single settle rather than the
    chip appearing over the old list first.
  2. Enter in the search field from day scope. main showed the new chip over a
    day-scope list for one frame; there should now be no such frame.
  3. Panel open, idle, with the bar widget publishing. The caret must land in
    the tag field exactly once, and the adopted interval must be on that first
    tree -- this is onOpen's apply and adoptState's apply both cashing in
    on the same commit.
  4. A bar click with a slow timew. The selection, the scope switch and the
    pager rewind are applied without a tree until the day's intervals land. Same
    as main, but it is the longest-lived apply-without-paint window in the file
    and the only one a human can catch.
  5. Filter to a tag in day scope, then click the one row left on screen. The
    state.shown publication finding 1 is about; on a correct build the chip does
    not move.
**Verdict: APPROVE** Re-derived rather than re-read: the invariant grep, the PR #6 race, the 32-state tree diff, the repaint counter, the timing, and every one of the six `apply` sites individually. The panel's behaviour is unchanged, the guard survived the move, `focusOnce` lands on the same tree, and one of the two "wins" is a real bug fix. Four findings, none blocking; two are about what the tests and the description *claim* rather than about the panel. Harnesses: `/tmp/pr10/h/` (`common.luau` mount + `order.luau`, `race.luau`, `bypass.luau`, `focus.luau`, `adopt.luau`), driven against `pmain.luau` / `pbranch.luau` rebuilt from `git show main:` and `git show refactor/t6-state:` -- `cmp` says both are byte-identical to the copies your harness used. --- ## Findings ### 1. The bypass invariant is grep-enforced, not test-enforced -- and the suite says otherwise (P2) `test.luau:1388` -- `paintsFor` The new section's second claim is "**Nothing bypasses `commit`**: same counter, from the other side". The counter sees only the half where a path mutates and *fails* to paint. The other half -- a path that writes `state` directly and paints anyway -- passes the whole suite, because the tree it renders is correct. What it silently drops is the other two things `commit` does at `:348` and `:354`. My mutant, `setFilter`'s `commit(patch)` replaced by the writes it stands for: ```lua local function setFilter(tag, extra) state.filter = Parse.trim(tag) state.search = state.filter state.searchKey = state.searchKey + 1 state.listOffset = 0 if extra then for field, value in pairs(extra) do state[field] = value end end render(viewModel()) fetchDays() ... ``` The gate catches it. The suite does not: ``` $ awk '/state\.[A-Za-z_]+ *,? *(state\.[A-Za-z_]+ *)?= *[^=]/ || /state\[[a-z]+\] *= *[^=]/ { print NR": "$0 }' nb_bypass.luau # your gate's own awk 326: state[field] = value 348: state.shown, state.matching = m.windowRows, m.pager.total 354: state.focusOnce = false 843: state.filter = Parse.trim(tag) 844: state.search = state.filter 845: state.searchKey = state.searchKey + 1 846: state.listOffset = 0 849: state[field] = value $ luau test.luau viewModel: 14 states, 1485 field comparisons ok ``` It is not a benign mutant. `commit` also publishes the window, and without that the slice `onRowN` indexes is the previous one (`/tmp/pr10/h/bypass.luau`): ``` == filter to 'admin' in day scope, then click the one row on screen == branch tree shows rows[admin] pager.total=1 branch after clicking row 1, the chip reads: "admin" bypass tree shows rows[admin] pager.total=1 bypass after clicking row 1, the chip reads: "focus" == the pager clamp reads the same stale publication == branch after filter->admin then 'next page': page 1 of 1, showing 1-5 of 5 bypass after filter->admin then 'next page': page 2 of 1, showing 9-8 of 5 ``` The tree diff cannot see it either -- I ran `treediff.luau` against this exact mutant and it reports `tree-identical`, correctly: the trees *are* identical, the state the next click reads is not. I accept that the suite cannot self-grep; I verified the reason rather than taking `test.luau:743` on trust: ``` $ luau -e 'print(type(io), type(os.getenv), type(loadstring), type(require))' nil nil function function $ luau -e 'local t={} for k in pairs(debug) do t[#t+1]=k end print(table.concat(t,","))' info,traceback ``` No `io`, no `os.getenv`, and `debug` is `info`/`traceback` only, so there is no route to the file's text. Fine. But then the claim in the section header has to shrink to what the counter actually proves. Two ways to close it, and the second is cheap enough that I would rather have it than the disclaimer. Fold `commit`'s own three writes into `paintsFor`, which already holds the painted state at exactly the right moment: ```suggestion local function paintsFor(what, expected, fn) paints = 0 fn() assert(paints == expected, what .. " painted " .. paints .. " times, not " .. expected) -- A path that wrote state and painted without going through `commit` leaves -- the published window behind: the tree is right, the slice onRowN indexes is -- not. Nothing else in this file can see that. if paints > 0 then local m = panel.viewModel() assert(panel.state.matching == m.pager.total, what .. ": the published match count is not the painted model's") assert(panel.state.focusOnce == false, what .. ": a paint left the focus request armed") assert(#panel.state.shown == #m.windowRows, what .. ": the published window is not the painted model's") for i, row in ipairs(m.windowRows) do assert(panel.state.shown[i] == row, what .. ": published window row " .. i .. " is not the model's") end end end ``` Applied, on the pristine tree and on the mutant: ``` pristine: viewModel: 14 states, 1485 field comparisons / ok bypass: ./test.luau:1397 function paintsFor <- "Enter in the search field: the published match count is not the painted model's" ``` That turns your 23 counted paths into 23 bypass checks for ten lines and no new mechanism. Whatever you do with it, the grep in the gate is still the only thing that sees the *source* shape, and the section comment should say so rather than say "nothing bypasses `commit`". One bypass shape neither the grep nor the suite covers, worth a sentence in the `state` comment since the whole design rests on that grep: `state.shown` **is** `m.windowRows`, the same table, so a builder that mutated the model would mutate state without ever matching `state.<field> =`. Nothing does today (`grep -nE '\bm\.[A-Za-z_]+ *=[^=]|table\.(insert|remove|sort)\(state'` on the branch: no matches), and main aliased it identically. Not introduced here; just the one hole in the invariant's shape. ### 2. The rewritten globals-snapshot comment claims coverage the snapshot does not have (P2) `test.luau:1030-1041` The comment this PR rewrites now says the table "answers half of that -- an accidental global, from a `local` dropped inside the model, lands the name in here", and the description repeats it as "**PR #8's env-key snapshot is untouched and still meaningful.** It catches a dropped `local` inside `viewModel`". It does not catch it, on this branch or on main. ``` $ sed -i '1092s/local chartPeak = /chartPeak = /' timew/panel.luau # branch $ luau test.luau viewModel: 14 states, 1485 field comparisons ok $ sed -i '1011s/local chartPeak = /chartPeak = /' timew/panel.luau # main, same mutation $ luau test.luau ok ``` The global is real -- it just predates the snapshot. `panelEnv` is taken at `:492` and the panel is driven from there on, so `viewModel` has run some hundreds of lines before `envBefore` is filled at `:1038`: ``` [probe] chartPeak already a module global at snapshot time: true ``` (probe = one `print` inserted immediately above `local envBefore = {}`, against the mutant.) So the loop at the bottom can only ever catch a global whose *first* write happens after `:1038`, which for `viewModel` is none of them. The fix is to snapshot before the first handler runs rather than before the first `open()`. The only key the panel's globals table legitimately gains after the stubs is `os`, added by the `panelEnv.os` stub further down, so it is seeded by hand: ```suggestion local envBefore = {} for k in pairs(panelEnv) do envBefore[k] = true end -- The os stub below is installed later, at the viewModel section; it is a key -- this table gains legitimately. envBefore.os = true ``` moved to sit immediately after `panelEnv.panel = { ... }` (`:535`), with the original block at `:1038-1041` deleted. Verified both directions: ``` pristine: viewModel: 14 states, 1485 field comparisons / ok dropped `local chartPeak`: ./test.luau:1597 "viewModel() wrote a module global: chartPeak" ``` The hole is PR #8's, not this wave's -- but this wave rewrites the comment that asserts it and repeats the claim in the description, and comments are load-bearing here. Worth noting that the *other* half is genuinely new and genuinely works: `dump(panel.state)` either side of five calls fails as advertised (`state.recent = {}` planted in `viewModel` -> `test.luau:1529`), so after this fix both halves would actually be covered. ### 3. The repaint harness never reaches `adoptState`'s `apply` (P3) `timew/panel.luau:663-672` `repaints.luau`'s `noctalia.state.get` returns `nil`, so `adoptState` takes the `a == nil` early return on all 38 measured steps and the `apply` under review is never executed. "0 paths worse" is true, but it does not cover this site -- routing it through `commit` changes nothing in that table: ``` apply@666 -> commit: repaints: main 39 -> branch 37 (-2); paths that got worse: 0 ``` The conclusion survives; I re-measured it with a bar widget that actually publishes (`/tmp/pr10/h/adopt.luau`): ``` == the bar widget is publishing, so adoptState succeeds and fetchActive is skipped == onOpen (defaults + refresh + commit) stop lands -> refresh main 1 paint [C:dayTotal C:tags C:dayTotals P] 0 paint branch 1 paint [C:dayTotal C:tags C:dayTotals P] 0 paint adoptState commits 2 paint [P C:dayTotal C:tags C:dayTotals P] 1 paint ``` +1 paint per open and +1 per mutating command. So the site is justified -- but by this measurement, not by the one in the description. Either give `repaints.luau` a publishing `state.get`, or say in the PR body that `adoptState` is argued rather than measured. ### 4. The quoted `awk` output in the description is two lines off (P3) `timew/panel.luau:322-328` The description's invariant block reads `324` / `346` / `352`. At `84d4201` the lines are `326` / `348` / `354`: ``` $ awk '/state\.[A-Za-z_]+ *,? *(state\.[A-Za-z_]+ *)?= *[^=]/ || /state\[[a-z]+\] *= *[^=]/ { print NR": "$0 }' timew/panel.luau 326: state[field] = value 348: state.shown, state.matching = m.windowRows, m.pager.total 354: state.focusOnce = false ``` Same three sites, same conclusion; it is the one piece of output in the description that is not reproducible verbatim, and it happens to be the PR's central evidence. It is not a stale commit either -- the branch is one commit, and `state[field] = value` is at `326` in it. --- ## Verified **The invariant holds, including the forms a naive grep misses.** `state[field] =` only at `:326`; `state.<field> =` only at `:348` and `:354`. No `rawset` anywhere in the repo; no `state["literal"] =`; no `state.X[...] =`; no `table.insert/remove/move/sort(state...)`; no alias binding the table to another name (`state = state` at `:1689` is the export); one `render(` call statement, at `:349`. `grep -c 'commit('` = 19 minus the definition = **18 commit sites**, and six `apply` sites at `666 / 734 / 761 / 985 / 999 / 1614`. Your "19 on main" is right too: `awk` over non-comment lines on `pmain.luau` gives exactly 19 `render()` statements at `618 622 626 633 642 649 659 671 696 715 722 767 852 859 870 916 927 1547 1555`. **Async ordering: the PR #6 race does not come back, and the split adds no new one.** Reproduction re-run per PR #6 (`/tmp/pr10/h/race.luau`), all-time under `focus`, filter to `email` mid-flight, stale walk last: ``` == R1: PR #6's stale-walk race, re-run on this branch == main stale walk landed: rows[email] allTotal=3:22 chip=email scope=all paints=0 branch stale walk landed: rows[email] allTotal=3:22 chip=email scope=all paints=0 == NEGATIVE CONTROL: rows guard removed from the branch == branch stale walk landed: rows[focus,focus] allTotal=3:22 chip=email scope=all paints=1 ``` R2 lands them in the other order, R3 races two day walks, R4 lands a stale `dayTotals`, R5 checks a keystroke reaching the tree on the next unrelated repaint, R6 checks `day 99` -- **every one identical on both sides**, including the two that are wrong on both (R3 leaves day 2 selected showing day 3's rows; R4 lands the stale bars, `fetchDays` taking no `wanted` guard). Pre-existing, byte-for-byte the same shape as main, not this PR's to fix -- but R3 is the sharper of the two and is the same class PR #6 closed for `fetchAll`. **No `apply` site leaves state unpainted longer than main did.** I compared the full `paint | command` interleaving per handler rather than the state after it (`/tmp/pr10/h/order.luau`, 24 scenarios). Nine streams differ; on eight of them the *commands* are identical and the branch's paint lands **earlier**: ``` scenarios: 24 full-stream differences: 9 COMMAND-STREAM differences: 1 paint moved EARLIER on 8 paths, LATER on 0 ``` The one command-stream difference is the win, and it is a bug fix, not a refactor -- worth saying so plainly in the description: ``` onSearchSubmit, all-time scope main C:dayTotals C:ALLWALK C:allTotal P C:ALLWALK C:allTotal P branch P C:dayTotals C:ALLWALK C:allTotal ``` main issues the 200-id DOM walk **and** its range total twice for one Enter. That is a duplicated `ALL_WALK_MAX`-bounded shell walk on the panel's most expensive query, on a keypress. Independent of the repaint story. **The six `apply` sites, ruled on individually.** Each converted to `commit` in turn and re-run through `repaints.luau` (baseline: branch 37 paints, 0 worse): | site | as `commit` | verdict | | --- | --- | --- | | `loadDay` `:761` | 43 paints, **6 paths worse** | `apply`. A bar click would paint before its intervals exist, and `day 99` would paint a day the chart has no entry for. | | `fetchDays` `:734` | 41 paints, **4 paths worse** | `apply`. The totals must land before `loadDay` can index them; committing splits one landing into two trees. | | `onSearchChange` `:985` | 39 paints, 1 worse | `apply`. One whole-tree rebuild per character. | | `onDraftChange` `:999` | 38 paints, 1 worse | `apply`. Same. | | `onOpen` `:1614` | 38 paints, 1 worse | `apply`. Two trees on every open. | | `adoptState` `:666` | 37 paints, 0 worse *in that harness* | `apply` -- but see finding 3; measured properly it is +1 per open and +1 per mutating command. | 6/6 genuinely justified, so the two-function split earns itself: this is not `apply` existing because `commit` felt too blunt once. `refresh`'s five landings staying five repaints is right for the same reason -- merging them means holding the tree until the slowest walk returns. The one thing I would still write down is that `apply` iterates `pairs(patch)`, so a patch field whose value is `nil` is silently *not written* where the twenty assignments it replaces would have cleared the field. No producer can emit `nil` today (`parseActive` returns literals or `Parse.unquote`, which falls back to its argument; every other parser returns a table or a string), so there is nothing to fix -- but the closed-field-set assert covers misspelling and not this, and it is the one semantic narrowing the rewrite makes. **`focusOnce` is carried by the same tree and consumed once** (`/tmp/pr10/h/focus.luau`, focus-per-paint recorded across the whole open): ``` F1 idle open + five landings + tick + two scope clicks main [1,0,0,0,0,0,0,0] trees carrying focus: 1 branch [1,0,0,0,0,0,0,0] trees carrying focus: 1 F2 reopened while already open main / branch [1,0,0,0,0,0,0,0,1,0] 2 F3 host replays watch values at registration (a paint *inside* onOpen) main / branch [1,0] 1 F4 host calls runAsync back inline (every fetch lands inside onOpen) main / branch [1,0,0,0,0] 1 focus sequence differences: 0 ``` F3 and F4 are the two hosts that could paint between `apply({focusOnce = true})` and `onOpen`'s `commit()`; both sides move the flag to the same tree at the same index. And the asserts that pin it are not vacuous -- `onOpen` applying `focusOnce = false` fails `test.luau:1059`, `commit` not clearing it fails `test.luau:1060`. **Tree identity and cost reproduce.** `pmain.luau`/`pbranch.luau` rebuilt from `git show`, `cmp`-identical to yours: ``` tree identity: main vs refactor/t6-state states: 32 bytes: main 244419, branch 244419 node lines compared: 2372 per side differences: 0 tree-identical TOTAL 39 paint 10 cmd 37 paint 8 cmd repaints: main 39 -> branch 37 (-2); paths that got worse: 0 main 0.0659 ms per submit branch 0.0345 ms (-47.7%) ``` Not vacuous either: `ROWS_MAX 8 -> 6` reports `node COUNT main 103 branch 91`, and deleting `commit`'s `state.focusOnce = false` reports the input's `focus=boolean:true` against main's `false`. Timing over three runs is noise with a sign flip, matching your claim: `-1.3% / -0.5% / +1.1%`, `0.21% -> 0.21%` of the 25 ms budget. **The suite still drives everything it drove.** The `panelEnv.*` handler set is a strict superset -- nothing dropped, five added directly (`onClearFilter`, `onDraftChange`, `onScopeDay`, `onSearchChange`, `onSearchSubmit`); one assert line changed and it is the focus message. And with `apply`/`commit` instrumented to log every field write over a full run, all 21 declared fields move: ``` active 19 tags 19 startEpoch 19 total 19 recent 20 filter 29 mode 34 days 22 selected 45 dayRows 23 allRows 10 allTotal 10 shown 201 matching 201 listOffset 77 draft 21 draftKey 20 search 30 searchKey 29 focusOnce 221 elapsedText 1 every declared field is driven by the suite ``` `elapsedText` at 1 is thin, but it is the watch's only writer on both sides. **Your new asserts fire where you say.** Independently re-run: `viewModel` clearing `recent` -> `:1529`; a patch misspelling a field -> `:1509`; `commit` painting twice -> `:1391`. **Gate, from the branch worktree.** `luau test.luau` -> `ok`. `noctalia plugins lint timew` -> `0 errors, 0 warnings`. No `getfenv`/`setfenv` in `timew/`. Every `onClick` is still a name off the model, no closures. `git status --porcelain` empty in both worktrees; everything I broke was broken in `/tmp/pr10/work` and `/tmp/pr10/h`, and each restore was `cmp`-checked. --- ## Not checked - The live shell. Everything above is under stubs, so `runAsync`'s real scheduling, the real `noctalia.state.watch` replay semantics and the real widget's handling of `focus`/`key` are inferred. F3/F4 in `focus.luau` are my attempt to bracket the two that matter, but they are models of a host, not the host. - The deferred backwards-range bug behind `day 99`. Reproduced identically on both sides (`R6`: 0 paints on the click, `dayTitle` `--` on the next tick), not investigated. - `Parse`/`Query`/`Timew`/the region builders. Waves 1-5 territory; I only confirmed `viewModel` and `render` read `state` and write nothing. - Whether `poke`/`watching` staying outside the table is right beyond confirming neither appears in `viewModel`. ## Panel states worth screenshotting The two that changed, and three the harness can only assert about: 1. **Enter in the search field while already in all-time scope.** The path that drops a paint *and* a 200-id walk. Watch for a single settle rather than the chip appearing over the old list first. 2. **Enter in the search field from day scope.** main showed the new chip over a day-scope list for one frame; there should now be no such frame. 3. **Panel open, idle, with the bar widget publishing.** The caret must land in the tag field exactly once, and the adopted interval must be on that first tree -- this is `onOpen`'s `apply` and `adoptState`'s `apply` both cashing in on the same `commit`. 4. **A bar click with a slow `timew`.** The selection, the scope switch and the pager rewind are applied without a tree until the day's intervals land. Same as main, but it is the longest-lived apply-without-paint window in the file and the only one a human can catch. 5. **Filter to a tag in day scope, then click the one row left on screen.** The `state.shown` publication finding 1 is about; on a correct build the chip does not move.
Review #47 on PR #10, findings 1-4.

The bypass invariant was grep-only where the comments said it was
test-enforced. The paint counter sees a path that mutates and fails to
paint; it cannot see one that writes `state` directly and paints anyway,
because the tree that path renders is correct. `paintsFor` now also
checks the three things `commit` does besides painting -- publishing
`state.shown`/`state.matching` off the painted model and clearing
`state.focusOnce` -- so all 23 counted paths are bypass checks too. The
section comment now claims that, and says the source-shape grep lives in
the gate because the luau CLI cannot read this file.

The globals snapshot never caught what its comment claimed. `panelEnv`
is the module's own globals table, so a global written by `viewModel`
existed before a snapshot taken beside the viewModel section: the loop
compared the table against itself, and a `local` dropped inside the
model passed on this branch and on main alike. Moved to before the first
handler runs, with `os` -- the one key the table legitimately gains
afterwards -- seeded by hand.

`adoptState`'s `apply` was the one panel path the counter never reached,
since this harness's `noctalia.state.get` returns nil and the early
return fires on every open. Driven now with the bar widget publishing:
adopting rides onOpen's single commit, and the adopted interval is on
that tree.

Also written down, both flagged in review: `state.shown` is the model's
own `windowRows` table rather than a copy, so a builder mutating the
model would move state without matching the gate's grep; and `apply`
iterates `pairs`, so a patch field set to `nil` is not written where the
assignments it replaced would have cleared the field.

Panel change is comments only. Suite green, lint clean, 32-state tree
diff still 0 differences, repaints still 39 -> 37 with 0 paths worse.
Author
Owner

All four applied, cd3395b. Both P2s were the review's own diagnosis reproduced
first and fixed second; nothing declined. The panel change is comments only —
git diff on timew/panel.luau has no non-comment line either side.

1. The bypass invariant (applied)

Reproduced your mutant exactly before touching anything, so the "before" is mine
and not inherited:

$ awk '/state\.[A-Za-z_]+ *,? *(state\.[A-Za-z_]+ *)?= *[^=]/ || /state\[[a-z]+\] *= *[^=]/ { print NR": "$0 }' timew/panel.luau
326: 		state[field] = value
348: 	state.shown, state.matching = m.windowRows, m.pager.total
354: 	state.focusOnce = false
836: 	state.filter = Parse.trim(tag)
837: 	state.search = state.filter
838: 	state.searchKey = state.searchKey + 1
839: 	state.listOffset = 0
842: 			state[field] = value

$ luau test.luau
viewModel: 14 states, 1485 field comparisons
ok
$ echo $?
0

Your paintsFor suggestion applied as written. After, on the same mutant, at
cd3395b:

$ luau test.luau
./test.luau:1424: Enter in the search field: the published match count is not the painted model's
stacktrace:
[C] function assert
./test.luau:1424 function paintsFor
./test.luau:1443
$ echo $?
1

Restored, cmp-clean, pristine back to ok. 23 counted paths, 23 bypass checks.

On the claim: the section comment now separates the three things by who proves
them. The count proves mutate-without-painting. paintsFor's new block proves
publishes-what-it-painted. The source shape stays grep-only in the gate, and
the comment says so and says why (:743, same reason en.json is a copy) instead
of saying "nothing bypasses commit".

Your extra hole is written on the state table itself, with the grep, since you
are right that the whole design rests on that grep:

-- One shape that grep cannot see, since the whole design rests on it: `commit`
-- publishes `state.shown` as the model's own `windowRows` table, not a copy, so
-- a region builder that mutated the model in place would move state without
-- ever writing `state.<field>`. Nothing does: ...
--   grep -nE '\bm\.[A-Za-z_]+ *=[^=]|table\.(insert|remove|move|sort)\(state'

Re-verified on the branch: no matches.

2. The globals snapshot (applied)

Confirmed dead before fixing, on the branch:

$ sed -i '1092s/local chartPeak = /chartPeak = /' timew/panel.luau
$ luau test.luau
viewModel: 14 states, 1485 field comparisons
ok
$ echo $?
0

Moved to immediately after the panelEnv.panel stub with envBefore.os = true
seeded, old block deleted. Same mutant, at cd3395b (panel.luau:1111 now):

$ luau test.luau
./test.luau:1687: viewModel() wrote a module global: chartPeak
$ echo $?
1

One check you did not ask for, because a hand-seeded key is exactly the kind of
thing that quietly becomes a blanket exemption: the seed is load-bearing, and
deleting it fails on a pristine panel.

$ sed -i '/^envBefore\.os = true$/d' test.luau      # pristine timew/panel.luau
$ luau test.luau
./test.luau:1686: viewModel() wrote a module global: os

That is what pins the snapshot as genuinely early rather than merely moved, so
it is written into the comment as the reason the seed is there.

The half you told me to keep is kept and re-verified live. Planted at
viewModel's tail rather than its head, so the current call's tree is still
correct and only a later call sees the wipe:

./test.luau:1568: a fetch landing lost its rows: recent 0, days 3, dayRows 3

Note which assert catches it: the named-values one, not the dump comparison —
the dump is taken after open() has already run the model once, so the wipe is
already idempotent by then. Your "idempotence is not purity" reasoning is what
makes that half work at all; the dump alone would pass.

3. adoptState never measured (applied, and further than asked)

Both your options were "measure it or admit it is argued". I measured it, with
your adopt.luau — +1 paint per open, +1 per mutating command, first tree
identical either way — and the PR body now carries that block instead of leaning
on the repaint table.

But the harness gap was the smaller half of it. noctalia.state.get returns
nil in test.luau too, so adoptState's success path was not merely
unmeasured, it was the one panel path the suite never executed — while the
section comment claimed "every path the panel has is counted here". Same defect
class as findings 1 and 2, so it is closed the same way rather than annotated:
the path is now driven with a publishing bar widget, asserting that adopting
costs no tree of its own, that the adopted interval is on onOpen's single tree
(action button onStop, subject line the adopted tag, both read before any fetch
has landed), and that the four landings still cost one paint each.

Mutation-proved both ways, at cd3395b:

adoptState commits instead of applying (panel.luau:685)
  ./test.luau:1415: an open with the bar widget publishing painted 2 times, not 1

adoptState drops the adopted tag (panel.luau:688)
  ./test.luau:1650: the interval adopted off the bar widget reached state without a tree of its own

So the site is justified by measurement in /tmp, and now defended by an assert
in the repo. Sixth apply counted; the "every path" claim is true.

4. The awk output is two lines off (applied)

It was, and it is worse than two lines now, because my own fixup moved them
again — the panel comment added ten lines above the block:

$ awk '/state\.[A-Za-z_]+ *,? *(state\.[A-Za-z_]+ *)?= *[^=]/ || /state\[[a-z]+\] *= *[^=]/ { print NR": "$0 }' timew/panel.luau
345: 		state[field] = value
367: 	state.shown, state.matching = m.windowRows, m.pager.total
373: 	state.focusOnce = false

Description re-quoted at 345 / 367 / 373. Same three sites, same conclusion.

The mutation-proof table had the same rot at eleven rows' scale after my commit
shifted test.luau by ~104 lines. Rather than re-quote numbers that will move
on the next edit, that table is now anchored — "line numbers are at 84d4201",
which git checkout 84d4201 reproduces verbatim — with the assert message as
the stable identifier, and the fixup's six mutations in their own table at
cd3395b. The two references outside it (:636/:640 → :659/:663, focus
one-shot :1060 → :1070) are rebased.

Also written down

Your pairs(patch) note, on apply's comment, since it is the one semantic
narrowing the rewrite makes and nothing tests it:

pairs does not visit a key whose value is nil, so a patch field set to
nil is silently not written where the old assignment would have cleared the
field. [...] A field that ever needs clearing must be given its zero value
(false, "", 0, {}) in the patch, the way the idle branch of
watchState does.

Verified that last clause rather than asserting it: panel.luau:706 is
patch.startEpoch, patch.tags = 0, "".

Gate

$ luau test.luau                       viewModel: 14 states, 1485 field comparisons / ok
$ noctalia plugins lint timew          0 errors, 0 warnings
$ git diff --stat main...HEAD          test.luau | 354 ++-   timew/panel.luau | 472 +-
$ luau treediff.luau                  states: 32   differences: 0   tree-identical
$ luau repaints.luau                  39 -> 37 (-2); paths that got worse: 0
$ git status --porcelain               (empty)

Tree identity and the repaint table both re-run against the fixup's
timew/panel.luau (harness pbranch.luau refreshed, cmp-identical to the
worktree copy). Neither moved. Every mutation above was run in /tmp/pr10fix,
restored with cp and confirmed with cmp, and the suite is green on the
pristine tree before and after each one.

Screenshot list extended with your items 5 and 3 — the filter-then-click state
finding 1 is about, and the idle open with the bar widget publishing. Merging.

All four applied, `cd3395b`. Both P2s were the review's own diagnosis reproduced first and fixed second; nothing declined. The panel change is comments only — `git diff` on `timew/panel.luau` has no non-comment line either side. ## 1. The bypass invariant (applied) Reproduced your mutant exactly before touching anything, so the "before" is mine and not inherited: ``` $ awk '/state\.[A-Za-z_]+ *,? *(state\.[A-Za-z_]+ *)?= *[^=]/ || /state\[[a-z]+\] *= *[^=]/ { print NR": "$0 }' timew/panel.luau 326: state[field] = value 348: state.shown, state.matching = m.windowRows, m.pager.total 354: state.focusOnce = false 836: state.filter = Parse.trim(tag) 837: state.search = state.filter 838: state.searchKey = state.searchKey + 1 839: state.listOffset = 0 842: state[field] = value $ luau test.luau viewModel: 14 states, 1485 field comparisons ok $ echo $? 0 ``` Your `paintsFor` suggestion applied as written. After, on the same mutant, at `cd3395b`: ``` $ luau test.luau ./test.luau:1424: Enter in the search field: the published match count is not the painted model's stacktrace: [C] function assert ./test.luau:1424 function paintsFor ./test.luau:1443 $ echo $? 1 ``` Restored, `cmp`-clean, pristine back to `ok`. 23 counted paths, 23 bypass checks. On the claim: the section comment now separates the three things by who proves them. The count proves mutate-without-painting. `paintsFor`'s new block proves publishes-what-it-painted. The *source* shape stays grep-only in the gate, and the comment says so and says why (`:743`, same reason en.json is a copy) instead of saying "nothing bypasses `commit`". Your extra hole is written on the `state` table itself, with the grep, since you are right that the whole design rests on that grep: ``` -- One shape that grep cannot see, since the whole design rests on it: `commit` -- publishes `state.shown` as the model's own `windowRows` table, not a copy, so -- a region builder that mutated the model in place would move state without -- ever writing `state.<field>`. Nothing does: ... -- grep -nE '\bm\.[A-Za-z_]+ *=[^=]|table\.(insert|remove|move|sort)\(state' ``` Re-verified on the branch: no matches. ## 2. The globals snapshot (applied) Confirmed dead before fixing, on the branch: ``` $ sed -i '1092s/local chartPeak = /chartPeak = /' timew/panel.luau $ luau test.luau viewModel: 14 states, 1485 field comparisons ok $ echo $? 0 ``` Moved to immediately after the `panelEnv.panel` stub with `envBefore.os = true` seeded, old block deleted. Same mutant, at `cd3395b` (`panel.luau:1111` now): ``` $ luau test.luau ./test.luau:1687: viewModel() wrote a module global: chartPeak $ echo $? 1 ``` One check you did not ask for, because a hand-seeded key is exactly the kind of thing that quietly becomes a blanket exemption: the seed is load-bearing, and deleting it fails on a **pristine** panel. ``` $ sed -i '/^envBefore\.os = true$/d' test.luau # pristine timew/panel.luau $ luau test.luau ./test.luau:1686: viewModel() wrote a module global: os ``` That is what pins the snapshot as genuinely early rather than merely moved, so it is written into the comment as the reason the seed is there. The half you told me to keep is kept and re-verified live. Planted at `viewModel`'s tail rather than its head, so the current call's tree is still correct and only a later call sees the wipe: ``` ./test.luau:1568: a fetch landing lost its rows: recent 0, days 3, dayRows 3 ``` Note which assert catches it: the *named-values* one, not the dump comparison — the dump is taken after `open()` has already run the model once, so the wipe is already idempotent by then. Your "idempotence is not purity" reasoning is what makes that half work at all; the dump alone would pass. ## 3. `adoptState` never measured (applied, and further than asked) Both your options were "measure it or admit it is argued". I measured it, with your `adopt.luau` — `+1 paint per open, +1 per mutating command`, first tree identical either way — and the PR body now carries that block instead of leaning on the repaint table. But the harness gap was the smaller half of it. `noctalia.state.get` returns `nil` in **test.luau** too, so `adoptState`'s success path was not merely unmeasured, it was the one panel path the suite never executed — while the section comment claimed "every path the panel has is counted here". Same defect class as findings 1 and 2, so it is closed the same way rather than annotated: the path is now driven with a publishing bar widget, asserting that adopting costs no tree of its own, that the adopted interval is on `onOpen`'s single tree (action button `onStop`, subject line the adopted tag, both read before any fetch has landed), and that the four landings still cost one paint each. Mutation-proved both ways, at `cd3395b`: ``` adoptState commits instead of applying (panel.luau:685) ./test.luau:1415: an open with the bar widget publishing painted 2 times, not 1 adoptState drops the adopted tag (panel.luau:688) ./test.luau:1650: the interval adopted off the bar widget reached state without a tree of its own ``` So the site is justified by measurement in `/tmp`, and now defended by an assert in the repo. Sixth `apply` counted; the "every path" claim is true. ## 4. The awk output is two lines off (applied) It was, and it is worse than two lines now, because my own fixup moved them again — the panel comment added ten lines above the block: ``` $ awk '/state\.[A-Za-z_]+ *,? *(state\.[A-Za-z_]+ *)?= *[^=]/ || /state\[[a-z]+\] *= *[^=]/ { print NR": "$0 }' timew/panel.luau 345: state[field] = value 367: state.shown, state.matching = m.windowRows, m.pager.total 373: state.focusOnce = false ``` Description re-quoted at `345 / 367 / 373`. Same three sites, same conclusion. The mutation-proof table had the same rot at eleven rows' scale after my commit shifted `test.luau` by ~104 lines. Rather than re-quote numbers that will move on the next edit, that table is now anchored — "line numbers are at `84d4201`", which `git checkout 84d4201` reproduces verbatim — with the assert message as the stable identifier, and the fixup's six mutations in their own table at `cd3395b`. The two references outside it (`:636`/`:640` → `:659`/`:663`, focus one-shot `:1060` → `:1070`) are rebased. ## Also written down Your `pairs(patch)` note, on `apply`'s comment, since it is the one semantic narrowing the rewrite makes and nothing tests it: > `pairs` does not visit a key whose value is `nil`, so a patch field set to > `nil` is silently not written where the old assignment would have cleared the > field. [...] A field that ever needs clearing must be given its zero value > (`false`, `""`, `0`, `{}`) in the patch, the way the idle branch of > `watchState` does. Verified that last clause rather than asserting it: `panel.luau:706` is `patch.startEpoch, patch.tags = 0, ""`. ## Gate ``` $ luau test.luau viewModel: 14 states, 1485 field comparisons / ok $ noctalia plugins lint timew 0 errors, 0 warnings $ git diff --stat main...HEAD test.luau | 354 ++- timew/panel.luau | 472 +- $ luau treediff.luau states: 32 differences: 0 tree-identical $ luau repaints.luau 39 -> 37 (-2); paths that got worse: 0 $ git status --porcelain (empty) ``` Tree identity and the repaint table both re-run against the fixup's `timew/panel.luau` (harness `pbranch.luau` refreshed, `cmp`-identical to the worktree copy). Neither moved. Every mutation above was run in `/tmp/pr10fix`, restored with `cp` and confirmed with `cmp`, and the suite is green on the pristine tree before and after each one. Screenshot list extended with your items 5 and 3 — the filter-then-click state finding 1 is about, and the idle open with the bar widget publishing. Merging.
yuris merged commit 1d49c5a018 into main 2026-09-01 08:32:56 +00:00
yuris deleted branch refactor/t6-state 2026-09-01 08:32:57 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/noctalia-timew!10
No description provided.