feat: sprints with burndown and carry-over
Plan sprints with dates and a goal, run one at a time per project, and put tickets in them from the ticket page. The Sprints tab follows the one under way with its progress and a burndown in tickets or points against the ideal line. Closing a sprint sends what was left to the next one or back to the backlog; the board can be filtered by sprint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,13 @@ import {
|
||||
ReleaseReadModel,
|
||||
ReleaseStatus,
|
||||
StatusCategory as C,
|
||||
SprintReadModel,
|
||||
SprintStatus,
|
||||
} from '@task-stream/api/tracker/domain';
|
||||
import {
|
||||
computeProjectAnalytics,
|
||||
computeReleaseBurnup,
|
||||
computeSprintBurndown,
|
||||
weekOf,
|
||||
} from './analytics';
|
||||
|
||||
@@ -182,3 +185,62 @@ describe('release burnup', () => {
|
||||
expect(at(burnup.done, '2026-10-14')).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sprint burndown', () => {
|
||||
const sprint = {
|
||||
id: 's1',
|
||||
projectId: 'p',
|
||||
startDate: '2026-10-12',
|
||||
endDate: '2026-10-16',
|
||||
status: SprintStatus.ACTIVE,
|
||||
closedAt: null,
|
||||
} as SprintReadModel;
|
||||
const into = (issueId: string, at: string): IssueTimelineEntry => ({
|
||||
id: `${issueId}:${++seq}`,
|
||||
issueId,
|
||||
projectId: 'p',
|
||||
at,
|
||||
kind: 'sprint',
|
||||
sprintId: 's1',
|
||||
});
|
||||
// X (3 points) and Y (5) committed on Monday; Z (2) added on Tuesday.
|
||||
// X done on Tuesday, Y cancelled on Wednesday. Today is Wednesday noon.
|
||||
const issues = [
|
||||
{ ...issue('x', '2026-10-01T09:00:00Z', 'DONE', C.DONE), storyPoints: 3 },
|
||||
{
|
||||
...issue('y', '2026-10-01T09:00:00Z', 'CANCELLED', C.CANCELLED),
|
||||
storyPoints: 5,
|
||||
},
|
||||
{ ...issue('z', '2026-10-01T09:00:00Z', 'TODO', C.TODO), storyPoints: 2 },
|
||||
] as IssueReadModel[];
|
||||
const timeline = [
|
||||
step('x', '2026-10-01T09:00:00Z', 'TODO', C.TODO),
|
||||
step('y', '2026-10-01T09:00:00Z', 'TODO', C.TODO),
|
||||
step('z', '2026-10-01T09:00:00Z', 'TODO', C.TODO),
|
||||
into('x', '2026-10-12T08:00:00Z'),
|
||||
into('y', '2026-10-12T08:00:00Z'),
|
||||
into('z', '2026-10-13T10:00:00Z'),
|
||||
step('x', '2026-10-13T16:00:00Z', 'DONE', C.DONE),
|
||||
step('y', '2026-10-14T09:00:00Z', 'CANCELLED', C.CANCELLED),
|
||||
];
|
||||
|
||||
it('counts open tickets and points per day, and leaves the future blank', () => {
|
||||
const burndown = computeSprintBurndown({
|
||||
sprint,
|
||||
issues,
|
||||
timeline,
|
||||
now: new Date('2026-10-14T12:00:00Z'),
|
||||
});
|
||||
expect(burndown.dates).toEqual([
|
||||
'2026-10-12',
|
||||
'2026-10-13',
|
||||
'2026-10-14',
|
||||
'2026-10-15',
|
||||
'2026-10-16',
|
||||
]);
|
||||
expect(burndown.scope).toEqual([2, 3, 2, null, null]);
|
||||
expect(burndown.remaining).toEqual([2, 2, 1, null, null]);
|
||||
expect(burndown.remainingPoints).toEqual([8, 7, 2, null, null]);
|
||||
expect(burndown.ideal).toEqual([2, 1.5, 1, 0.5, 0]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user