@@ -0,0 +1,141 @@
/**
* 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 )
} )