feat: real Runway — per-milestone Monte Carlo forecasts
The Runway milestone list is now real. Each open gitea milestone's open scope gets its own Monte Carlo forecast (reusing the P2 engine); the p80 landing range is shown, and compared to the milestone's due date (on track / at risk) when one exists. Ranges, never point dates. - backlog.ts: runwayView(issues, milestones, deps) → RunwayMilestone[] — per milestone: forecast its open scope, map p50..p90 to a date range, normalize the RunwayBar band across a shared horizon, tone/ note from due-vs-p80. Milestones with no open scope (shipped) are omitted; empty → the demo fixture. - RunwayScreen takes optional `milestones`; AppShell feeds runwayView. The header's calibration note was already real (#1). Scope: each milestone forecasts its remaining work *from today* independently — they aren't scheduled relative to each other yet (so a smaller later phase can show an earlier date). Cross-milestone sequencing is a refinement. Capacity stays fixture — true per-person capacity (focus factor, allocation) is #8, config-driven. Verified: desktop typecheck clean, 14 fixture e2e green. Live: Runway shows the real P1/P2/P4/P5 milestones with per-milestone forecasts (e.g. "P2 — Scheduler + Monte Carlo · 80% Aug 14–25 · 32d of work"); the fixture lists Beta/Pilot/v1.0, so the real names prove it (new assertion + screenshot). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,10 @@ test.describe('live backlog', () => {
|
||||
await expect(
|
||||
win.getByText(/cold-start priors · \d+\/20 closed issues estimated|calibrated on \d+ closed/),
|
||||
).toBeVisible()
|
||||
// Real per-milestone forecasts — these milestone names come from gitea, not the
|
||||
// fixture (which lists Beta / Pilot-ready / v1.0).
|
||||
await expect(win.getByText(/P2 — Scheduler/)).toBeVisible()
|
||||
await win.screenshot({ path: join(here, '.artifacts', 'screens', 'live-runway.png'), fullPage: true, animations: 'disabled' })
|
||||
await win.getByRole('button', { name: 'Full report' }).click()
|
||||
await expect(win.getByText(/cold-start · \d+\/20|curve active · n ≥ 20/)).toBeVisible()
|
||||
await win.screenshot({ path: join(here, '.artifacts', 'screens', 'live-calibration.png'), fullPage: true, animations: 'disabled' })
|
||||
|
||||
Reference in New Issue
Block a user