/** * `query_project` — Reginald's single read tool. It selects a compact, * model-friendly view over the reconciled backlog. Every number comes from * deterministic code (scheduler, lifecycle inference, calibration); the model * only requests a shape and narrates it — it never computes (decisions.md). * * v0 serves focus / board / calibration / issue. The remaining views * (milestone / runway / standup / search) return a `notImplemented` marker so * the model degrades honestly instead of inventing data. */ import { fitCalibration, calibrationSamples } from '../calibration/calibration-v0.js' import type { GiteaIssue } from '../gitea/types.js' import { inferLifecycle, type LifecycleColumn, type LifecycleEvent } from '../lifecycle/lifecycle-v0.js' import { type DependencyEdge, schedule, selectFocus } from '../scheduler/scheduler-v0.js' export type ProjectView = | 'focus' | 'board' | 'calibration' | 'issue' | 'milestone' | 'runway' | 'standup' | 'search' export interface QueryFilters { issueId?: number query?: string limit?: number } /** The reconciled inputs a view is built from. */ export interface ProjectSnapshot { issues: GiteaIssue[] timelines: Record deps: DependencyEdge[] } const COLUMN_ORDER: LifecycleColumn[] = ['diagnosis', 'triage', 'steeping', 'review', 'done'] function toSchedulable(issues: GiteaIssue[]) { return issues .filter((i) => i.state === 'open') .map((i) => ({ number: i.number, title: i.title, labels: i.labels, estimateDays: i.facts.estimateDays, priority: i.facts.priority, })) } function focusView(snap: ProjectSnapshot) { const plan = schedule(toSchedulable(snap.issues), snap.deps) const f = selectFocus(plan) const slot = (item: (typeof plan.items)[number] | null) => item ? { issue: item.number, title: item.title, rationale: item.rationale } : null return { now: slot(f.now), next: slot(f.next), later: slot(f.later), openCount: plan.items.length } } function boardView(snap: ProjectSnapshot, asOf: Date) { const columns: Record = {} for (const key of COLUMN_ORDER) columns[key] = [] for (const issue of snap.issues) { const inf = inferLifecycle(issue, snap.timelines[issue.number] ?? [], asOf) columns[inf.column].push({ issue: issue.number, title: issue.title, labels: issue.labels }) } return { columns: COLUMN_ORDER.map((key) => ({ column: key, count: columns[key].length, issues: columns[key] })), } } function calibrationView(snap: ProjectSnapshot, asOf: Date) { const model = fitCalibration(calibrationSamples(snap.issues, snap.timelines, asOf)) return { n: model.n, coldStart: model.coldStart, globalMultiplier: Number(Math.exp(model.global.mu).toFixed(2)), byBucket: Object.entries(model.byBucket).map(([bucket, fit]) => ({ estimate: `est/${bucket}d`, n: fit.n, multiplier: Number(Math.exp(fit.mu).toFixed(2)), })), } } function issueView(snap: ProjectSnapshot, filters: QueryFilters, asOf: Date) { const issue = snap.issues.find((i) => i.number === filters.issueId) if (!issue) return { notFound: filters.issueId ?? null } const inf = inferLifecycle(issue, snap.timelines[issue.number] ?? [], asOf) const blockedBy = snap.deps.filter((d) => d.issue === issue.number).map((d) => d.dependsOn) const blocks = snap.deps.filter((d) => d.dependsOn === issue.number).map((d) => d.issue) return { issue: issue.number, title: issue.title, state: issue.state, column: inf.column, labels: issue.labels, assignee: issue.assignee, milestone: issue.milestone?.title ?? null, estimateDays: issue.facts.estimateDays, priority: issue.facts.priority, steepingDays: inf.steepingDays, blockedBy, blocks, } } function searchView(snap: ProjectSnapshot, filters: QueryFilters) { const q = (filters.query ?? '').toLowerCase().trim() const limit = Math.min(filters.limit ?? 20, 100) const hits = q ? snap.issues.filter( (i) => i.title.toLowerCase().includes(q) || i.labels.some((l) => l.toLowerCase().includes(q)), ) : [] return { query: filters.query ?? '', results: hits.slice(0, limit).map((i) => ({ issue: i.number, title: i.title, state: i.state, labels: i.labels })), } } /** Build the compact payload for one view. Unknown/unbuilt views return a marker. */ export function buildProjectView( view: ProjectView, filters: QueryFilters | undefined, snap: ProjectSnapshot, asOf: Date, ): unknown { const f = filters ?? {} switch (view) { case 'focus': return focusView(snap) case 'board': return boardView(snap, asOf) case 'calibration': return calibrationView(snap, asOf) case 'issue': return issueView(snap, f, asOf) case 'search': return searchView(snap, f) default: return { notImplemented: view } } }