Skip to content

feat(tracker-stats): project the current bucket to end-of-bucket totals - #1158

Merged
potiuk merged 1 commit into
apache:mainfrom
potiuk:stats-current-bucket-projection
Sep 8, 2026
Merged

feat(tracker-stats): project the current bucket to end-of-bucket totals#1158
potiuk merged 1 commit into
apache:mainfrom
potiuk:stats-current-bucket-projection

Conversation

@potiuk

@potiuk potiuk commented Sep 8, 2026

Copy link
Copy Markdown
Member

The final bucket on every chart is cut short by "now", so regenerating the dashboard mid-month reads as a collapse in reports rather than a month that is only a quarter over. On the reference tracker today (8 September, 23 % elapsed) September shows 9 opened against August's 65.

Two kinds of series, two extrapolations

Kind Series Formula
rate — accumulates from zero inside the bucket opened / rejected / reported in bucket observed / elapsed
level — carries over from the previous bucket cumulative opened / closed / rejected / reported, every lifecycle band, the untriaged backlog prev + (observed - prev) / elapsed

Scaling a level series whole would multiply years of accumulated history by four, so only the movement inside the bucket is extrapolated. A falling level (a backlog being worked down) projects further down, floored at zero; a rate projection is never below what already happened.

Where it lands

Every chart that carries a projectable series: the lifecycle bands, opened-vs-untriaged, the cumulative lines, and the rejections chart. Each projection is a dotted two-point segment from the last complete bucket to the projected bucket end, so a forecast cannot be mistaken for a measurement. On the stacked lifecycle chart each band is drawn at its projected position in the stack, with the band's own projected count in the hover and legend entries suppressed. Because the bands partition every tracker, their projections sum to the projected cumulative opened — a free consistency check.

The mean-time charts are deliberately not projected. A mean over the items seen so far is already an estimate of the bucket's mean, not a partial accumulation; scaling it by elapsed time would be meaningless.

Config

projection:
  enabled: true
  min_elapsed_fraction: 0.1

min_elapsed_fraction suppresses the projection early in a bucket, where one report extrapolates to a dozen. The projection is also skipped on a single-bucket axis (no baseline for the level series) and when the bucket is already complete; stdout always says which.

Reference output

Current-bucket projection (2026-09, 23% elapsed, now -> month-end):
  opened                      9 -> 38
  reported                   12 -> 51
  cum_opened                355 -> 384
  band:open_untriaged        14 -> 7
  band:open_triaged          16 -> 39

Testing

  • 25 new unit tests over the three new pure helpers (bucket_bounds, elapsed_fraction, project_bucket_total / project_bucket_level) — bounds in all three bucket modes, clamping, rounding, the never-below-observed floor, the zero-floor on falling levels.
  • Full prek run green (ruff, mypy, pytest, markdownlint, doctoc); lychee offline clean.
  • Rendered against a live 355-tracker cache in monthly, quarterly and weekly modes, and all four skip paths exercised (disabled, below threshold, single-bucket axis, complete bucket).
  • Emitted JS executed under Node against a stubbed Plotly to confirm all 13 projection traces parse and land on the right charts.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WJopDiCvbwSG6t4k15JE4H

The final bucket on every chart is cut short by "now", so regenerating
the dashboard mid-month reads as a collapse in reports rather than a
month that is only a quarter over.

Project it. Two kinds of series need two different extrapolations:

- rate series (opened / rejected / reported in bucket) accumulate from
  zero inside the bucket, so the whole count scales:
  observed / elapsed_fraction.
- level series (cumulative totals, the lifecycle bands, the untriaged
  backlog) carry over from the previous bucket, so only the movement
  inside the bucket scales: prev + (observed - prev) / elapsed. Scaling
  the level itself would multiply years of accumulated history by four.

Projections land on every chart that carries a projectable series: the
lifecycle bands (each drawn at its projected position in the stack,
carrying the band's own projected count in the hover), the
opened-vs-untriaged chart, the cumulative lines, and the rejections
chart. Each is a dotted two-point segment from the last complete
bucket, so a forecast cannot be mistaken for a measurement. Because the
bands partition every tracker, their projections sum to the projected
cumulative opened.

The mean-time charts are deliberately not projected: a mean over the
items seen so far is already an estimate of the bucket's mean, not a
partial accumulation, so scaling it by elapsed time would be
meaningless.

`projection.enabled` switches the stat off; `min_elapsed_fraction`
(default 0.1) suppresses it early in a bucket, where a single report
extrapolates to a dozen. A single-bucket axis is skipped as well — the
level series have no baseline to project from.

Generated-by: Claude Code (Opus 5)
@potiuk
potiuk merged commit cf05bd4 into apache:main Sep 8, 2026
10 checks passed
@potiuk
potiuk deleted the stats-current-bucket-projection branch September 8, 2026 01:35
potiuk added a commit that referenced this pull request Sep 8, 2026
The marker sat at a1cff44, 17 commits behind main. Bumping it alone
would claim those commits are described by the specs, so the drift is
closed first.

Specs updated for what actually shipped:

- meta-and-quality-tooling: skill-evals errors, rather than passing, when
  a case's CLI produced no gradeable output (#1161).
- security-reporting: the tracker dashboard projects the current partial
  bucket to its end-of-bucket value, splitting RATE series (accumulate
  from zero) from LEVEL series (carry over), and deliberately not
  projecting mean-based signals (#1158).
- project-agnosticism: <PROJECT> and <project> are two placeholders
  holding different values, and the lint carries both spellings plus
  spaced variants (#1154).
- adapters: the forwarder relay's contact_handle defaults to an
  org-level shared inbox rather than a named individual (#1135). The
  multi-hop coordinator case is designed in RFC-AI-0008 and unimplemented.
- issue-management-family: the family's eval suites, and the note that
  --cli runs belong outside a credential-denying sandbox (#1145).

Commits needing no spec change: #1152, #1143 and #1156 updated their own
specs in-commit; #1149, #1147, #1151 are behaviour-preserving bug fixes;
#1155 and #1141 are CI and dependency chores; #1159's spec edits landed
with it; #1144 removes hardcoded literals that no spec asserted.

One genuine gap recorded rather than papered over: no spec covers
marketplace distribution or the dev-version stamping rule from #1160,
which is load-bearing because `claude plugin update` compares version
strings, not commit SHAs. Logged in adoption-and-setup as wanting its own
spec.

Generated-by: Claude Code (Opus 5)
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