#38 (Monte Carlo) and #39 (lifecycle) showed merged, but they merged into their stacked base branches, not main — only #37 (scheduler) actually reached main. This branch is cut from main and cherry-picks both back on, plus the new calibration work, so main becomes whole in one merge. Base is main (no stacking this time).
The forecast now learns from the team's own estimate-vs-actual history (the working time #5 infers from git events) instead of guessing forever.
Core (calibration-v0)
fitCalibration(samples) — lognormal fit on log(actual/estimate): global + per-bucket (once a bucket clears the floor) + per-person bias. coldStart until n ≥ 20 closed-with-estimate issues.
calibrationSamples() — pull samples from the closed backlog via lifecycle inference.
forecast() gains options.model: past cold-start, the fitted params drive the sim (per bucket, global fallback); otherwise the code priors do. Forecast.coldStart reflects the model.
App — AppShell fits calibration once from the reconciled backlog, feeds the model into the cone, and drives the Calibration screen + Runway header. Focus footer / Runway note / Calibration screen now show cold-start (N/20) vs calibrated (on N closed) from real data; scatter, bucket bias, and per-person all fitted, degrading honestly on a thin dataset.
Live result
The repo is honestly cold-start · 0/20: the dogfood issues were opened and closed the same day, so day-granular actuals are 0 and excluded. The flip-to-calibrated is proven by unit tests, not fabricated data.
Known refinements
Same-day closes → 0 working-day actuals (day-granular) are excluded; a fractional/hours-based actual would let fast repos accumulate history.
Per-person uses gitea login until a display-name map lands.
Re-landed in this PR
#10 Monte Carlo — real seeded burn-up cone (cold-start lognormal priors, deterministic-order sim).
#5 lifecycle inference — five-column board from the event stream + actualWorkingDays (which this PR now consumes).
Verified
74 core tests green (9 calibration + 2 forecast-switch new) · desktop typecheck clean · 14 fixture e2e green · live spec asserts the real cold-start calibration surface.
## ⚠️ Also fixes a stacked-merge gap
#38 (Monte Carlo) and #39 (lifecycle) showed `merged`, but they merged into their **stacked base branches**, not `main` — only #37 (scheduler) actually reached `main`. This branch is cut from `main` and cherry-picks both back on, **plus** the new calibration work, so `main` becomes whole in one merge. Base is `main` (no stacking this time).
---
## #1 — Calibration closes the D3 loop
The forecast now learns from the team's **own** estimate-vs-actual history (the working time #5 infers from git events) instead of guessing forever.
**Core (`calibration-v0`)**
- `fitCalibration(samples)` — lognormal fit on log(actual/estimate): global + per-bucket (once a bucket clears the floor) + per-person bias. `coldStart` until **n ≥ 20** closed-with-estimate issues.
- `calibrationSamples()` — pull samples from the closed backlog via lifecycle inference.
- `forecast()` gains `options.model`: past cold-start, the **fitted params drive the sim** (per bucket, global fallback); otherwise the code priors do. `Forecast.coldStart` reflects the model.
**App** — AppShell fits calibration once from the reconciled backlog, feeds the model into the cone, and drives the Calibration screen + Runway header. Focus footer / Runway note / Calibration screen now show *cold-start (N/20)* vs *calibrated (on N closed)* from real data; scatter, bucket bias, and per-person all fitted, degrading honestly on a thin dataset.
## Live result
The repo is honestly **cold-start · 0/20**: the dogfood issues were opened *and closed the same day*, so day-granular actuals are 0 and excluded. The flip-to-calibrated is proven by unit tests, not fabricated data.
## Known refinements
- Same-day closes → 0 working-day actuals (day-granular) are excluded; a fractional/hours-based actual would let fast repos accumulate history.
- Per-person uses gitea login until a display-name map lands.
## Re-landed in this PR
- **#10 Monte Carlo** — real seeded burn-up cone (cold-start lognormal priors, deterministic-order sim).
- **#5 lifecycle inference** — five-column board from the event stream + `actualWorkingDays` (which this PR now consumes).
## Verified
74 core tests green (9 calibration + 2 forecast-switch new) · desktop typecheck clean · 14 fixture e2e green · live spec asserts the real cold-start calibration surface.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
christian
added 3 commits 2026-07-08 23:52:15 +00:00
Replace the demo cone on Morning service with a real, seeded Monte Carlo
forecast over the open backlog. The LLM never does this — it's plain,
reproducible code (evidence-based scheduling).
core (@commitea/core/forecast-v0):
- Code-resident lognormal cold-start priors per estimate bucket (D3):
sampled actual = estimate * exp(N(mu, sigma)), mu > 0 (actuals run long),
sigma shrinks as tickets grow. Replaced by the team's empirical fit at
n >= 20 (#5 supplies the actuals).
- forecast(): seeded mulberry32 + Box-Muller over the scheduler's
deterministic order (order is fixed from estimates/deps; only durations
vary, so the cone stretches, never reorders). Returns p50/p80/p95 landing
+ a per-issue burn-up curve (p10/p50/p90). 12 unit tests; reproducible.
renderer:
- lib/dates.ts: working-day -> calendar mapper (skips weekends) + buildBurnUpData.
- BurnUpCone gains a data-driven twin; falls back byte-identical to the
fixture cone when no forecast (demo mode unchanged).
- Focus card shows the real "80% of the open backlog lands by <range>",
real scope count, and names the cold-start priors.
v0 scope (each a later slice): single serial worker (capacity is #8);
cold-start priors only (empirical fit is #5); no historical actual polyline
(needs lifecycle events, #5). Header chrome (reconcile time, ahead/behind
badge) stays fixture until milestone due dates land.
Verified: 51 core tests green, desktop typecheck clean, 14 fixture e2e green,
live spec asserts the real cone renders (25 open issues, "lands by Nov 11-27").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fill the board's Steeping / In-review columns (and the calibration actuals)
from real gitea timeline events, replacing the three-column-only v0.
core (@commitea/core):
- inferLifecycle(issue, events, asOf): five-column inference — closed → done;
open PR ref → review; commit ref → steeping; any triage signal → triage;
else diagnosis. Earliest event of each kind fixes the stage timestamp.
- Derives actualWorkingDays (work-start → close) — the estimate-vs-actual the
calibration fit (D3) learns from — and steepingDays (first commit → now) for
the board age badge.
- workingDaysBetween(): whole Mon–Fri days in [start, end), day-granular.
- normalizeTimeline() + client.getIssueTimeline(): map gitea's raw timeline
(label/milestone → triage, commit_ref → commit, pull_ref → pull, close,
reopen), drop the rest. Paginated.
app:
- reconcile now fetches every issue's timeline and returns it keyed by number;
threaded through the bridge → useBacklog → board/focus.
- issuesToBoardColumns + scheduleFocus run inferLifecycle: real Steeping/In-review
columns, steeping-age `days` badge, focus-card steeping badge.
Known refinement: gitea's pull_ref fires on any PR mention, so an issue merely
referenced in a PR body can read as In-review; distinguishing closing refs from
mentions needs the PR link's state (later). Re-opening multi-segment actuals
also deferred.
Verified: 63 core tests green (15 lifecycle, incl. workingDaysBetween + the five
transitions), desktop typecheck clean, 14 fixture e2e green, live spec asserts
the board's Done column is populated from real events.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Close the D3 loop. The forecast now learns from the team's own estimate-vs-actual
history (the working time #5 infers from git events) instead of guessing forever.
core (@commitea/core/calibration-v0):
- fitCalibration(samples): lognormal fit on log(actual/estimate) — global +
per-bucket (once a bucket clears the floor) + per-person bias. coldStart until
n >= 20 closed-with-estimate issues.
- calibrationSamples(): pull those samples from the closed backlog via lifecycle
inference (estimate label vs inferred actualWorkingDays).
- toDurationModel(): project the fit to the params forecast consumes.
- forecast() gains options.model: when past cold-start, fitted params drive the
sim (per bucket, global fallback); otherwise the code priors do. Forecast.coldStart
now reflects the model. nearestBucket extracted + exported.
app:
- AppShell fits calibration once from the reconciled backlog, feeds the model into
forecastBacklog (cone), and drives the Calibration screen + Runway header.
- Focus cone footer, Runway note, and Calibration screen now say cold-start (N/20)
vs calibrated (on N closed) from real data; Calibration scatter / bucket bias /
per-person all fitted, degrading honestly on a thin dataset.
Known refinement: same-day closes yield 0 working-day actuals (day-granular) and
are excluded, so a fast-moving repo can sit at n=0 — honest, but a fractional
(hours-based) actual would let those count. Per-person uses gitea login, not
display name, until the person map lands.
Note: also re-lands #10 (Monte Carlo) and #5 (lifecycle) which merged into their
stacked base branches but never propagated to main (stacked-merge trap); this
branch is cut from main and carries all three so main is whole again.
Verified: 74 core tests green (9 calibration + 2 forecast-switch added), desktop
typecheck clean, 14 fixture e2e green, live spec asserts the real cold-start
calibration surface (Runway note + screen badge fitted from actuals).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
⚠️ Also fixes a stacked-merge gap
#38 (Monte Carlo) and #39 (lifecycle) showed
merged, but they merged into their stacked base branches, notmain— only #37 (scheduler) actually reachedmain. This branch is cut frommainand cherry-picks both back on, plus the new calibration work, somainbecomes whole in one merge. Base ismain(no stacking this time).#1 — Calibration closes the D3 loop
The forecast now learns from the team's own estimate-vs-actual history (the working time #5 infers from git events) instead of guessing forever.
Core (
calibration-v0)fitCalibration(samples)— lognormal fit on log(actual/estimate): global + per-bucket (once a bucket clears the floor) + per-person bias.coldStartuntil n ≥ 20 closed-with-estimate issues.calibrationSamples()— pull samples from the closed backlog via lifecycle inference.forecast()gainsoptions.model: past cold-start, the fitted params drive the sim (per bucket, global fallback); otherwise the code priors do.Forecast.coldStartreflects the model.App — AppShell fits calibration once from the reconciled backlog, feeds the model into the cone, and drives the Calibration screen + Runway header. Focus footer / Runway note / Calibration screen now show cold-start (N/20) vs calibrated (on N closed) from real data; scatter, bucket bias, and per-person all fitted, degrading honestly on a thin dataset.
Live result
The repo is honestly cold-start · 0/20: the dogfood issues were opened and closed the same day, so day-granular actuals are 0 and excluded. The flip-to-calibrated is proven by unit tests, not fabricated data.
Known refinements
Re-landed in this PR
actualWorkingDays(which this PR now consumes).Verified
74 core tests green (9 calibration + 2 forecast-switch new) · desktop typecheck clean · 14 fixture e2e green · live spec asserts the real cold-start calibration surface.
🤖 Generated with Claude Code