diff --git a/changelog.d/1779-digest-count-rollover.fixed.md b/changelog.d/1779-digest-count-rollover.fixed.md new file mode 100644 index 000000000..14059adf7 --- /dev/null +++ b/changelog.d/1779-digest-count-rollover.fixed.md @@ -0,0 +1 @@ +Fix the team run report printing `1000.0k` for a count between 999,950 and 999,999; it now carries into the next unit and reads `1.0M` ([#1779](https://github.com/ThinkFlowLab/FlyBot/issues/1779)). diff --git a/docs/subsystems/timeline.md b/docs/subsystems/timeline.md index b253eb594..161bd9599 100644 --- a/docs/subsystems/timeline.md +++ b/docs/subsystems/timeline.md @@ -28,7 +28,7 @@ Everything in `plugins/timeline/`: the recorder, the trace file, the read model | `plugins/timeline/src/store.js` | The trace: single writer, write-tmp + fsync + rename, capped segments. | | `plugins/timeline/src/window.js` | Pure read model: records to lanes, turns, tools, arrows, edges, utilization, the token spend per Bot (#357), per-Bot outcomes and recent failures (#471), the open requests waiting longest (#469) and the tools each Bot used (#359). | | `plugins/timeline/src/health.js` | Team health (#357): pure `buildHealth(window)` turning the window into per-Bot signals and flags. | -| `plugins/timeline/src/digest.js` | Team run report (#582): pure `buildDigest(window)` turning the window into short Markdown, with the count and duration formatting (`formatCount`, `formatMs`). A duration rounds once and carries into the next unit (#761), so the text says `1m` and `2h00m`, never `60s`, `60m` or `1h60m`. | +| `plugins/timeline/src/digest.js` | Team run report (#582): pure `buildDigest(window)` turning the window into short Markdown, with the count and duration formatting (`formatCount`, `formatMs`). A duration rounds once and carries into the next unit (#761), so the text says `1m` and `2h00m`, never `60s`, `60m` or `1h60m`. A count carries the same way (#1779): 999,950 reads `1.0M`, never `1000.0k`. | | `plugins/timeline/src/alerts.js` | Durable alerts (#584): pure `createAlertTracker()` folding a window across reads, so a condition opens one alert and resolves when it clears. | | `plugins/timeline/client.js` | Browser entry (`@flybot/timeline`): the 2 s poller exposed as `ctx.flybot.timeline`, the geometry functions (`layoutSwimlanes`, `layoutMap`, `waitRows`, `utilizationRows`) and the panel (swimlanes, who waits on whom, utilization, and **Copy report**), opened from its `flybot.sidebar.section` row, the only entry (not in the + menu, #269). | | `plugins/timeline/src/sanitize.js` | The cleaners for ids, labels, codes, times, counts. | diff --git a/plugins/timeline/src/digest.js b/plugins/timeline/src/digest.js index fa5ce2748..1478e2d52 100644 --- a/plugins/timeline/src/digest.js +++ b/plugins/timeline/src/digest.js @@ -14,7 +14,14 @@ const TOP_TOOLS = 5 export function formatCount(value) { const n = Number.isFinite(value) ? Math.max(0, Math.round(value)) : 0 if (n >= M) return `${(n / M).toFixed(1)}M` - if (n >= K) return `${(n / K).toFixed(1)}k` + if (n >= K) { + // Carry a count that rounds up into the next unit (#1779), the same carry #761 fixed for + // durations: picking the unit from the raw value and rounding inside it printed `1000.0k` for + // 999,950–999,999, a number the k unit cannot hold. + const tenths = Math.round(n / 100) + if (tenths >= 10_000) return `${(tenths / 10_000).toFixed(1)}M` + return `${(n / K).toFixed(1)}k` + } return String(n) } diff --git a/plugins/timeline/test/digest.test.js b/plugins/timeline/test/digest.test.js index 8e8910378..434a3b4fd 100644 --- a/plugins/timeline/test/digest.test.js +++ b/plugins/timeline/test/digest.test.js @@ -36,6 +36,17 @@ test('formatCount and formatMs read at a glance', () => { assert.equal(formatMs(undefined), '0s') }) +// The unit is picked from the raw value, so the rounding inside it can overflow it (#1779): a count +// one step below a million printed `1000.0k`, which is a number its own unit cannot hold and one a +// reader has to convert back before it means anything. The carry is the same one #761 fixed for +// durations. +test('#1779: a count that rounds up into the next unit prints that unit, not 1000.0k', () => { + assert.equal(formatCount(999_949), '999.9k') + assert.equal(formatCount(999_950), '1.0M') + assert.equal(formatCount(999_999), '1.0M') + assert.equal(formatCount(1_000_000), '1.0M') +}) + // The carry must never overflow the unit the text prints (#761): rounding the raw milliseconds // inside a unit chosen from the raw value printed `60s` for 59.6 s and `1h60m` for 1h59m50s, and a // person pasting the report read those numbers in a standup.