From 3873e652f2493141bebff51b38651c3e8c721e9b Mon Sep 17 00:00:00 2001 From: Croissant Le Doux Date: Thu, 9 Jul 2026 11:23:01 -0400 Subject: [PATCH] Gantt bars use the capacity-aware lane layout (match the forecast) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ganttView ran schedule() — the single-serial-worker layout — while the Monte Carlo forecast ran scheduleWithCapacity() over the real lanes. So the bars you saw didn't match what was forecast (serial 1.0/day vs the team's actual lanes). Switch ganttView to scheduleWithCapacity(open, deps, workers): startDay/endDay now come from the same lane layout each forecast trial uses, and `who` shows the lane an issue actually landed on (falling back to assignee, then blank). The dogfood harness had the same split — updated it to match and to print the lane per row. Verified: typecheck clean, 14 fixture e2e green, live-onboarding e2e renders the real capacity-aware Gantt with no uncaught errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/scripts/dogfood-report.ts | 7 ++++--- .../src/renderer/src/lib/views/gantt-view.ts | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/desktop/scripts/dogfood-report.ts b/apps/desktop/scripts/dogfood-report.ts index 30a017d..466b0b2 100644 --- a/apps/desktop/scripts/dogfood-report.ts +++ b/apps/desktop/scripts/dogfood-report.ts @@ -16,7 +16,7 @@ import { inferLifecycle, parseCapacityConfig, capacityPerWorkday, - schedule, + scheduleWithCapacity, type GiteaIssue, type DependencyEdge, type Worker, @@ -94,7 +94,7 @@ async function main() { assignee: i.assignee, })) - const plan = schedule(toSchedulable(open), edges) + const plan = scheduleWithCapacity(toSchedulable(open), edges, workers) const today = new Date() const f = forecast(toSchedulable(open), edges, { workers }) @@ -108,9 +108,10 @@ async function main() { console.log(`\n Schedule (dependency + priority order, ${workers.length || 1} lane${workers.length === 1 ? '' : 's'}):`) for (const it of plan.items) { const crit = it.critical ? ' ★crit' : '' + const lane = it.worker ? ` [${it.worker}]` : '' const blocks = it.blocks.length ? ` blocks ${it.blocks.map((b) => '#' + b).join(',')}` : '' console.log( - ` #${String(it.number).padEnd(3)} ${fmt(addWorkingDays(today, it.startDay))}→${fmt(addWorkingDays(today, it.endDay))}${crit.padEnd(7)} ${it.title.slice(0, 40)}${blocks}`, + ` #${String(it.number).padEnd(3)} ${fmt(addWorkingDays(today, it.startDay))}→${fmt(addWorkingDays(today, it.endDay))}${crit.padEnd(7)}${lane} ${it.title.slice(0, 38)}${blocks}`, ) } if (plan.cycle) console.log(` ⚠ dependency cycle: ${plan.cycle.join(' → ')}`) diff --git a/apps/desktop/src/renderer/src/lib/views/gantt-view.ts b/apps/desktop/src/renderer/src/lib/views/gantt-view.ts index 0a6e5f6..2a4707a 100644 --- a/apps/desktop/src/renderer/src/lib/views/gantt-view.ts +++ b/apps/desktop/src/renderer/src/lib/views/gantt-view.ts @@ -1,4 +1,4 @@ -import { forecast, inferLifecycle, schedule, workingDaysBetween } from '@commitea/core' +import { forecast, inferLifecycle, scheduleWithCapacity, workingDaysBetween } from '@commitea/core' import type { GanttData, GanttRow, GanttWeek } from '../../data/fixtures.js' import { addWorkingDays, formatShort } from '../dates.js' @@ -26,14 +26,15 @@ function toSchedulable(issues: ProjectData['issues']) { const roundUpToWeek = (n: number): number => Math.ceil(Math.max(n, 1) / 7) * 7 /** - * Real Gantt: the deterministic scheduler's serial layout over the open - * backlog (startDay/endDay/critical), plus a handful of recently-closed - * issues shown as already-done bars. No per-issue Monte Carlo exists at this - * grain, so `p80` is a rough single-issue buffer (endDay padded by half its - * own duration) rather than a simulated percentile — flagged below. + * Real Gantt: the capacity-aware scheduler's lane layout over the open backlog + * (startDay/endDay/critical/worker) — the SAME layout the Monte Carlo forecast + * runs each trial on, so the bars you see match what's forecast. Plus a handful + * of recently-closed issues shown as already-done bars. No per-issue Monte Carlo + * exists at this grain, so `p80` is a rough single-issue buffer (endDay padded by + * half its own duration) rather than a simulated percentile — flagged below. */ export function ganttView(d: ProjectData): GanttData { - const plan = schedule(toSchedulable(d.issues), d.deps) + const plan = scheduleWithCapacity(toSchedulable(d.issues), d.deps, d.workers) const issueByNumber = new Map(d.issues.map((i) => [i.number, i])) const scheduledRows: GanttRow[] = plan.items.map((item) => { @@ -44,7 +45,9 @@ export function ganttView(d: ProjectData): GanttData { return { id: item.number, title: item.title, - who: initials(issue?.assignee ?? ''), + // The lane it actually landed on (capacity-aware) — falls back to the + // assignee, then blank, so an unassigned issue shows the lane doing it. + who: initials(item.worker ?? issue?.assignee ?? ''), start: item.startDay, end: item.endDay, crit: item.critical,