/** * Dogfood (#31): run CommiTea's own deterministic engine against the live * christian/commitea backlog and print the project report each screen derives — * board, critical path, schedule, Monte Carlo forecast, per-milestone runway. * * This is the core brain (schedule + forecast + capacity), the exact code the * desktop app runs. Read-only. Run: yarn tsx scripts/dogfood-report.ts */ import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { createGiteaClient, forecast, inferLifecycle, parseCapacityConfig, capacityPerWorkday, schedule, type GiteaIssue, type DependencyEdge, type Worker, } from '@commitea/core' const here = dirname(fileURLToPath(import.meta.url)) function token(): string { let dir = here for (let i = 0; i < 6; i++) { try { const m = /^GITEA_TOKEN\s*=\s*(.+?)\s*$/m.exec(readFileSync(join(dir, '.env.local'), 'utf8')) if (m) return m[1].trim() } catch { /* keep walking up */ } dir = dirname(dir) } throw new Error('no GITEA_TOKEN in any .env.local up the tree') } /** working-day offset → calendar date (skip Sat/Sun), then a short label. */ function addWorkingDays(base: Date, n: number): Date { const d = new Date(base) let left = Math.ceil(n) while (left > 0) { d.setDate(d.getDate() + 1) const day = d.getDay() if (day !== 0 && day !== 6) left-- } return d } const fmt = (d: Date) => d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) async function main() { const client = createGiteaClient( { baseUrl: 'https://gitea.stephenmann.io', owner: 'christian', repo: 'commitea', token: token() }, fetch, ) const all = await client.listIssues({ state: 'all' }) const open = all.filter((i) => i.state === 'open') const closed = all.filter((i) => i.state === 'closed') // Real dependency edges (open issues only — the scheduler drops out-of-scope endpoints anyway). const edges: DependencyEdge[] = [] for (const i of open) { for (const dep of await client.getIssueDependencies(i.number)) edges.push({ issue: i.number, dependsOn: dep }) } // Capacity lanes from the pm-state repo (a separate repo, exactly like the app's // pmStateClient — NOT the managed repo). const pmClient = createGiteaClient( { baseUrl: 'https://gitea.stephenmann.io', owner: 'christian', repo: 'commitea-pm-state', token: token() }, fetch, ) let workers: Worker[] = [] try { const f = await pmClient.getFile('capacity/members.json') if (f) { const members = parseCapacityConfig(JSON.parse(Buffer.from(f.contentBase64, 'base64').toString('utf8'))) workers = members.map((m) => ({ person: m.person, speed: capacityPerWorkday(m) })) } } catch { /* no capacity → single serial worker */ } const toSchedulable = (issues: GiteaIssue[]) => issues.map((i) => ({ number: i.number, title: i.title, labels: i.labels, estimateDays: i.facts.estimateDays, priority: i.facts.priority, assignee: i.assignee, })) const plan = schedule(toSchedulable(open), edges) const today = new Date() const f = forecast(toSchedulable(open), edges, { workers }) const bar = '─'.repeat(64) console.log(`\n${bar}\n CommiTea, on itself — ${fmt(today)} ${today.getFullYear()}\n${bar}`) console.log(` Board: ${open.length} open · ${closed.length} done`) console.log(` Team: ${workers.length ? workers.map((w) => `${w.person} (${w.speed.toFixed(2)}/day)`).join(', ') : 'single serial worker'}`) const unassigned = open.filter((i) => !i.assignee).length if (unassigned) console.log(` ⚠ ${unassigned}/${open.length} open issues have no assignee`) 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 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}`, ) } if (plan.cycle) console.log(` ⚠ dependency cycle: ${plan.cycle.join(' → ')}`) const p90 = f.curve.length ? f.curve[f.curve.length - 1].p90Day : f.p95Day console.log(`\n Forecast (Monte Carlo, ${f.trials} trials, ${f.coldStart ? 'cold-start priors' : 'fitted'}):`) console.log(` whole backlog lands p50 ${fmt(addWorkingDays(today, f.p50Day))} · p80 ${fmt(addWorkingDays(today, f.p80Day))} · p90 ${fmt(addWorkingDays(today, p90))}`) // Per-milestone runway. const ms = await client.listMilestones() console.log(`\n Runway (per open milestone):`) for (const m of ms.filter((x) => x.state === 'open')) { const scope = open.filter((i) => i.milestone?.id === m.id) if (!scope.length) continue const mf = forecast(toSchedulable(scope), edges, { workers }) const mp90 = mf.curve.length ? mf.curve[mf.curve.length - 1].p90Day : mf.p95Day const due = m.dueOn ? fmt(new Date(m.dueOn)) : 'no due date' console.log( ` ${m.title.padEnd(28)} ${scope.length} open · p50 ${fmt(addWorkingDays(today, mf.p50Day))}–p90 ${fmt(addWorkingDays(today, mp90))} (due: ${due})`, ) } console.log(`\n${bar}\n`) } main().catch((e) => { console.error(e) process.exit(1) })