Productionize: real data everywhere (Phase B) + shareable unsigned macOS .dmg (Phase C) #52

Merged
christian merged 5 commits from feat/kill-fixtures into main 2026-07-09 19:23:39 +00:00
20 changed files with 3473 additions and 42 deletions
Showing only changes of commit 3873e652f2 - Show all commits

View File

@@ -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(' → ')}`)

View File

@@ -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,