refactor(panel): route every fetch through Timew #6

Merged
yuris merged 3 commits from refactor/t3b-rewire into main 2026-08-31 18:34:29 +00:00
Owner

Wave 3, first of two: the panel's fetch layer now goes through Timew, and the
duplication that was deliberate for one commit is gone.

The invariant this PR establishes

All 8 query paths originate in Timew. The two mutating strings stay at their
call sites, because bar.luau duplicates them and cannot require across plugin
entries.

Exactly three matches, and no more:

$ grep -n 'run("timew' timew/panel.luau
734:	run("timew start " .. shQuote(tag))     -- startTag
897:	run("timew stop")                       -- onStop
1328:		run("timew stop")                   -- onIpc, event == "stop"

startTag builds the one interpolated string (its own shQuote); onStop and
onIpc share the constant one. All three go through run -> Timew.exec, so
the execution is Timew's; only the verb is not. Timew.exec's comment (wave 1)
is why: a typed Timew.start/Timew.stop pair would put the tag's quoting in a
table bar.luau:215 cannot reach, serving one caller of two and leaving two
idioms for one operation. I updated that comment's last sentence to record that
the rewire left them in place, rather than reading it as advice to a wave that
has now happened.

$ grep -c runAsync timew/panel.luau
11

8 calls, every one inside Timew (activeInterval, todayTotal, tags,
dayTotals, intervalsForDay, allIntervals, rangeTotal, exec), plus three
prose mentions in comments. No getfenv/setfenv.

What changed

The eight fetch functions keep their names, their side effects (they own the
module state and call render()) and their call sites. Their bodies shrank to a
Timew call and an assignment.

old now calls timeout, before -> after
fetchActive Timew.activeInterval 3000 -> 3000
fetchTotal Timew.todayTotal 3000 -> 3000
fetchTags Timew.tags 3000 -> 3000
fetchDays Timew.dayTotals(DAYS, filter, …) 15000 -> 15000
loadDay Timew.intervalsForDay(day.date, DAY_WALK_MAX, …) 10000 -> 10000
fetchAll Timew.allIntervals(ALL_WALK_MAX, filter, …) 20000 -> 20000
fetchAll Timew.rangeTotal(filter, …) 10000 -> 10000
run Timew.exec 3000 -> 3000

Timeouts are not asserted by prose: the harness below compares the timeout
argument of every issued command, old against new.

RECENT_MAX moved to the caller. Timew.parseTags returns every tag on purpose
and test.luau:177 pins that, so fetchTags caps:
recent = table.move(list, 1, math.min(#list, RECENT_MAX), 1, {}). The no-cap
assert was not touched or weakened.

Quoting: filter and day.date reach Timew bare, and Timew.tagArg /
Timew.cmd.intervalsForDay are the only things that quote them. Nothing
double-quotes: the harness drives "x'; rm -rf /; #" and "it's" through
fetchDays and fetchAll and compares the resulting command strings character
for character against the old code's.

Deletions, each with its grep

grep -n for remaining callers before removing (call-site line numbers are the
pre-change ones):

  • unquote — callers were parseInterval:332 and fetchActive:690. Both gone. Parse.unquote (105) is the surviving copy, and what Timew.parseActive and Parse.interval use. Deleted.
  • parseInterval — callers were loadDay:789 and fetchAll:812, both now Timew.parseIntervals. Deleted.
  • parseClock — callers were fetchDays:744 and fetchAll:823. Parse.clock covers both, through Timew.parseDayTotals and Timew.parseRangeTotal. Deleted.
  • parseDuration — only caller was parseInterval:331. Deleted with it.
  • matchesFilter — callers were fetchAll:813 (gone), activityRows:1127, and the row button's selected:1163. The two survivors now read Timew.tagMatches(row.tag, filter), the disposition wave 1 recorded above Timew.tagMatches. Deleted.
  • kept, callers remain: trim (startTag, setFilter), shQuote (Timew.tagArg, Timew.cmd.intervalsForDay, startTag), formatSpan (render), formatElapsed (render), tagColor (activityRows), elapsed (render). These go with the Fmt/Parse call-site move, which is the next PR.

test.luau follows the two exports that no longer exist: panel.parseDuration
and panel.parseClock became panel.Parse.duration / panel.Parse.clock, the
same assert list against the copy that survived. The bar/panel drift comment no
longer claims panel has a private unquote, and the frozen-literals comment no
longer says the fetch functions are still there to be diffed against.

New coverage: bar's stop command

The suite pinned every query builder and bar's start command, but nothing
pinned "timew stop" against a real call site — the stop branch of
bar.onClick is reachable only once sync has seen a live interval, which
nothing in the suite had done. Added, at the end of the drift section:

syncCb({ stdout = "1000 focus\n" })
barEnv.onClick()
assert(lastCmd == panel.Timew.cmd.exec("timew stop"), "bar is tracking, so onClick stops")

bar's start assert now composes Timew.cmd.exec as well, so the 2>&1 redirect
is pinned against the builder instead of a second inline literal.

Why no Timew.cmd.start/Timew.cmd.stop. They were considered as
test-only frozen literals, and skipped: no call site may use them (see the
invariant above), so they would be two functions with zero callers, and the
asserts pinning them would compare a builder against its own literal — a
tautology that cannot fail when startTag, onStop or onIpc changes.
test.luau:274 already freezes "timew stop" inside the cmd.exec assert, and
test.luau:408 freezes "timew start" plus its quoting against bar's actual
command. The one real gap was bar's stop branch, and the assert above closes it
without adding dead code to a module on a 25 ms per-handler budget. panel's own
three call sites stay unreachable from test.luau — the luau CLI gives each
required module its own global table, so no stub of panel's runAsync is
visible inside it. That gap is the no-require rule's, and it closes at plugin
API 4.

Equivalence evidence

This PR changes live behaviour, so here is the proof that it does not. Throwaway
harness, not committed (/tmp/t3b/): git show main:timew/panel.luau and the
new file are each wrapped with a noctalia.runAsync stub and an epilogue that
exposes the module state, then both are driven through the same 21 scenarios —
all eight paths, including idle/live/untagged/quoted active intervals, a prose
day total, six tags against the cap, loadDay with keepScope, without it and
out of range, both fetchAll commands, the shell injection strings, and run on
exit 0 and exit 1 with the whole refresh cascade behind it.

Compared per scenario, field by field: every issued command string, every
timeout, the notifyError text, every noctalia.state.set write, and active,
startEpoch, tags, total, recent, days, dayRows, allRows,
allTotal, mode, selected, listOffset, filter.

21 scenarios, 511 field comparisons, 0 differences
ok

Negative controls, so that count means something: changing the dayTotals
timeout to 14000 reports 5 differences; dropping the RECENT_MAX cap reports
tags/six.state.recent.5 and .6.

Gate

$ luau test.luau
ok
$ noctalia plugins lint timew
0 errors, 0 warnings
$ git diff --stat main...HEAD
 test.luau        |  28 +++++--
 timew/panel.luau | 228 +++++++++++++------------------------------------------
 2 files changed, 72 insertions(+), 184 deletions(-)

For the reviewer to challenge

  • fetchDays passes DAYS, loadDay passes DAY_WALK_MAX, fetchAll passes ALL_WALK_MAX, rather than relying on Timew's defaults. Deliberate: the command strings stay byte-identical to the old ones (the harness compares them), and the bound reads next to the caller that owns it. test.luau separately pins that an explicit value equals the default.
  • run ignores Timew.exec's ok argument. That matches the old behaviour: the panel refetches after a failure too, since a failed start otherwise leaves it claiming the old state.
  • loadDay's state logic (pager rewind, scope reset, the keepScope path) is copied unchanged; only its tail is new. Worth a read against main — the harness covers all three branches.
Wave 3, first of two: the panel's fetch layer now goes through `Timew`, and the duplication that was deliberate for one commit is gone. ## The invariant this PR establishes **All 8 query paths originate in `Timew`. The two mutating strings stay at their call sites, because `bar.luau` duplicates them and cannot `require` across plugin entries.** Exactly three matches, and no more: ``` $ grep -n 'run("timew' timew/panel.luau 734: run("timew start " .. shQuote(tag)) -- startTag 897: run("timew stop") -- onStop 1328: run("timew stop") -- onIpc, event == "stop" ``` `startTag` builds the one interpolated string (its own `shQuote`); `onStop` and `onIpc` share the constant one. All three go through `run` -> `Timew.exec`, so the *execution* is Timew's; only the verb is not. `Timew.exec`'s comment (wave 1) is why: a typed `Timew.start`/`Timew.stop` pair would put the tag's quoting in a table `bar.luau:215` cannot reach, serving one caller of two and leaving two idioms for one operation. I updated that comment's last sentence to record that the rewire left them in place, rather than reading it as advice to a wave that has now happened. ``` $ grep -c runAsync timew/panel.luau 11 ``` 8 calls, every one inside `Timew` (`activeInterval`, `todayTotal`, `tags`, `dayTotals`, `intervalsForDay`, `allIntervals`, `rangeTotal`, `exec`), plus three prose mentions in comments. No `getfenv`/`setfenv`. ## What changed The eight fetch functions keep their names, their side effects (they own the module state and call `render()`) and their call sites. Their bodies shrank to a `Timew` call and an assignment. | old | now calls | timeout, before -> after | | --- | --- | --- | | `fetchActive` | `Timew.activeInterval` | 3000 -> 3000 | | `fetchTotal` | `Timew.todayTotal` | 3000 -> 3000 | | `fetchTags` | `Timew.tags` | 3000 -> 3000 | | `fetchDays` | `Timew.dayTotals(DAYS, filter, …)` | 15000 -> 15000 | | `loadDay` | `Timew.intervalsForDay(day.date, DAY_WALK_MAX, …)` | 10000 -> 10000 | | `fetchAll` | `Timew.allIntervals(ALL_WALK_MAX, filter, …)` | 20000 -> 20000 | | `fetchAll` | `Timew.rangeTotal(filter, …)` | 10000 -> 10000 | | `run` | `Timew.exec` | 3000 -> 3000 | Timeouts are not asserted by prose: the harness below compares the timeout argument of every issued command, old against new. `RECENT_MAX` moved to the caller. `Timew.parseTags` returns every tag on purpose and `test.luau:177` pins that, so `fetchTags` caps: `recent = table.move(list, 1, math.min(#list, RECENT_MAX), 1, {})`. The no-cap assert was not touched or weakened. Quoting: `filter` and `day.date` reach `Timew` bare, and `Timew.tagArg` / `Timew.cmd.intervalsForDay` are the only things that quote them. Nothing double-quotes: the harness drives `"x'; rm -rf /; #"` and `"it's"` through `fetchDays` and `fetchAll` and compares the resulting command strings character for character against the old code's. ## Deletions, each with its grep `grep -n` for remaining callers before removing (call-site line numbers are the pre-change ones): - `unquote` — callers were `parseInterval:332` and `fetchActive:690`. Both gone. `Parse.unquote` (105) is the surviving copy, and what `Timew.parseActive` and `Parse.interval` use. Deleted. - `parseInterval` — callers were `loadDay:789` and `fetchAll:812`, both now `Timew.parseIntervals`. Deleted. - `parseClock` — callers were `fetchDays:744` and `fetchAll:823`. `Parse.clock` covers both, through `Timew.parseDayTotals` and `Timew.parseRangeTotal`. Deleted. - `parseDuration` — only caller was `parseInterval:331`. Deleted with it. - `matchesFilter` — callers were `fetchAll:813` (gone), `activityRows:1127`, and the row button's `selected:1163`. The two survivors now read `Timew.tagMatches(row.tag, filter)`, the disposition wave 1 recorded above `Timew.tagMatches`. Deleted. - kept, callers remain: `trim` (`startTag`, `setFilter`), `shQuote` (`Timew.tagArg`, `Timew.cmd.intervalsForDay`, `startTag`), `formatSpan` (`render`), `formatElapsed` (`render`), `tagColor` (`activityRows`), `elapsed` (`render`). These go with the `Fmt`/`Parse` call-site move, which is the next PR. `test.luau` follows the two exports that no longer exist: `panel.parseDuration` and `panel.parseClock` became `panel.Parse.duration` / `panel.Parse.clock`, the same assert list against the copy that survived. The bar/panel drift comment no longer claims panel has a private `unquote`, and the frozen-literals comment no longer says the fetch functions are still there to be diffed against. ## New coverage: bar's stop command The suite pinned every query builder and bar's *start* command, but nothing pinned `"timew stop"` against a real call site — the stop branch of `bar.onClick` is reachable only once `sync` has seen a live interval, which nothing in the suite had done. Added, at the end of the drift section: ```lua syncCb({ stdout = "1000 focus\n" }) barEnv.onClick() assert(lastCmd == panel.Timew.cmd.exec("timew stop"), "bar is tracking, so onClick stops") ``` bar's start assert now composes `Timew.cmd.exec` as well, so the `2>&1` redirect is pinned against the builder instead of a second inline literal. **Why no `Timew.cmd.start`/`Timew.cmd.stop`.** They were considered as test-only frozen literals, and skipped: no call site may use them (see the invariant above), so they would be two functions with zero callers, and the asserts pinning them would compare a builder against its own literal — a tautology that cannot fail when `startTag`, `onStop` or `onIpc` changes. `test.luau:274` already freezes `"timew stop"` inside the `cmd.exec` assert, and `test.luau:408` freezes `"timew start"` plus its quoting against bar's *actual* command. The one real gap was bar's stop branch, and the assert above closes it without adding dead code to a module on a 25 ms per-handler budget. panel's own three call sites stay unreachable from `test.luau` — the luau CLI gives each required module its own global table, so no stub of panel's `runAsync` is visible inside it. That gap is the no-`require` rule's, and it closes at plugin API 4. ## Equivalence evidence This PR changes live behaviour, so here is the proof that it does not. Throwaway harness, not committed (`/tmp/t3b/`): `git show main:timew/panel.luau` and the new file are each wrapped with a `noctalia.runAsync` stub and an epilogue that exposes the module state, then both are driven through the same 21 scenarios — all eight paths, including idle/live/untagged/quoted active intervals, a prose day total, six tags against the cap, `loadDay` with `keepScope`, without it and out of range, both `fetchAll` commands, the shell injection strings, and `run` on exit 0 and exit 1 with the whole `refresh` cascade behind it. Compared per scenario, field by field: every issued command string, every timeout, the `notifyError` text, every `noctalia.state.set` write, and `active`, `startEpoch`, `tags`, `total`, `recent`, `days`, `dayRows`, `allRows`, `allTotal`, `mode`, `selected`, `listOffset`, `filter`. ``` 21 scenarios, 511 field comparisons, 0 differences ok ``` Negative controls, so that count means something: changing the `dayTotals` timeout to 14000 reports 5 differences; dropping the `RECENT_MAX` cap reports `tags/six.state.recent.5` and `.6`. ## Gate ``` $ luau test.luau ok $ noctalia plugins lint timew 0 errors, 0 warnings $ git diff --stat main...HEAD test.luau | 28 +++++-- timew/panel.luau | 228 +++++++++++++------------------------------------------ 2 files changed, 72 insertions(+), 184 deletions(-) ``` ## For the reviewer to challenge - `fetchDays` passes `DAYS`, `loadDay` passes `DAY_WALK_MAX`, `fetchAll` passes `ALL_WALK_MAX`, rather than relying on `Timew`'s defaults. Deliberate: the command strings stay byte-identical to the old ones (the harness compares them), and the bound reads next to the caller that owns it. `test.luau` separately pins that an explicit value equals the default. - `run` ignores `Timew.exec`'s `ok` argument. That matches the old behaviour: the panel refetches after a failure too, since a failed start otherwise leaves it claiming the old state. - `loadDay`'s state logic (pager rewind, scope reset, the `keepScope` path) is copied unchanged; only its tail is new. Worth a read against `main` — the harness covers all three branches.
The eight fetch functions keep their names, their side effects and their
module state, and lose their bodies: each one now calls Timew and assigns
what the callback hands back. With the inline command strings and inline
parsing gone, timewarrior has exactly one route out of this file.

The RECENT_MAX cap moves to fetchTags, since Timew.parseTags deliberately
returns every tag (test.luau pins that). matchesFilter is deleted in favour
of Timew.tagMatches, as wave 1 recorded; unquote, parseInterval, parseClock
and parseDuration lost their last callers with the inline parsing and go
with it. trim, shQuote, formatSpan, formatElapsed and tagColor still have
callers and stay until the call sites move.

Equivalence was checked outside the tree: the old and new modules were both
loaded with runAsync stubbed and driven through 21 scenarios covering all
eight paths, comparing every issued command string, every timeout and every
state field. 511 comparisons, 0 differences.
The stop branch of bar.onClick was unasserted: it is reachable only after
sync has seen a live interval, which nothing in the suite did before the
drift section. "timew stop" is one of the two commands no builder owns, so
this is the only thing that would catch a typo in bar's copy.

bar's start assert now composes Timew.cmd.exec too, so the 2>&1 redirect is
pinned against the builder rather than a second inline literal.
yuris left a comment

Verdict: REQUEST_CHANGES

I rebuilt the equivalence check from scratch rather than take the deleted harness on faith: git show main:timew/panel.luau and the branch file, each wrapped in the same runAsync/ui/panel stub with a probe spliced into the return table, driven through 45 scenarios / 2521 field comparisons — 0 differences, cascades included. The rewire is faithful, the deletions are clean, the quoting is single-layered on every path, and loadDay's state logic is byte-identical. Two things do not hold up: fetchAll moved the tag test from callback time to issue time, which is a real behaviour change under a mid-flight setFilter, and the RECENT_MAX cap now has no guard at all — I deleted it in the worktree and the suite still printed ok, which is exactly what two comments in this diff's neighbourhood promise cannot happen.

Findings

1. Tag the all-time rows at callback time, not at issue time — timew/panel.luau:696-705 (P2)

main filtered inside the callback (matchesFilter(row.tag) reads the module's filter), so a walk that outlived its tag still produced rows for the tag that is current now. The branch passes filter into Timew.allIntervals, which closes over it and applies it in Timew.parseIntervals(result.stdout, tag) — the issue-time tag.

The two differ whenever filter changes while a walk is in flight, and that is a one-click path: in all-time scope, onRow1..8 → filterFromRow → setFilter, or a second Enter in the search box (onSearchSubmit:874), both fire while the previous 200-id DOM walk is still spawning timew get subprocesses under a 20 s timeout. If the older walk lands last, allRows is overwritten with the previous tag's rows while the chip shows the new one, and it sticks: update():1308 only re-renders, so nothing refetches until the user acts again. activityRows:1002 short-circuits on mode == "all" and does not re-filter at render, so the stale rows are displayed as-is.

Reproduced against both files with the same stub — issue fetchAll() under filter = "focus", change the filter to "email", then land the walk:

main:   stale-callback allRows tags: [email]           (module filter is now 'email')
branch: stale-callback allRows tags: [focus, focus]    (module filter is now 'email')

A stale-drop guard fixes it and also stops allTotal going stale, which main got wrong in the other direction (its rows followed the new tag while its total was still the old tag's):

function fetchAll()
	-- Both callbacks carry the filter that issued them: a 200-id walk can outlive
	-- the tag it was asked for, and the newer fetchAll owns the list by then.
	local wanted = filter
	Timew.allIntervals(ALL_WALK_MAX, wanted, function(rows)
		if wanted ~= filter then
			return
		end
		allRows = rows
		render()
	end)
	Timew.rangeTotal(wanted, function(value)
		if wanted ~= filter then
			return
		end
		allTotal = value
		render()
	end)
end

2. The RECENT_MAX cap is unguarded, and two comments say it is guarded — timew/panel.luau:647-655 (P2)

I did the mutation the comments invite. In the worktree, recent = table.move(list, 1, math.min(#list, RECENT_MAX), 1, {}) → recent = list:

$ luau test.luau
ok
$ noctalia plugins lint timew
0 errors, 0 warnings

(File restored byte-identically afterwards; cmp clean, git status --porcelain empty.)

Nothing fails, because the cap moved from parseTags — reachable through the export table — to fetchTags, a module local that no require can see at plugin API 3. My own harness catches it (18 differing lines, tags/six/state.recent.5 = admin, .6 = reading), but the repo's suite cannot, and that is the suite that gates the merge.

Two load-bearing comments now say otherwise, and this PR is the event that made them false:

  • timew/panel.luau:363-366 — "test.luau asserts the absence of a cap rather than trusting the claim: a rewire that drops the cap has to fail the suite". The rewire has happened; dropping the cap does not fail the suite.
  • test.luau:175-179 — "this is what fails if a rewire forgets it — the panel cannot clip, so an uncapped chip row overflows it silently". assert(#many == 6) pins parseTags' no-cap contract, which is untouched by forgetting the caller-side cap. It fails only if someone adds a cap to parseTags, the opposite hazard.

The assert is still worth having; the claim around it is not. Concretely, at timew/panel.luau:360-366:

-- Every tag timew printed, in its order. The count is the caller's business:
-- RECENT_MAX is how many the panel has room for, not how many exist. This is
-- the one place the new layer disagrees with the old code, which capped inside
-- the loop, so test.luau pins the absence of a cap here -- that assert fails if
-- a cap is ever put back into this parser. The caller's cap (fetchTags:652) is
-- the unguarded half: it is a module local, so no require can reach it until
-- plugin API 4, and the chip row it protects sits in a panel that cannot clip.

and the matching sentence at test.luau:175-179 should say the same thing, rather than claiming this assert catches a forgotten cap.

3. The or DEFAULT fallbacks in Timew.cmd.* are now provably dead — timew/panel.luau:659-666 (P3)

Answering your first challenge item with a deletion rather than a preference. Passing DAYS, DAY_WALK_MAX and ALL_WALK_MAX explicitly is the right half: the bound reads next to the caller that owns it, and it costs one argument. The default is the redundant half. Before this PR Timew had no callers at all, so (days or DAYS), (walkMax or DAY_WALK_MAX) and (walkMax or ALL_WALK_MAX) were plausibly for a future one. This PR is that future, and all three call sites pass the value — so the fallback branch is now reachable only from the three asserts that exist to pin it:

test.luau:245  assert(T.cmd.dayTotals(14) == DAY_HEAD .. DAY_TAIL, "an explicit 14 is the same string as the default")
test.luau:256  assert(T.cmd.intervalsForDay("2026-08-31", 60) == T.cmd.intervalsForDay("2026-08-31"), "an explicit 60 is the default")
test.luau:263  assert(T.cmd.allIntervals(200) == T.cmd.allIntervals(), "an explicit 200 is the default")

That is the shape you rejected for Timew.cmd.start/stop: a code path with no call site, plus an assert comparing a builder against another spelling of itself. Verified harmless — omitting all three arguments at the call sites reproduces main byte for byte (0 differing lines over the same 2521 comparisons) — so this is weight, not risk. Drop or DAYS / or DAY_WALK_MAX / or ALL_WALK_MAX at panel.luau:449, :464, :481, make the parameters required, and delete those three asserts: a future caller that forgets the bound then fails loudly on nil arithmetic instead of silently getting 14. Net −6 lines. Not a merge blocker.

4. The new stop assert leaves bar tracking — test.luau:461-463 (P3)

syncCb({ stdout = "1000 focus\n" }) makes bar active and onClick() does not flip that back — sync() runs only in the callback, which the assert never fires. So the file now ends with bar's module state tracking. test.luau:399-402 exists precisely to catch that ("pin it, so a later assert that leaves bar tracking fails here"), but it runs earlier, so it cannot: anything appended after line 463 silently takes the stop branch. You restore barEnv.os one line later for exactly this reason; restore the state too.

syncCb({ stdout = "1000 focus\n" })
barEnv.onClick()
assert(lastCmd == panel.Timew.cmd.exec("timew stop"), "bar is tracking, so onClick stops")
-- Back to idle, so the "bar is idle" pin above still holds for anything appended.
syncCb({ stdout = "\n" })

Verified

Gate, in the worktree.

$ nix shell nixpkgs#luau -c luau test.luau
ok
$ noctalia plugins lint timew
yuris/timew
  ok

0 errors, 0 warnings

My own equivalence run. Both git show main:timew/panel.luau and the branch file wrapped with the same stub (noctalia.runAsync queues instead of running; noctalia.state.*, noctalia.tr, noctalia.notifyError, panel.render, panel.setWantsSecondTicks, ui.* and os.time all recorded or frozen), with a probe spliced into the return table exposing the module state and the eight fetch entry points. Each scenario drains the whole queue in order — nested calls included — and compares every issued command string, every timeout, every state.set write, every notifyError text, the full module state, and the rendered tree.

main lines: 2521  branch lines: 2521
45 scenarios, 2521 field comparisons, 0 differences

Covered per path: fetchActive idle/live/untagged/quoted/garbage; fetchTotal normal/empty; fetchTags 0/1/4/6/whitespace-and-blank/raw-awk-output; fetchDays plain/filtered/it's/x'; rm -rf /; #, plus the callback re-entering loadDay(selected, true) both when days[selected] exists and when it does not; loadDay same index / other index / keepScope / keepScope on the same index / out of range with and without keepScope / a date containing a quote / with a tag filter set; fetchAll unfiltered / filtered / multi-word / prose total / injection / apostrophe / (untagged); run on exit 0 and exit 1 with the whole refresh cascade behind it; startTag plain / injection / whitespace-only; setFilter in day scope, all scope and clearing; refresh in both scopes.

Negative controls, so that 0 means something.

dayTotals timeout 15000 -> 14000     32 differing lines
intervalsForDay timeout 10000 -> 9000    42 differing lines
recent = table.move(...) -> recent = list    18 differing lines
  > tags/six/state.recent.5 = admin
  > tags/six/state.recent.6 = reading
allIntervals drops the tag filter    490 differing lines
intervalsForDay gains a tag filter   718 differing lines

Every timeout is compared on every path, in both directions; a 1000 ms change is detectable, and none of 3000/3000/3000/15000/10000/20000/10000/3000 moved.

The cap, directly. 0 tags -> {}; 1 -> 1; 4 -> 4; 6 -> the first 4 in order (focus, deep work, email, review), identical to main's in-loop #recent < RECENT_MAX. table.move(list, 1, 0, 1, {}) is a no-op that returns the fresh table, so the empty case is safe, and the destination is always new — recent never aliases what parseTags returned. The mutation result is finding 2.

Quoting, through a real /bin/sh with a fake timew on PATH logging its argv. The commands were taken verbatim out of the branch's own output, not retyped:

ARGV: [summary] [:all] [x'; rm -rf /; #]
ARGV: [summary] [:all] [it's]
ARGV: [summary] [2026-09-01] [-] [2026-09-02] [x'; rm -rf /; #]
ARGV: [start] [x'; rm -rf /; #]

One argument each, nothing executed, the canary file untouched. loadDay's date reaches the shell as d='2026-08-31'\''x'; for a date carrying a quote. Exactly one layer of quoting everywhere: filter and day.date go into Timew bare, Timew.tagArg / Timew.cmd.intervalsForDay are the only quoters, and the composed strings are byte-identical to main's.

Deletions. No surviving caller for any of the five, over timew/*.luau and test.luau: unquote (bar's private copy at bar.luau:94 is bar's own, and Parse.unquote is what the compare-against assert now names), parseInterval (zero hits), parseClock / parseDuration (a comment at test.luau:6 only), matchesFilter (a comment at panel.luau:325 only). panel.parseDuration / panel.parseClock are gone from the return table and test.luau no longer names them. Every helper kept still has a live caller — trim (730, 738), shQuote (417, 466, 734), tagColor (1028), formatSpan (1067), formatElapsed and elapsed (1076) — so nothing was orphaned by the deletions.

Invariants. runAsync appears 8 times as a call, all inside Timew (508, 515, 522, 529, 536, 545, 552, 570), plus 3 in prose; run("timew is exactly 3 (734, 897, 1328). No getfenv/setfenv anywhere in timew/. git diff --name-status main...HEAD is M test.luau and M timew/panel.luau and nothing else — no excluded file in the diff. test.luau's no-cap assert is untouched: the hunks are at 5, 211, 376, 405 and 451, and the assert sits at 177 (main) / 179 (branch), outside all of them.

loadDay line by line. The pager/scope block, function loadDay through local day = days[index], diffs empty against main — 15 lines, byte for byte, listOffset rewind and keepScope included. All six branch combinations are in the run above and match, including the out-of-range early return issuing no command while still having moved selected and mode.

Answers

DAYS / DAY_WALK_MAX / ALL_WALK_MAX passed explicitly rather than defaulted. The explicit argument is right; keep it. The default it overrides is what should go — see finding 3. There is no drift risk either way (both spellings read the same constant, and omitting all three reproduces main exactly), so this is weight rather than a bug: three or branches and three asserts kept alive for a call site that does not exist.

run ignoring Timew.exec's ok. Correct, and it matches main exactly. The ordering is preserved too — notifyError fires inside Timew.exec before cb(ok) reaches pokeBar() and refresh(), which is the order main had inline. The run/fail scenario compares the notify text, the poke write and the entire five-command refresh cascade behind it: no differences. The comment you added above it is accurate.

loadDay's state logic copied unchanged. Verified textually and behaviourally, both above. Nothing shifted. One note that is not an objection: fetchDays' callback re-enters loadDay(selected, true) on every refresh, so the index ~= selected guard is the only thing keeping a reader on page 3, and it is unchanged and covered (days/reenter, loadDay/keepscope_same).

No Timew.cmd.start / Timew.cmd.stop. Agreed, and the reasoning holds up under checking rather than just reading well. bar.luau:215 builds "timew stop" .. " 2>&1" from its own literal, so the new assert at test.luau:463 compares two independently-built strings and is not a tautology — and it is anchored, because test.luau:274 freezes T.cmd.exec("timew stop") == "timew stop 2>&1" against a literal and :280-285 freezes tagArg. So rewriting the bar start assert to panel.Timew.cmd.exec("timew start" .. panel.Timew.tagArg(tag)) did not weaken it either: both halves it now leans on are pinned to literals elsewhere in the same file. Adding the builders would have added two callerless functions to a module on a 25 ms budget to buy asserts that cannot fail. The one thing to fix in that new block is finding 4.

**Verdict: REQUEST_CHANGES** I rebuilt the equivalence check from scratch rather than take the deleted harness on faith: `git show main:timew/panel.luau` and the branch file, each wrapped in the same `runAsync`/`ui`/`panel` stub with a probe spliced into the return table, driven through **45 scenarios / 2521 field comparisons — 0 differences**, cascades included. The rewire is faithful, the deletions are clean, the quoting is single-layered on every path, and `loadDay`'s state logic is byte-identical. Two things do not hold up: `fetchAll` moved the tag test from callback time to issue time, which is a real behaviour change under a mid-flight `setFilter`, and the `RECENT_MAX` cap now has no guard at all — I deleted it in the worktree and the suite still printed `ok`, which is exactly what two comments in this diff's neighbourhood promise cannot happen. ## Findings ### 1. Tag the all-time rows at callback time, not at issue time — `timew/panel.luau:696-705` (P2) `main` filtered inside the callback (`matchesFilter(row.tag)` reads the module's `filter`), so a walk that outlived its tag still produced rows for the tag that is current *now*. The branch passes `filter` into `Timew.allIntervals`, which closes over it and applies it in `Timew.parseIntervals(result.stdout, tag)` — the issue-time tag. The two differ whenever `filter` changes while a walk is in flight, and that is a one-click path: in all-time scope, `onRow1..8` → `filterFromRow` → `setFilter`, or a second Enter in the search box (`onSearchSubmit:874`), both fire while the previous 200-id DOM walk is still spawning `timew get` subprocesses under a 20 s timeout. If the older walk lands last, `allRows` is overwritten with the *previous* tag's rows while the chip shows the new one, and it sticks: `update():1308` only re-renders, so nothing refetches until the user acts again. `activityRows:1002` short-circuits on `mode == "all"` and does not re-filter at render, so the stale rows are displayed as-is. Reproduced against both files with the same stub — issue `fetchAll()` under `filter = "focus"`, change the filter to `"email"`, then land the walk: ``` main: stale-callback allRows tags: [email] (module filter is now 'email') branch: stale-callback allRows tags: [focus, focus] (module filter is now 'email') ``` A stale-drop guard fixes it and also stops `allTotal` going stale, which `main` got wrong in the other direction (its rows followed the new tag while its total was still the old tag's): ```suggestion function fetchAll() -- Both callbacks carry the filter that issued them: a 200-id walk can outlive -- the tag it was asked for, and the newer fetchAll owns the list by then. local wanted = filter Timew.allIntervals(ALL_WALK_MAX, wanted, function(rows) if wanted ~= filter then return end allRows = rows render() end) Timew.rangeTotal(wanted, function(value) if wanted ~= filter then return end allTotal = value render() end) end ``` ### 2. The `RECENT_MAX` cap is unguarded, and two comments say it is guarded — `timew/panel.luau:647-655` (P2) I did the mutation the comments invite. In the worktree, `recent = table.move(list, 1, math.min(#list, RECENT_MAX), 1, {})` → `recent = list`: ``` $ luau test.luau ok $ noctalia plugins lint timew 0 errors, 0 warnings ``` (File restored byte-identically afterwards; `cmp` clean, `git status --porcelain` empty.) Nothing fails, because the cap moved from `parseTags` — reachable through the export table — to `fetchTags`, a module local that no `require` can see at plugin API 3. My own harness catches it (18 differing lines, `tags/six/state.recent.5 = admin`, `.6 = reading`), but the repo's suite cannot, and that is the suite that gates the merge. Two load-bearing comments now say otherwise, and this PR is the event that made them false: - `timew/panel.luau:363-366` — *"test.luau asserts the absence of a cap rather than trusting the claim: a rewire that drops the cap has to fail the suite"*. The rewire has happened; dropping the cap does not fail the suite. - `test.luau:175-179` — *"this is what fails if a rewire forgets it — the panel cannot clip, so an uncapped chip row overflows it silently"*. `assert(#many == 6)` pins `parseTags`' no-cap contract, which is untouched by forgetting the caller-side cap. It fails only if someone *adds* a cap to `parseTags`, the opposite hazard. The assert is still worth having; the claim around it is not. Concretely, at `timew/panel.luau:360-366`: ```suggestion -- Every tag timew printed, in its order. The count is the caller's business: -- RECENT_MAX is how many the panel has room for, not how many exist. This is -- the one place the new layer disagrees with the old code, which capped inside -- the loop, so test.luau pins the absence of a cap here -- that assert fails if -- a cap is ever put back into this parser. The caller's cap (fetchTags:652) is -- the unguarded half: it is a module local, so no require can reach it until -- plugin API 4, and the chip row it protects sits in a panel that cannot clip. ``` and the matching sentence at `test.luau:175-179` should say the same thing, rather than claiming this assert catches a forgotten cap. ### 3. The `or DEFAULT` fallbacks in `Timew.cmd.*` are now provably dead — `timew/panel.luau:659-666` (P3) Answering your first challenge item with a deletion rather than a preference. Passing `DAYS`, `DAY_WALK_MAX` and `ALL_WALK_MAX` explicitly is the right half: the bound reads next to the caller that owns it, and it costs one argument. The *default* is the redundant half. Before this PR `Timew` had no callers at all, so `(days or DAYS)`, `(walkMax or DAY_WALK_MAX)` and `(walkMax or ALL_WALK_MAX)` were plausibly for a future one. This PR is that future, and all three call sites pass the value — so the fallback branch is now reachable only from the three asserts that exist to pin it: ``` test.luau:245 assert(T.cmd.dayTotals(14) == DAY_HEAD .. DAY_TAIL, "an explicit 14 is the same string as the default") test.luau:256 assert(T.cmd.intervalsForDay("2026-08-31", 60) == T.cmd.intervalsForDay("2026-08-31"), "an explicit 60 is the default") test.luau:263 assert(T.cmd.allIntervals(200) == T.cmd.allIntervals(), "an explicit 200 is the default") ``` That is the shape you rejected for `Timew.cmd.start`/`stop`: a code path with no call site, plus an assert comparing a builder against another spelling of itself. Verified harmless — omitting all three arguments at the call sites reproduces `main` byte for byte (0 differing lines over the same 2521 comparisons) — so this is weight, not risk. Drop `or DAYS` / `or DAY_WALK_MAX` / `or ALL_WALK_MAX` at `panel.luau:449`, `:464`, `:481`, make the parameters required, and delete those three asserts: a future caller that forgets the bound then fails loudly on `nil` arithmetic instead of silently getting 14. Net −6 lines. Not a merge blocker. ### 4. The new stop assert leaves bar tracking — `test.luau:461-463` (P3) `syncCb({ stdout = "1000 focus\n" })` makes bar active and `onClick()` does not flip that back — `sync()` runs only in the callback, which the assert never fires. So the file now ends with bar's module state tracking. `test.luau:399-402` exists precisely to catch that (*"pin it, so a later assert that leaves bar tracking fails here"*), but it runs earlier, so it cannot: anything appended after line 463 silently takes the stop branch. You restore `barEnv.os` one line later for exactly this reason; restore the state too. ```suggestion syncCb({ stdout = "1000 focus\n" }) barEnv.onClick() assert(lastCmd == panel.Timew.cmd.exec("timew stop"), "bar is tracking, so onClick stops") -- Back to idle, so the "bar is idle" pin above still holds for anything appended. syncCb({ stdout = "\n" }) ``` ## Verified **Gate, in the worktree.** ``` $ nix shell nixpkgs#luau -c luau test.luau ok $ noctalia plugins lint timew yuris/timew ok 0 errors, 0 warnings ``` **My own equivalence run.** Both `git show main:timew/panel.luau` and the branch file wrapped with the same stub (`noctalia.runAsync` queues instead of running; `noctalia.state.*`, `noctalia.tr`, `noctalia.notifyError`, `panel.render`, `panel.setWantsSecondTicks`, `ui.*` and `os.time` all recorded or frozen), with a probe spliced into the return table exposing the module state and the eight fetch entry points. Each scenario drains the whole queue in order — nested calls included — and compares every issued command string, every timeout, every `state.set` write, every `notifyError` text, the full module state, and the rendered tree. ``` main lines: 2521 branch lines: 2521 45 scenarios, 2521 field comparisons, 0 differences ``` Covered per path: `fetchActive` idle/live/untagged/quoted/garbage; `fetchTotal` normal/empty; `fetchTags` 0/1/4/6/whitespace-and-blank/raw-awk-output; `fetchDays` plain/filtered/`it's`/`x'; rm -rf /; #`, plus the callback re-entering `loadDay(selected, true)` both when `days[selected]` exists and when it does not; `loadDay` same index / other index / `keepScope` / `keepScope` on the same index / out of range with and without `keepScope` / a date containing a quote / with a tag filter set; `fetchAll` unfiltered / filtered / multi-word / prose total / injection / apostrophe / `(untagged)`; `run` on exit 0 and exit 1 with the whole `refresh` cascade behind it; `startTag` plain / injection / whitespace-only; `setFilter` in day scope, all scope and clearing; `refresh` in both scopes. **Negative controls, so that 0 means something.** ``` dayTotals timeout 15000 -> 14000 32 differing lines intervalsForDay timeout 10000 -> 9000 42 differing lines recent = table.move(...) -> recent = list 18 differing lines > tags/six/state.recent.5 = admin > tags/six/state.recent.6 = reading allIntervals drops the tag filter 490 differing lines intervalsForDay gains a tag filter 718 differing lines ``` Every timeout is compared on every path, in both directions; a 1000 ms change is detectable, and none of 3000/3000/3000/15000/10000/20000/10000/3000 moved. **The cap, directly.** 0 tags -> `{}`; 1 -> 1; 4 -> 4; 6 -> the first 4 in order (`focus, deep work, email, review`), identical to `main`'s in-loop `#recent < RECENT_MAX`. `table.move(list, 1, 0, 1, {})` is a no-op that returns the fresh table, so the empty case is safe, and the destination is always new — `recent` never aliases what `parseTags` returned. The mutation result is finding 2. **Quoting, through a real `/bin/sh` with a fake `timew` on `PATH` logging its argv.** The commands were taken verbatim out of the branch's own output, not retyped: ``` ARGV: [summary] [:all] [x'; rm -rf /; #] ARGV: [summary] [:all] [it's] ARGV: [summary] [2026-09-01] [-] [2026-09-02] [x'; rm -rf /; #] ARGV: [start] [x'; rm -rf /; #] ``` One argument each, nothing executed, the canary file untouched. `loadDay`'s date reaches the shell as `d='2026-08-31'\''x';` for a date carrying a quote. Exactly one layer of quoting everywhere: `filter` and `day.date` go into `Timew` bare, `Timew.tagArg` / `Timew.cmd.intervalsForDay` are the only quoters, and the composed strings are byte-identical to `main`'s. **Deletions.** No surviving caller for any of the five, over `timew/*.luau` and `test.luau`: `unquote` (bar's private copy at `bar.luau:94` is bar's own, and `Parse.unquote` is what the compare-against assert now names), `parseInterval` (zero hits), `parseClock` / `parseDuration` (a comment at `test.luau:6` only), `matchesFilter` (a comment at `panel.luau:325` only). `panel.parseDuration` / `panel.parseClock` are gone from the return table and `test.luau` no longer names them. Every helper kept still has a live caller — `trim` (730, 738), `shQuote` (417, 466, 734), `tagColor` (1028), `formatSpan` (1067), `formatElapsed` and `elapsed` (1076) — so nothing was orphaned by the deletions. **Invariants.** `runAsync` appears 8 times as a call, all inside `Timew` (508, 515, 522, 529, 536, 545, 552, 570), plus 3 in prose; `run("timew` is exactly 3 (734, 897, 1328). No `getfenv`/`setfenv` anywhere in `timew/`. `git diff --name-status main...HEAD` is `M test.luau` and `M timew/panel.luau` and nothing else — no excluded file in the diff. `test.luau`'s no-cap assert is untouched: the hunks are at 5, 211, 376, 405 and 451, and the assert sits at 177 (`main`) / 179 (branch), outside all of them. **`loadDay` line by line.** The pager/scope block, `function loadDay` through `local day = days[index]`, diffs empty against `main` — 15 lines, byte for byte, `listOffset` rewind and `keepScope` included. All six branch combinations are in the run above and match, including the out-of-range early return issuing no command while still having moved `selected` and `mode`. ## Answers **`DAYS` / `DAY_WALK_MAX` / `ALL_WALK_MAX` passed explicitly rather than defaulted.** The explicit argument is right; keep it. The default it overrides is what should go — see finding 3. There is no drift risk either way (both spellings read the same constant, and omitting all three reproduces `main` exactly), so this is weight rather than a bug: three `or` branches and three asserts kept alive for a call site that does not exist. **`run` ignoring `Timew.exec`'s `ok`.** Correct, and it matches `main` exactly. The ordering is preserved too — `notifyError` fires inside `Timew.exec` before `cb(ok)` reaches `pokeBar()` and `refresh()`, which is the order `main` had inline. The `run/fail` scenario compares the notify text, the poke write and the entire five-command refresh cascade behind it: no differences. The comment you added above it is accurate. **`loadDay`'s state logic copied unchanged.** Verified textually and behaviourally, both above. Nothing shifted. One note that is not an objection: `fetchDays`' callback re-enters `loadDay(selected, true)` on every refresh, so the `index ~= selected` guard is the only thing keeping a reader on page 3, and it is unchanged and covered (`days/reenter`, `loadDay/keepscope_same`). **No `Timew.cmd.start` / `Timew.cmd.stop`.** Agreed, and the reasoning holds up under checking rather than just reading well. `bar.luau:215` builds `"timew stop" .. " 2>&1"` from its own literal, so the new assert at `test.luau:463` compares two independently-built strings and is not a tautology — and it is anchored, because `test.luau:274` freezes `T.cmd.exec("timew stop") == "timew stop 2>&1"` against a literal and `:280-285` freezes `tagArg`. So rewriting the bar start assert to `panel.Timew.cmd.exec("timew start" .. panel.Timew.tagArg(tag))` did not weaken it either: both halves it now leans on are pinned to literals elsewhere in the same file. Adding the builders would have added two callerless functions to a module on a 25 ms budget to buy asserts that cannot fail. The one thing to fix in that new block is finding 4.
fetchAll passed the module `filter` into Timew.allIntervals and
Timew.rangeTotal, so both applied the tag the walk was issued with, where
the code this replaced tested the tag inside the callback. A 200-id walk
under a 20 s timeout outlives its tag whenever setFilter fires mid-flight
-- onRowN in all-time scope, or a second Enter in the search box -- and
whichever walk lands last owns the list: with the chip on 'email', a walk
issued under 'focus' relisted focus rows and the focus total, and it
stuck, because update() only re-renders and activityRows does not
re-filter in all-time scope. Both callbacks now carry the filter that
issued them and drop themselves once it has moved on, which also keeps
allTotal in step with the rows.

test.luau reaches the caller-side RECENT_MAX cap for the first time. The
rewire moved it out of Timew.parseTags, which the export table reaches,
into fetchTags, which no require can reach at plugin API 3, so the suite
could not see a dropped cap -- while two comments claimed it would fail.
The way in is the one bar.luau already needs: stub the host in panel's
own global table and count the chip row on the rendered tree. Nothing new
is exported, and no *fenv call enters timew/. Both comments now say what
the parseTags assert actually pins. The same stubs pin the stale-walk
drop on each callback separately, and restoring bar to idle after the
stop assert keeps the "bar is idle" pin valid for the section appended
below it.
Author
Owner

Both blockers fixed in 8aa25e7, and the cap is now guarded rather than promised. One finding pushed back with evidence: the or DEFAULT fallbacks are the suite's only reach into three constants, so dropping them repeats finding 2 rather than resolving it.

1. Stale-filter race in fetchAll — fixed, your guard applied

Reproduced first, on 0ee6121, with a throwaway harness (not committed): host stubbed in panel's own global table, runAsync queues, and the rows read back off the rendered tree so nothing has to be exported. Driven entirely through onIpc, which is the path onRowN and the search box take. All-time scope under focus, setFilter("email") mid-flight, then land the walks in the order that matters — the newer one first, the older one last:

BEFORE (0ee6121)
email walk landed:               allRows tags: [email]          allTotal: 3:22 (email's)   chip: email
focus walk landed after it:      allRows tags: [focus, focus]   allTotal: 9:11 (focus's)   chip: email
after update():                  allRows tags: [focus, focus]   allTotal: 9:11 (focus's)   chip: email

AFTER (8aa25e7)
email walk landed:               allRows tags: [email]          allTotal: 3:22 (email's)   chip: email
focus walk landed after it:      allRows tags: [email]          allTotal: 3:22 (email's)   chip: email
after update():                  allRows tags: [email]          allTotal: 3:22 (email's)   chip: email

update() is in there because it is what makes it stick: it only re-renders. The total goes with the rows, as you said — allTotal was landing at the old tag's 9:11 too.

The guard is yours verbatim: local wanted = filter before both calls, wanted into Timew.allIntervals and Timew.rangeTotal, and if wanted ~= filter then return end opening each callback. It also does not over-drop: clearing the tag (wanted == "") relists everything, which is asserted.

This is pinned in the suite now, per callback:

rows guard deleted                      -> test.luau:590: a walk issued under 'focus' cannot relist while the tag is 'email', got focus,focus
total guard deleted                     -> test.luau:602: the stale walk's all-time total cannot land either
fetchAll reverted to issue-time filter  -> test.luau:590: a walk issued under 'focus' cannot relist while the tag is 'email', got focus,focus

2. RECENT_MAX — comments corrected, and the cap is now actually guarded

The honesty problem went first. timew/panel.luau:354-363 and test.luau:173-179 now say that assert(#many == 6) pins the parser's absence of a cap and fails only if one is put back — the opposite hazard — and that the caller's cap is elsewhere. assert(#many == 6) is untouched.

Then the cap itself, because it turns out to be reachable without the two things that were off the table. No new export from timew/panel.luau, and no *fenv in timew/ (grep -c 'getfenv\|setfenv' timew/*.luau -> bar.luau:0, panel.luau:0). The route is the one test.luau already uses for bar.luau: getfenv(panel.formatElapsed) hands back panel's own global table, so the host stubs in, onOpen() issues the real refresh(), the tags callback is picked out by job.cmd == T.cmd.tags(), and the chip row is counted off the rendered tree by its onTagN handlers. Feed it six tags, get four chips, in timew's order.

Your mutation, which used to print ok:

$ sed -i 's/recent = table.move(list, 1, math.min(#list, RECENT_MAX), 1, {})/recent = list/' timew/panel.luau
$ luau test.luau
./test.luau:560: fetchTags caps the chip row at RECENT_MAX: got 6 chips for six tags
exit=1

(Restored byte-identically after each run; cmp clean.)

So the comment at panel.luau:354-363 says the cap is checked, because it is, and names how. The section sits at the end of test.luau because it leaves the panel holding stubs.

3. or DAYS / or DAY_WALK_MAX / or ALL_WALK_MAX — keeping both halves, and here is why

Pushing back on this one. The premise is that the fallback branch is reachable only from three asserts; the measurement says the opposite — the fallback branch is the suite's only reach into the values of DAYS, DAY_WALK_MAX and ALL_WALK_MAX. They are module locals, absent from the export table: exactly the shape finding 2 is about. T.cmd.dayTotals() is what pins DAYS at 14, by way of seq 0 13.

Applying finding 3 exactly as specified — drop the three or branches, make the parameters required, delete the three explicit-argument asserts, and pass 14/60/200 in the frozen-literal asserts — then drifting one constant:

as merged, DAYS 14 -> 30                  -> test.luau:254: day totals: no tag, and days defaults to DAYS
finding 3 applied, DAYS still 14          -> ok
finding 3 applied, DAYS 14 -> 30          -> ok
as merged, DAY_WALK_MAX 60 -> 90          -> test.luau:261: one day's walk: ... walkMax defaults to DAY_WALK_MAX
finding 3 applied, DAY_WALK_MAX 60 -> 90  -> ok

Lines 3 and 5 are the cost. DAYS is not just a bound: render walks math.min(DAYS, #days) bars, RIGHT_W is BAR_W * DAYS + BAR_GAP * (DAYS - 1), and the onDay1..onDay14 block must be exactly DAYS long or every bar past the fourteenth emits an onClick naming a function that does not exist and clicks nothing — the hazard panel.luau:768-775 is written for. Today that drift fails the suite; with the defaults gone it is silent, and there is no way to pin the constant directly without exporting it.

The three explicit-argument asserts stay for the mirror-image reason: they are the only reach into the branch the panel's own call sites actually take. Cheap to keep, and a bound with neither half checked is a bound that is merely claimed. The argument is recorded at test.luau:243-249 so this does not get re-filed as weight.

4. Bar left tracking at the end of the file — fixed

Applied verbatim: syncCb({ stdout = "\n" }) after the stop assert, with the reason next to it. This one earned itself immediately — the new panel section is appended below, so the test.luau:409-412 pin had to be true again first.

Gate, in the worktree

$ nix shell nixpkgs#luau -c luau test.luau
ok
$ noctalia plugins lint timew
yuris/timew
  ok

0 errors, 0 warnings
$ git diff --stat main...HEAD
 test.luau        | 182 +++++++++++++++++++++++++++++++++++++---
 timew/panel.luau | 249 ++++++++++++++++---------------------------------------
 2 files changed, 242 insertions(+), 189 deletions(-)
$ git status --porcelain
(empty)

No require added between entries, no new export, no *fenv in timew/, no formatter run, and no timew data touched.

Both blockers fixed in `8aa25e7`, and the cap is now guarded rather than promised. One finding pushed back with evidence: the `or DEFAULT` fallbacks are the suite's only reach into three constants, so dropping them repeats finding 2 rather than resolving it. ## 1. Stale-filter race in `fetchAll` — fixed, your guard applied Reproduced first, on `0ee6121`, with a throwaway harness (not committed): host stubbed in panel's own global table, `runAsync` queues, and the rows read back off the rendered tree so nothing has to be exported. Driven entirely through `onIpc`, which is the path `onRowN` and the search box take. All-time scope under `focus`, `setFilter("email")` mid-flight, then land the walks in the order that matters — the newer one first, the older one last: ``` BEFORE (0ee6121) email walk landed: allRows tags: [email] allTotal: 3:22 (email's) chip: email focus walk landed after it: allRows tags: [focus, focus] allTotal: 9:11 (focus's) chip: email after update(): allRows tags: [focus, focus] allTotal: 9:11 (focus's) chip: email AFTER (8aa25e7) email walk landed: allRows tags: [email] allTotal: 3:22 (email's) chip: email focus walk landed after it: allRows tags: [email] allTotal: 3:22 (email's) chip: email after update(): allRows tags: [email] allTotal: 3:22 (email's) chip: email ``` `update()` is in there because it is what makes it stick: it only re-renders. The total goes with the rows, as you said — `allTotal` was landing at the old tag's `9:11` too. The guard is yours verbatim: `local wanted = filter` before both calls, `wanted` into `Timew.allIntervals` **and** `Timew.rangeTotal`, and `if wanted ~= filter then return end` opening each callback. It also does not over-drop: clearing the tag (`wanted == ""`) relists everything, which is asserted. This is pinned in the suite now, per callback: ``` rows guard deleted -> test.luau:590: a walk issued under 'focus' cannot relist while the tag is 'email', got focus,focus total guard deleted -> test.luau:602: the stale walk's all-time total cannot land either fetchAll reverted to issue-time filter -> test.luau:590: a walk issued under 'focus' cannot relist while the tag is 'email', got focus,focus ``` ## 2. `RECENT_MAX` — comments corrected, and the cap is now actually guarded The honesty problem went first. `timew/panel.luau:354-363` and `test.luau:173-179` now say that `assert(#many == 6)` pins the parser's *absence* of a cap and fails only if one is put back — the opposite hazard — and that the caller's cap is elsewhere. `assert(#many == 6)` is untouched. Then the cap itself, because it turns out to be reachable without the two things that were off the table. No new export from `timew/panel.luau`, and no `*fenv` in `timew/` (`grep -c 'getfenv\|setfenv' timew/*.luau` -> `bar.luau:0`, `panel.luau:0`). The route is the one `test.luau` already uses for `bar.luau`: `getfenv(panel.formatElapsed)` hands back panel's own global table, so the host stubs in, `onOpen()` issues the real `refresh()`, the tags callback is picked out by `job.cmd == T.cmd.tags()`, and the chip row is counted off the rendered tree by its `onTagN` handlers. Feed it six tags, get four chips, in timew's order. Your mutation, which used to print `ok`: ``` $ sed -i 's/recent = table.move(list, 1, math.min(#list, RECENT_MAX), 1, {})/recent = list/' timew/panel.luau $ luau test.luau ./test.luau:560: fetchTags caps the chip row at RECENT_MAX: got 6 chips for six tags exit=1 ``` (Restored byte-identically after each run; `cmp` clean.) So the comment at `panel.luau:354-363` says the cap is checked, because it is, and names how. The section sits at the end of `test.luau` because it leaves the panel holding stubs. ## 3. `or DAYS` / `or DAY_WALK_MAX` / `or ALL_WALK_MAX` — keeping both halves, and here is why Pushing back on this one. The premise is that the fallback branch is reachable only from three asserts; the measurement says the opposite — the fallback branch is the suite's **only** reach into the values of `DAYS`, `DAY_WALK_MAX` and `ALL_WALK_MAX`. They are module locals, absent from the export table: exactly the shape finding 2 is about. `T.cmd.dayTotals()` is what pins `DAYS` at 14, by way of `seq 0 13`. Applying finding 3 exactly as specified — drop the three `or` branches, make the parameters required, delete the three explicit-argument asserts, and pass 14/60/200 in the frozen-literal asserts — then drifting one constant: ``` as merged, DAYS 14 -> 30 -> test.luau:254: day totals: no tag, and days defaults to DAYS finding 3 applied, DAYS still 14 -> ok finding 3 applied, DAYS 14 -> 30 -> ok as merged, DAY_WALK_MAX 60 -> 90 -> test.luau:261: one day's walk: ... walkMax defaults to DAY_WALK_MAX finding 3 applied, DAY_WALK_MAX 60 -> 90 -> ok ``` Lines 3 and 5 are the cost. `DAYS` is not just a bound: `render` walks `math.min(DAYS, #days)` bars, `RIGHT_W` is `BAR_W * DAYS + BAR_GAP * (DAYS - 1)`, and the `onDay1..onDay14` block must be exactly `DAYS` long or every bar past the fourteenth emits an `onClick` naming a function that does not exist and clicks nothing — the hazard `panel.luau:768-775` is written for. Today that drift fails the suite; with the defaults gone it is silent, and there is no way to pin the constant directly without exporting it. The three explicit-argument asserts stay for the mirror-image reason: they are the only reach into the branch the panel's own call sites actually take. Cheap to keep, and a bound with neither half checked is a bound that is merely claimed. The argument is recorded at `test.luau:243-249` so this does not get re-filed as weight. ## 4. Bar left tracking at the end of the file — fixed Applied verbatim: `syncCb({ stdout = "\n" })` after the stop assert, with the reason next to it. This one earned itself immediately — the new panel section is appended below, so the `test.luau:409-412` pin had to be true again first. ## Gate, in the worktree ``` $ nix shell nixpkgs#luau -c luau test.luau ok $ noctalia plugins lint timew yuris/timew ok 0 errors, 0 warnings $ git diff --stat main...HEAD test.luau | 182 +++++++++++++++++++++++++++++++++++++--- timew/panel.luau | 249 ++++++++++++++++--------------------------------------- 2 files changed, 242 insertions(+), 189 deletions(-) $ git status --porcelain (empty) ``` No `require` added between entries, no new export, no `*fenv` in `timew/`, no formatter run, and no `timew` data touched.
yuris merged commit 854b9dae45 into main 2026-08-31 18:34:29 +00:00
yuris deleted branch refactor/t3b-rewire 2026-08-31 18:34:30 +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!6
No description provided.