Add tracer-bullet Kanban baseline with live issues read path
This commit is contained in:
parent
7537ec27b0
commit
c09420dc68
15 changed files with 748 additions and 8 deletions
99
src/lib/kanban.ts
Normal file
99
src/lib/kanban.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import type { BeadIssue } from './types';
|
||||
|
||||
export const KANBAN_STATUSES = ['open', 'in_progress', 'blocked', 'deferred', 'closed'] as const;
|
||||
|
||||
export type KanbanStatus = (typeof KANBAN_STATUSES)[number];
|
||||
|
||||
export type KanbanColumns = Record<KanbanStatus, BeadIssue[]>;
|
||||
|
||||
export interface KanbanFilterOptions {
|
||||
query?: string;
|
||||
type?: string;
|
||||
priority?: string;
|
||||
showClosed?: boolean;
|
||||
}
|
||||
|
||||
export interface KanbanStats {
|
||||
total: number;
|
||||
open: number;
|
||||
active: number;
|
||||
blocked: number;
|
||||
done: number;
|
||||
p0: number;
|
||||
}
|
||||
|
||||
function isKanbanStatus(status: string): status is KanbanStatus {
|
||||
return KANBAN_STATUSES.includes(status as KanbanStatus);
|
||||
}
|
||||
|
||||
function issueSort(a: BeadIssue, b: BeadIssue): number {
|
||||
const priorityDiff = a.priority - b.priority;
|
||||
if (priorityDiff !== 0) {
|
||||
return priorityDiff;
|
||||
}
|
||||
|
||||
return b.updated_at.localeCompare(a.updated_at);
|
||||
}
|
||||
|
||||
export function filterKanbanIssues(issues: BeadIssue[], filters: KanbanFilterOptions): BeadIssue[] {
|
||||
const query = (filters.query ?? '').trim().toLowerCase();
|
||||
const type = (filters.type ?? '').trim().toLowerCase();
|
||||
const priority = (filters.priority ?? '').trim();
|
||||
const showClosed = filters.showClosed ?? false;
|
||||
|
||||
return issues.filter((issue) => {
|
||||
if (!showClosed && issue.status === 'closed') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const haystack = [issue.id, issue.title, issue.description ?? '', issue.assignee ?? '', issue.labels.join(' ')].join(' ').toLowerCase();
|
||||
if (!haystack.includes(query)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (type && issue.issue_type.toLowerCase() !== type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (priority && String(issue.priority) !== priority) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function buildKanbanColumns(issues: BeadIssue[]): KanbanColumns {
|
||||
const columns = {
|
||||
open: [],
|
||||
in_progress: [],
|
||||
blocked: [],
|
||||
deferred: [],
|
||||
closed: [],
|
||||
} as KanbanColumns;
|
||||
|
||||
for (const issue of issues) {
|
||||
if (isKanbanStatus(issue.status)) {
|
||||
columns[issue.status].push(issue);
|
||||
}
|
||||
}
|
||||
|
||||
for (const status of KANBAN_STATUSES) {
|
||||
columns[status].sort(issueSort);
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
export function buildKanbanStats(issues: BeadIssue[]): KanbanStats {
|
||||
return {
|
||||
total: issues.length,
|
||||
open: issues.filter((x) => x.status === 'open').length,
|
||||
active: issues.filter((x) => x.status === 'in_progress').length,
|
||||
blocked: issues.filter((x) => x.status === 'blocked').length,
|
||||
done: issues.filter((x) => x.status === 'closed').length,
|
||||
p0: issues.filter((x) => x.priority === 0).length,
|
||||
};
|
||||
}
|
||||
33
src/lib/read-issues.ts
Normal file
33
src/lib/read-issues.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { parseIssuesJsonl } from './parser';
|
||||
import { canonicalizeWindowsPath } from './pathing';
|
||||
import type { BeadIssue } from './types';
|
||||
|
||||
export interface ReadIssuesOptions {
|
||||
projectRoot?: string;
|
||||
includeTombstones?: boolean;
|
||||
}
|
||||
|
||||
export function resolveIssuesJsonlPath(projectRoot: string = process.cwd()): string {
|
||||
const absolute = path.resolve(projectRoot, '.beads', 'issues.jsonl');
|
||||
return canonicalizeWindowsPath(absolute);
|
||||
}
|
||||
|
||||
export async function readIssuesFromDisk(options: ReadIssuesOptions = {}): Promise<BeadIssue[]> {
|
||||
const issuesPath = resolveIssuesJsonlPath(options.projectRoot);
|
||||
|
||||
try {
|
||||
const jsonl = await fs.readFile(issuesPath, 'utf8');
|
||||
return parseIssuesJsonl(jsonl, {
|
||||
includeTombstones: options.includeTombstones ?? false,
|
||||
});
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue