feat(backlog): add staleness markers
This commit is contained in:
parent
f84d5661c0
commit
de05eafe21
3 changed files with 94 additions and 1 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
# V1 Block 04 — Backlog and Quick Capture
|
# V1 Block 04 — Backlog and Quick Capture
|
||||||
|
|
||||||
Status: Planned
|
Status: Completed
|
||||||
|
|
||||||
Purpose: Support low-friction task capture and a unified backlog/wishlist model.
|
Purpose: Support low-friction task capture and a unified backlog/wishlist model.
|
||||||
|
|
||||||
|
|
@ -116,6 +116,8 @@ BREAKPOINT: Stop here. Confirm `high` mode before implementing scheduling integr
|
||||||
|
|
||||||
Recommended Codex level: low
|
Recommended Codex level: low
|
||||||
|
|
||||||
|
Status: Completed
|
||||||
|
|
||||||
Tasks:
|
Tasks:
|
||||||
|
|
||||||
Implement age/staleness calculation for backlog display:
|
Implement age/staleness calculation for backlog display:
|
||||||
|
|
@ -130,6 +132,15 @@ Acceptance criteria:
|
||||||
|
|
||||||
- Tests cover fresh, aging, stale, and configurable thresholds.
|
- Tests cover fresh, aging, stale, and configurable thresholds.
|
||||||
|
|
||||||
|
Execution notes:
|
||||||
|
|
||||||
|
- Added `BacklogStalenessMarker` values for green, blue, and purple display buckets.
|
||||||
|
- Added `BacklogStalenessSettings` with configurable green and blue thresholds.
|
||||||
|
- Added `BacklogView.stalenessMarkerFor` to calculate display marker state without side effects or prompts.
|
||||||
|
- Default thresholds are green through 7 days, blue through 30 days, and purple from 31 days onward.
|
||||||
|
- Added tests for fresh, aging, stale, and custom threshold behavior.
|
||||||
|
- `dart analyze` and `dart test` passed.
|
||||||
|
|
||||||
Commit suggestion:
|
Commit suggestion:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|
@ -19,17 +19,54 @@ enum BacklogSortKey {
|
||||||
timesPushed,
|
timesPushed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Visual age bucket for backlog display.
|
||||||
|
enum BacklogStalenessMarker {
|
||||||
|
green,
|
||||||
|
blue,
|
||||||
|
purple,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configurable thresholds for backlog age markers.
|
||||||
|
class BacklogStalenessSettings {
|
||||||
|
const BacklogStalenessSettings({
|
||||||
|
this.greenMaxAge = const Duration(days: 7),
|
||||||
|
this.blueMaxAge = const Duration(days: 30),
|
||||||
|
});
|
||||||
|
|
||||||
|
final Duration greenMaxAge;
|
||||||
|
final Duration blueMaxAge;
|
||||||
|
|
||||||
|
BacklogStalenessMarker markerFor({
|
||||||
|
required Task task,
|
||||||
|
required DateTime now,
|
||||||
|
}) {
|
||||||
|
final age = now.difference(task.createdAt);
|
||||||
|
|
||||||
|
if (age <= greenMaxAge) {
|
||||||
|
return BacklogStalenessMarker.green;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (age <= blueMaxAge) {
|
||||||
|
return BacklogStalenessMarker.blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BacklogStalenessMarker.purple;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Read-only backlog projection over the unified task list.
|
/// Read-only backlog projection over the unified task list.
|
||||||
class BacklogView {
|
class BacklogView {
|
||||||
const BacklogView({
|
const BacklogView({
|
||||||
required this.tasks,
|
required this.tasks,
|
||||||
required this.now,
|
required this.now,
|
||||||
this.staleAfter = const Duration(days: 31),
|
this.staleAfter = const Duration(days: 31),
|
||||||
|
this.stalenessSettings = const BacklogStalenessSettings(),
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<Task> tasks;
|
final List<Task> tasks;
|
||||||
final DateTime now;
|
final DateTime now;
|
||||||
final Duration staleAfter;
|
final Duration staleAfter;
|
||||||
|
final BacklogStalenessSettings stalenessSettings;
|
||||||
|
|
||||||
List<Task> get backlogTasks {
|
List<Task> get backlogTasks {
|
||||||
return tasks.where((task) => task.isBacklog).toList(growable: false);
|
return tasks.where((task) => task.isBacklog).toList(growable: false);
|
||||||
|
|
@ -48,6 +85,10 @@ class BacklogView {
|
||||||
return List<Task>.unmodifiable(sortedTasks);
|
return List<Task>.unmodifiable(sortedTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BacklogStalenessMarker stalenessMarkerFor(Task task) {
|
||||||
|
return stalenessSettings.markerFor(task: task, now: now);
|
||||||
|
}
|
||||||
|
|
||||||
bool _matchesFilter(Task task, BacklogFilter filter) {
|
bool _matchesFilter(Task task, BacklogFilter filter) {
|
||||||
return switch (filter) {
|
return switch (filter) {
|
||||||
BacklogFilter.inbox => task.projectId == 'inbox',
|
BacklogFilter.inbox => task.projectId == 'inbox',
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,47 @@ void main() {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('backlog staleness markers cover default thresholds', () {
|
||||||
|
final fresh = Task.quickCapture(
|
||||||
|
id: 'fresh',
|
||||||
|
title: 'Fresh',
|
||||||
|
createdAt: now.subtract(const Duration(days: 7)),
|
||||||
|
);
|
||||||
|
final aging = Task.quickCapture(
|
||||||
|
id: 'aging',
|
||||||
|
title: 'Aging',
|
||||||
|
createdAt: now.subtract(const Duration(days: 8)),
|
||||||
|
);
|
||||||
|
final stale = Task.quickCapture(
|
||||||
|
id: 'stale',
|
||||||
|
title: 'Stale',
|
||||||
|
createdAt: now.subtract(const Duration(days: 31)),
|
||||||
|
);
|
||||||
|
final view = BacklogView(tasks: [fresh, aging, stale], now: now);
|
||||||
|
|
||||||
|
expect(view.stalenessMarkerFor(fresh), BacklogStalenessMarker.green);
|
||||||
|
expect(view.stalenessMarkerFor(aging), BacklogStalenessMarker.blue);
|
||||||
|
expect(view.stalenessMarkerFor(stale), BacklogStalenessMarker.purple);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('backlog staleness markers support configurable thresholds', () {
|
||||||
|
final task = Task.quickCapture(
|
||||||
|
id: 'custom',
|
||||||
|
title: 'Custom',
|
||||||
|
createdAt: now.subtract(const Duration(days: 5)),
|
||||||
|
);
|
||||||
|
final view = BacklogView(
|
||||||
|
tasks: [task],
|
||||||
|
now: now,
|
||||||
|
stalenessSettings: const BacklogStalenessSettings(
|
||||||
|
greenMaxAge: Duration(days: 2),
|
||||||
|
blueMaxAge: Duration(days: 4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(view.stalenessMarkerFor(task), BacklogStalenessMarker.purple);
|
||||||
|
});
|
||||||
|
|
||||||
test('unchecked quick capture goes to backlog', () {
|
test('unchecked quick capture goes to backlog', () {
|
||||||
const service = QuickCaptureService();
|
const service = QuickCaptureService();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue