Gantt bars use the capacity-aware lane layout (match the forecast)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
|||||||
inferLifecycle,
|
inferLifecycle,
|
||||||
parseCapacityConfig,
|
parseCapacityConfig,
|
||||||
capacityPerWorkday,
|
capacityPerWorkday,
|
||||||
schedule,
|
scheduleWithCapacity,
|
||||||
type GiteaIssue,
|
type GiteaIssue,
|
||||||
type DependencyEdge,
|
type DependencyEdge,
|
||||||
type Worker,
|
type Worker,
|
||||||
@@ -94,7 +94,7 @@ async function main() {
|
|||||||
assignee: i.assignee,
|
assignee: i.assignee,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const plan = schedule(toSchedulable(open), edges)
|
const plan = scheduleWithCapacity(toSchedulable(open), edges, workers)
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
const f = forecast(toSchedulable(open), edges, { workers })
|
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'}):`)
|
console.log(`\n Schedule (dependency + priority order, ${workers.length || 1} lane${workers.length === 1 ? '' : 's'}):`)
|
||||||
for (const it of plan.items) {
|
for (const it of plan.items) {
|
||||||
const crit = it.critical ? ' ★crit' : ''
|
const crit = it.critical ? ' ★crit' : ''
|
||||||
|
const lane = it.worker ? ` [${it.worker}]` : ''
|
||||||
const blocks = it.blocks.length ? ` blocks ${it.blocks.map((b) => '#' + b).join(',')}` : ''
|
const blocks = it.blocks.length ? ` blocks ${it.blocks.map((b) => '#' + b).join(',')}` : ''
|
||||||
console.log(
|
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(' → ')}`)
|
if (plan.cycle) console.log(` ⚠ dependency cycle: ${plan.cycle.join(' → ')}`)
|
||||||
|
|||||||
@@ -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 type { GanttData, GanttRow, GanttWeek } from '../../data/fixtures.js'
|
||||||
import { addWorkingDays, formatShort } from '../dates.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
|
const roundUpToWeek = (n: number): number => Math.ceil(Math.max(n, 1) / 7) * 7
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Real Gantt: the deterministic scheduler's serial layout over the open
|
* Real Gantt: the capacity-aware scheduler's lane layout over the open backlog
|
||||||
* backlog (startDay/endDay/critical), plus a handful of recently-closed
|
* (startDay/endDay/critical/worker) — the SAME layout the Monte Carlo forecast
|
||||||
* issues shown as already-done bars. No per-issue Monte Carlo exists at this
|
* runs each trial on, so the bars you see match what's forecast. Plus a handful
|
||||||
* grain, so `p80` is a rough single-issue buffer (endDay padded by half its
|
* of recently-closed issues shown as already-done bars. No per-issue Monte Carlo
|
||||||
* own duration) rather than a simulated percentile — flagged below.
|
* 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 {
|
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 issueByNumber = new Map(d.issues.map((i) => [i.number, i]))
|
||||||
|
|
||||||
const scheduledRows: GanttRow[] = plan.items.map((item) => {
|
const scheduledRows: GanttRow[] = plan.items.map((item) => {
|
||||||
@@ -44,7 +45,9 @@ export function ganttView(d: ProjectData): GanttData {
|
|||||||
return {
|
return {
|
||||||
id: item.number,
|
id: item.number,
|
||||||
title: item.title,
|
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,
|
start: item.startDay,
|
||||||
end: item.endDay,
|
end: item.endDay,
|
||||||
crit: item.critical,
|
crit: item.critical,
|
||||||
|
|||||||
Reference in New Issue
Block a user