Compare commits
4 Commits
19897f7e53
...
feat/kill-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3873e652f2 | ||
|
|
63f0ea1735 | ||
| 87a0ba4ba1 | |||
| 62a521e7eb |
@@ -34,6 +34,7 @@
|
||||
"electron-vite": "^3.1.0",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tsx": "^4",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.1.0"
|
||||
}
|
||||
|
||||
142
apps/desktop/scripts/dogfood-report.ts
Normal file
142
apps/desktop/scripts/dogfood-report.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 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,
|
||||
scheduleWithCapacity,
|
||||
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 = scheduleWithCapacity(toSchedulable(open), edges, workers)
|
||||
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 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)}${lane} ${it.title.slice(0, 38)}${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)
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
18
yarn.lock
18
yarn.lock
@@ -261,6 +261,7 @@ __metadata:
|
||||
react: "npm:^18.3.1"
|
||||
react-dom: "npm:^18.3.1"
|
||||
tailwindcss: "npm:^3.4.17"
|
||||
tsx: "npm:^4"
|
||||
typescript: "npm:^5.7.3"
|
||||
vite: "npm:^6.1.0"
|
||||
languageName: unknown
|
||||
@@ -2687,7 +2688,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"esbuild@npm:^0.27.0 || ^0.28.0":
|
||||
"esbuild@npm:^0.27.0 || ^0.28.0, esbuild@npm:~0.28.0":
|
||||
version: 0.28.1
|
||||
resolution: "esbuild@npm:0.28.1"
|
||||
dependencies:
|
||||
@@ -5330,6 +5331,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tsx@npm:^4":
|
||||
version: 4.23.0
|
||||
resolution: "tsx@npm:4.23.0"
|
||||
dependencies:
|
||||
esbuild: "npm:~0.28.0"
|
||||
fsevents: "npm:~2.3.3"
|
||||
dependenciesMeta:
|
||||
fsevents:
|
||||
optional: true
|
||||
bin:
|
||||
tsx: dist/cli.mjs
|
||||
checksum: 10c0/e4fade6bf8a4447424652da3a68f5ab7b927d1cbe5f697dba876c626c5fb7bf7663c4f71777992c9cbbdc8148c63ee1ddcf15536ff9d1863f9fbea25247ee0a9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-fest@npm:^0.13.1":
|
||||
version: 0.13.1
|
||||
resolution: "type-fest@npm:0.13.1"
|
||||
|
||||
Reference in New Issue
Block a user