Skip to content

Review follow-ups for 1.0.0b2: lap-press steps open in the execution analysis, health note window, event last day, display rounding - #40

Merged
futureweb merged 2 commits into
mainfrom
fix/review-followups-b2
Oct 15, 2026
Merged

futureweb merged 2 commits into
mainfrom
fix/review-followups-b2

Conversation

@futureweb

@futureweb futureweb commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Open follow-ups from the reviews of #36 and #39, wanted before release 1.0.0b2. No new tools, no
parameter or catalogue changes, no extra API requests.

1. Lap-press steps in the execution analysis (R36-11)

The duration or distance of an until_lap_press step is only a planning estimate, but
analyze_workout_execution treated it as the planned length. Lap-press steps are now open:

  • "ended by lap after (planning estimate )" instead of "-59 s vs plan",
    "shorter than planned" or a cap at the estimate with "extra time inside the plan";
    "ended with the recording" when the last step runs to the end of the activity.
  • Target adherence, time in target and the other metrics are evaluated as before.
  • The plan clock restarts at the actual lap, so the timed steps after it are judged from there (no
    "started 5:00 earlier than the plan timeline" after a short warm-up).
  • Alignment: any length costs little; ties go to the length nearer the estimate. With device
    auto-laps the step ends on the lap closed by the press (a partial lap), never on a full auto-lap.
    Its end boundary is never moved, split or extended by trailing laps. Its estimate no longer makes
    auto-laps of the same length look like lap presses. Carving a lap-less step uses the plan bounds,
    so it is skipped for a lap-press step.
  • JSON: row lap_press (ended_by, elapsed_s, moving_time_s, estimate_s/estimate_m,
    actual_m), planned.lap_press, summary lap_press_steps/lap_press_estimate_s. The plan total
    is marked "≈" with the estimate share.
  • get_activity_report key finding names the lap-press steps. get_activity_intervals(include_planned_types=true)
    shows "until lap press, ended by lap after …" and never beyond_plan_s for them.

Code paths checked with synthetic tests (tests/test_execution_lap_press.py):

  • exact laps: warm-up 12:00 / 16:01 / 25:00 against a 17:00 estimate;
  • device auto-laps (1 km, counter reset at the press): warm-up 7:00 / 10:00 / 20:00 against 10:00;
  • carried-over boundary: a timed warm-up ending inside an auto-lap, then a lap-press step;
  • lap-press recoveries inside repeats, with step laps and with auto-laps;
  • no-streams interval mapping, the auto-lap detection, a last step running to the end of the recording;
  • a fuzz test with 30 random plans × 2 lap layouts. Lap-press steps last 0.3-3× their estimate. Every
    timed step stays exact, has no start shift and has no length or timeline note.

The warm-up and auto-lap cases also fail on main, so the tests cover the change.

Live, read-only: activity i196751426, paired with event 144186380 (lap-press warm-up with a 17:00
estimate, 4×(3+3 min), 3 min, 10 min).

Before (main):

Plan: 11 steps, 54:00 planned | Actual: 12 intervals, 58:40 in total | matched 11, …
[plan 1] warmup 17:00 @ 110-175 W 'Press lap Einrollen bis Startpunkt' -> Press lap Einrollen bis Startpunkt (WORK) 16:01 from 0:00, -59 s vs plan, in range (110% of target)
compact:  [plan 1] warmup 17:00 @ 110-175 W: 16:01, 157 W, HR 126/143, 73% in target, in range (110% of target)
report:   - Plan: 11/11 steps executed; work steps 301/288/292/289/278 W vs 275-285 W planned (…); steps with clear deviations: 1
intervals:  [1] Intervals.icu WORK | plan step 1 warmup 17:00
JSON: rows[1..10].start_offset_s = -59.0 (every step after the warm-up "earlier" than the plan clock)

After:

Plan: 11 steps, ≈54:00 (incl. 17:00 planning estimate of 1 lap-press step) planned | Actual: 12 intervals, 58:40 in total | matched 11, …
[plan 1] warmup until lap press @ 110-175 W 'Press lap Einrollen bis Startpunkt' -> Press lap Einrollen bis Startpunkt (WORK) from 0:00, ended by lap after 16:01 (planning estimate 17:00), in range (110% of target)
compact:  [plan 1] warmup until lap press @ 110-175 W: ended by lap after 16:01 (planning estimate 17:00), 157 W, HR 126/143, 73% in target, in range (110% of target)
report:   - Plan: 11/11 steps executed; work steps 301/288/292/289/278 W vs 275-285 W planned (…); steps with clear deviations: 1; lap-press steps (open length, not judged): plan 1 warmup ended by lap after 16:01 (planning estimate 17:00)
intervals:  [1] Intervals.icu WORK | plan step 1 warmup until lap press, ended by lap after 16:01 (planning estimate 17:00)
JSON: rows[1..10].start_offset_s = 0.0 (rep 4 recovery 2:57 → last interval -3.0); rows[0].lap_press = {ended_by: lap, elapsed_s: 961, estimate_s: 1020, …}

The one remaining clear deviation is rep 1 at +5.6 % above the target range, which is correct.

Offline, same plan, warm-up ended by lap after 12:00 (compact):

before: [plan 1] warmup 17:00 @ 110-175 W: 12:00, … | 5:00 shorter than planned      (+ "started 5:00 earlier than the plan timeline" on every later step in standard)
after:  [plan 1] warmup until lap press @ 110-175 W: ended by lap after 12:00 (planning estimate 17:00), …   steps with clear deviations 0

Offline, warm-up ended by lap after 25:00:

before: [plan 1] warmup 17:00 @ 110-175 W: 17:00, …; +8:00 extra time inside the plan
after:  [plan 1] warmup until lap press @ 110-175 W: ended by lap after 25:00 (planning estimate 17:00), …

2. Health markers: note only while current (R39-10)

The note from a SICK/INJURED entry's description is shown only while the entry covers the tool's
day, or ended at most 2 days before it. Later in the 14-day window the line has only category,
name and dates (JSON note null). The note limit stays at 120 characters (NOTE_CHARS, confirmed
and tested). Live, read-only, get_recovery_snapshot(date_str=…) for the entry of 2024-12-22 to
2024-12-27:

2024-12-29 (ended 2 days before), before and after:
- SICK 2024-12-22 (Sun, 662 days ago) to 2024-12-27 (Fri, 657 days ago): "Infekt ~39,2°C Fieber ...", note "27.12. - Fieberfrei"
2025-01-03 (ended 7 days before), before:
- SICK 2024-12-22 (Sun, 662 days ago) to 2024-12-27 (Fri, 657 days ago): "Infekt ~39,2°C Fieber ...", note "27.12. - Fieberfrei"
2025-01-03, after:
- SICK 2024-12-22 (Sun, 662 days ago) to 2024-12-27 (Fri, 657 days ago): "Infekt ~39,2°C Fieber ..."

3. get_event_by_id: last day of multi-day events (R39-13)

This reuses event_day_span from #39. Live, read-only, SICK event 49921901:

before: End: 2024-12-28T00:00:00 (local)                         JSON: no last_day
after:  End: 2024-12-28T00:00:00 (local; last day 2024-12-27 (Fri))   JSON: "last_day": "2024-12-27"

Single-day events (e.g. the workout 144186380, End: 2026-10-14T00:00:00 (local)) are unchanged in
text and JSON, and so are events without an end. An end after midnight (22:00 → 02:00) names the
next day.

4. Float noise in text outputs (R39-14)

format_value (the "as stored" helper behind _stored/_val) now shows floats with 4 significant
digits instead of up to 6 decimals. Whole numbers are never rounded. Known metrics get their usual
precision through a new digits argument:

Output Precision
activity summary power/HR 0; intensity, decoupling, load, CTL/ATL, TRIMP, L/R and wind shares 1; VI, Power:HR, EF, stride 2; speeds 3
interval and group blocks load 1, torque 1, respiration 1, W/kg 2, variability 3
thresholds eFTP/CP/W'/Pmax 0
wellness CTL/ATL/ramp 1, eFTP 0, weight 2
recovery snapshot CTL/ATL 1; 7-day and baseline mean/median/SD 1

Also covered: training-summary form and ramp, get_event_by_id other fields, report device values.
A stored non-zero is never shown as 0; it keeps one significant digit, e.g. a gradient of 0.001984 % is
shown as 0.002%. JSON values are unchanged: a structural comparison of the live JSON outputs before
and after shows only the new lap-press keys and the corrected start_offset_s.

Live before → after (i196751426 and today's snapshot):

Interval 1:  Distance: 8150.6797 meters → 8150.7 meters | Training Load: 12.480418 → 12.5 | Torque: Avg 18.373035, Min 0.540526, Max 61.539913 → Avg 18.4, Min 0.5, Max 61.5
             Decoupling: -1.340999 → -1.3 | Respiration: 28.267124 → 28.3 breaths/min | Variability: 0.039962 → 0.04
Activity:    Intensity: 91.02564 → 91 | Power:HR Ratio: 1.2857143 → 1.29 | Variability Index: 1.1833333 → 1.18 | Fitness (CTL): 67.02364 → 67 | Efficiency Factor: 1.5214286 → 1.52
Custom:      Recovery Time [RecoveryTime]: 25.666667 h → 25.67 h | VO2 Max [VO2MaxGarmin]: 50.969925 → 50.97 | EPOC 165.7373 ml/kg → 165.7 ml/kg
Snapshot:    hrv: … 7d mean 39.714286 | 42d baseline mean 38.333333, median 40, sd 5.308377 → 7d mean 39.7 | 42d baseline mean 38.3, median 40, sd 5.3
             CTL 65.44669 | ATL 67.85514 → CTL 65.4 | ATL 67.9
Event:       Other fields: icu_atl=76.81097, ss_p_max=0.47685498 → icu_atl=76.81, ss_p_max=0.4769

Floats with 4 or more decimals across about 67 live text outputs (44 read tools): 727 before, 11
after. The remaining ones are intended:

  • values below 1 at 4 significant digits (Stride 0.9177, ss_p_max=0.4769, Elapsed Time 0.9775 hr);
  • the threshold pace in m/s, 4 decimals as in get_sport_settings;
  • a near-zero gradient kept non-zero (-0.00002%).

Review of this PR (R40) — second commit

  • R40-1 (first/last lap-press step took in riding before/after the workout):
    • The press closes exactly one lap. So a lap-press step is its closing lap, preceded only by full
      device auto-laps: laps of the auto-lap length, at most 0.5 % short of the setting (a press lap
      usually is shorter).
    • Without a detected layout, a usual auto-lap distance from 200 m to 20 km counts instead.
    • The step never reaches across a press, a step lap or the lap that starts the workout.
    • With lap-press steps, the riding before or after the plan costs its fixed part once per block.
      A long ride home in many auto-laps is therefore not moved into the plan.
    • The first step takes all full auto-laps back to the workout start. A lap well off its target at
      the plan edges counts as outside the plan.
    • Reviewer cases R4/R5 and a 60 min ride home with 1 km auto-laps at three speeds are now
      correct: "Additional training after the plan: 15:00 / 60:00", "Riding before the plan: 20:00".
  • R40-2 (press lap of about the auto-lap length made the DP skip a step):
    • Ending on a full auto-lap now costs 0.4, below GAP_COST.
    • It costs 1.0 only when a shorter lap that fits the target follows (the press came later).
    • R1 (975/991 m), R3 (986 m) and seeds 195/210 are matched with 0 deviations.
    • Such a row says "ended at a full device auto-lap (no shorter lap closed by a press; …)".
  • Lap trigger: Intervals.icu does not expose the lap trigger. The interval objects and the activity
    have no such field. The original FIT has lap_trigger (manual/time/session_end) and
    wkt_step_index (verified read-only for i196751426). Using them would need an extra file download
    per analysis and a FIT decoder, so this is left as a possible follow-up. The rule above uses the
    lap lengths instead.
  • R40-3: a long warm-up whose auto-laps are too few for a layout is matched as a whole.
  • R40-6 to R40-9:
    • A non-zero value is never shown as 0, also below 1e-6.
    • Form and ramp are never -0.0.
    • Snapshot baselines use 1 decimal only for the known metrics, 4 significant digits otherwise.
    • "Work: 632 kJ" replaces "Kilojoules: 631775", which was in J.
    • With a pause, the text names the elapsed and the moving time.
    • The plan total includes distance estimates.
    • The report says "length set by the lap press, not judged; targets judged".
    • JSON note_withheld is added.
  • R40-10: with very many laps, the pairwise merging never merges across a lap that may close a
    lap-press step.
  • R40-4/5/10: listed as known limitations in the execution guide (no press at all, double press in
    consecutive lap-press steps or at the plan edges, workout start exactly on an auto-lap at the
    warm-up's intensity, more than about 600 laps).

Measurements (offline, the reviewer's scripts):

Check Result
Reviewer's 400-workout lap-press fuzz (r40/lp_fuzz3.py) bad 5 (main 184); worse than main 2, better 181
Same fuzz, riding before/after at a 3.7 % different speed bad 4 (main 187); worse than main 0, better 185
Reviewer's 95 lap-press scenarios worse than main only the documented R40-4/R40-5 cases
Timed-only plans (r26 scripts, 2,307 analyze calls) JSON and text identical to main, 0 exceptions, same runtime

The 2 worse cases in the original fuzz (seeds 77 and 226) share one cause. The synthetic riding
before the workout covers an exact multiple of the auto-lap at the warm-up's intensity, so no lap
marks the workout start.

Live, read-only, i196751426: unchanged.

[plan 1] warmup until lap press @ 110-175 W: ended by lap after 16:01 (planning estimate 17:00), 157 W, HR 126/143, 73% in target, in range (110% of target)

The report finding now ends "…; lap-press steps (length set by the lap press, not judged; targets
judged): plan 1 warmup ended by lap after 16:01 (planning estimate 17:00)".

Checks

  • ruff check .: all checks passed.
  • mypy src tests: no issues in 130 files.
  • pytest: 1154 passed (main 1129), including the catalogue budgets.
  • pylint gate (--disable=C0301 --fail-under=0 --fail-on=E,F): exit 0, and no new messages compared
    with main.
  • Live checks were read-only. Every non-GET request was refused at the httpx transport (sync and
    async), and all requests were GET. Request counts per tool are unchanged.
  • Nothing was written to Intervals.icu. The production checkout and the services were not touched.

🤖 Generated with Claude Code

futureweb and others added 2 commits October 15, 2026 11:48
…ow, event last day, display rounding

- analyze_workout_execution / get_activity_report / get_activity_intervals plan matching: lap-press
  steps (until_lap_press) are open. "ended by lap after <actual> (planning estimate <estimate>)",
  no length deviation, no cap or split, target adherence kept, plan clock restarts at the actual
  lap. Alignment: soft estimate cost, a lap-press step ends on the lap closed by the press (not on
  a full device auto-lap), its end is never moved or extended, its estimate is no lap length for
  the auto-lap detection (R36-11).
- Health markers: the description note (max 120 characters) only while the entry covers the
  tool's day or ended at most 2 days before (R39-10).
- get_event_by_id: last day of multi-day events in text and JSON last_day; single-day unchanged
  (R39-13).
- Text outputs round stored floats for display (4 significant digits, known metrics with their
  usual decimals; a stored non-zero is never shown as 0); JSON unchanged (R39-14).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w R40 follow-ups

- R40-1: a lap-press step is its closing lap preceded only by full device auto-laps
  (auto_lap_evidence: layout length at most 0.5 % short of the setting, else a usual auto-lap
  distance); it never reaches across a press, a step lap or the workout start, so riding before
  the workout and after the last press stays outside the plan. With lap-press steps the edge
  blocks pay their fixed cost once, the first step takes all full auto-laps back to the workout
  start, a lap off target at the plan edges costs what it costs outside.
- R40-2: ending on a full auto-lap costs 0.4 (below GAP_COST), 1.0 only when a shorter lap that
  fits the target follows (the press came later); "ended at a full device auto-lap".
- R40-3: long warm-ups whose auto-laps are too few for a layout are matched as a whole.
- Coarsening never merges across a lap that may close a lap-press step.
- R40-6/7/8/9: non-zero never shown as 0 (also below 1e-6), form/ramp never -0.0, snapshot
  baselines 1 decimal only for known metrics, "Work: 872 kJ", elapsed + moving time with a pause,
  distance estimates in the plan total, report wording, JSON note_withheld.
- Known limitations (no press, double press, start on an exact auto-lap, >600 laps) in the guide.
- Plans without lap-press steps are analysed exactly as before (2,307 r26 analyses identical).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@futureweb
futureweb merged commit afc9981 into main Oct 15, 2026
10 checks passed
@futureweb
futureweb deleted the fix/review-followups-b2 branch October 15, 2026 11:30
@futureweb futureweb mentioned this pull request Oct 15, 2026
futureweb added a commit that referenced this pull request Oct 15, 2026
Prepares tag `v1.0.0b2` (RELEASING.md steps 2–3; owner approved the
release).

- Version `1.0.0b2` everywhere it is stored
(`scripts/check_release_metadata.py`: consistent); jq only re-laid out
the JSON files, the semantic diff is the version fields.
- README and docs/DOCKERHUB.md install examples and badge on `1.0.0b2`;
status line updated, release item removed from "Next".
- CHANGELOG: Unreleased → `[1.0.0b2] - 2026-10-15` with a short summary
and **Known limitations** of the lap-press execution analysis (from the
PR #40 re-check).
- Execution guide: the claim that devices whose auto-laps do not restart
at a press are handled is corrected — fixed auto-lap markers are a known
limitation; very short laps named as the cause of the many-laps
limitation.

Main changes since 1.0.0b1: lap-press steps (#36, confirmed on a Garmin
Edge 1040, #38), coaching answer rules, health markers, date anchors and
n/a fixes (#39), lap-press steps in the execution analysis, health note
window, event last day, display rounding (#40), serverInfo version
(#34), docs (#35, #37).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant