Activity: computation and range syntax in the core #279
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #277. Needs #278.
Goal
The core computes what every host shows of an activity command for a range of days: each day's cell, the statistics in both modes and the matching shards. Plus the range syntax, shared by the CLI, the TUI and the app.
Plan
Range (
commands/activity/range.rs, pure)RangeSpec::parse(text): Errors name the accepted forms. Whole days only.RangeSpec::resolve(today, tracked_start) -> Range { from, to, unit }: clamped to[start, today], wherestartis the command'sstart, else the day of the first matching shard.unitisAll | Day | Month | Year | Days(n).label(): short text:All time,2026,Oct 2026,16 Oct 2026,3–17 Oct(28 Sep–4 Oct, across years28 Dec 2025–3 Jan 2026).step(±1)moves by the unit (a custom range by its length);can_stepis false for All and when the step would lie wholly after today or before the start.quick_picks(today): Today, Yesterday, Last 7 days, Last 30 days, This month, Last month, This year, Last year, All time, each as aRangeSpec.Shape::of(range):Day(1 day),Calendar(2–42 days),Grid(more, and All). Every host picks its chart by it.Computation (
commands/activity/compute.rs)query(query::find_outermost_shard, as anifreminder names its shards, R29), so a placement handed down to sub-headings counts once. Its moment is the shard's moment (LocalizedShard; the file name's unless a marker says otherwise), and its day is that moment's local date in the repository timezone (tz). A shard matching two series counts for both and is reported inshared(a warning, not an error).counts[series], the classificationNone | Single(i) | Mixed(Vec<i>), and how it is drawn:intensity = "count": level 1, 2, 3, 4 for 1, 2, 3, 4+ shards;fixed: always 4.split: all matching series in list order (a host draws two as diagonal halves, three or more as wedges);first: the first in list order;most: the most shards, a tie to the first in list order. The level of a mixed day comes from the sum of its shards.Future, days before the startUntracked. Both are left out of every statistic.shards,shards_per_series,tracked_days,active_days,per_week = shards / (tracked_days / 7); and the Days table in both modes, so a toggle needs no recomputation:series_days[i](mixed included),mixed_days,none_days;series_only_days[i],mixed_days,none_days.Percentages are of
tracked_days(inclusive ones may add up to more than 100 %). A range with 0 tracked days gives zeros, never a division by zero.ActivityShard { series: Vec<usize>, moment, day, file, line, summary }(summaryisshard_summary), paged:offset,limit,total.day_series(day)per series: count and first/last time, for the day tiles (Run 1 · at 06:45).Facade
api::activity(project, stream, key, range: &RangeSpec, now, tz, page) -> ActivityReport { range, label, shape, series (name, colour), days: Vec<DayCell>, stats, shards: Page<ActivityShard>, shared }.api::activity_range(text)parses for a host. Errors asCoreError(UnknownCommand,NotAnActivityCommand, a range parse error).Tests
The sample of the screenshots (Run and Climbing, Oct 2026, today = Sat 17 Oct 2026): 15 shards, 11 of 17 days active, per week 6.2, Run 7 incl. 2 mixed, Climbing 6, Mixed 2, None 6; exclusive mode;
startcutting days; a shard on two series; a sub-heading under a matching heading counted once;mosttie;fixed; a day at midnight across a DST change; every range form, label, step and clamp; the paging.Docs
R36 (computation, range syntax), AGENTS.md (the module and the facade functions).
streamd <key> [RANGE]prints the chart and the Days table #280streamd <key> -i) #281Implemented in #305.
Time: about 21 minutes (12:43–13:05 CEST), wall clock, including
nix flake checkandnix build .#streamd-app.Tokens: about 145k of context used (Claude Opus 5.5).
Findings during the implementation
shard_summaryneeds the text of the shard's file, soapi::activitycan't work fromStreamalone. It takes aStreamStoreand reads only the files of the requested page.allis cut, to run from the first tracked day to today, or today alone if nothing is tracked.startis taken over the whole stream, not just the range. That waycan_stepand theUntrackeddays agree across ranges.shards_per_seriesstill count it for each.UnknownCommand/NotAnActivityCommandvariants to reuse. The facade's existingConfigErrorwording is kept, and onlyCoreError::InvalidRangeis new. Because the bindings keep one variant perCoreErrorvariant, it crosses asStreamdException.InvalidRange, and the app's exhaustivereason()gained one arm. The report's records are left for #283.@HHMMSS:@1900isn't read as a time, which tripped a fixture.streamd <key> [RANGE]prints the chart and the Days table #280