From eba197d5aed78911a8f2124a779a11eafa13b00c Mon Sep 17 00:00:00 2001 From: Ashley Venn Date: Wed, 24 Jun 2026 11:54:25 -0700 Subject: [PATCH] feat(tasks): add child task entry defaults --- .../V1_BLOCK_07_Child_Tasks.md | 12 ++ .../V1_BLOCK_07_Child_Tasks.md | 12 ++ lib/src/child_tasks.dart | 71 ++++++++++++ test/child_tasks_test.dart | 104 ++++++++++++++++++ 4 files changed, 199 insertions(+) diff --git a/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_07_Child_Tasks.md b/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_07_Child_Tasks.md index 85e9f82..5ecfcb5 100644 --- a/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_07_Child_Tasks.md +++ b/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_07_Child_Tasks.md @@ -47,6 +47,8 @@ Execution notes: Recommended Codex level: medium +Status: Completed + Tasks: Support row-style child entry data: @@ -72,6 +74,16 @@ Acceptance criteria: - Tests cover inherited project and overridden project. - Tests cover reward `not set` remaining distinct from very low reward. +Execution notes: + +- Added `ChildTaskEntry` for row-style child creation data. +- Child entries support title, nullable priority, reward, difficulty, duration, and optional project override. +- Child entries inherit the parent project when no override is supplied. +- Missing priority remains null so children without explicit priority can preserve row insertion order. +- Missing reward remains `RewardLevel.notSet`, distinct from `RewardLevel.veryLow`. +- Added tests for explicit child fields, project inheritance/override, reward not-set behavior, and no-priority row order. +- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` passed. + BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the child-task edge-case regression suite. ## Chunk 7.3 — Child-task edge-case regression test suite diff --git a/Codex Documentation/Current Software Plan/V1_BLOCK_07_Child_Tasks.md b/Codex Documentation/Current Software Plan/V1_BLOCK_07_Child_Tasks.md index 85e9f82..5ecfcb5 100644 --- a/Codex Documentation/Current Software Plan/V1_BLOCK_07_Child_Tasks.md +++ b/Codex Documentation/Current Software Plan/V1_BLOCK_07_Child_Tasks.md @@ -47,6 +47,8 @@ Execution notes: Recommended Codex level: medium +Status: Completed + Tasks: Support row-style child entry data: @@ -72,6 +74,16 @@ Acceptance criteria: - Tests cover inherited project and overridden project. - Tests cover reward `not set` remaining distinct from very low reward. +Execution notes: + +- Added `ChildTaskEntry` for row-style child creation data. +- Child entries support title, nullable priority, reward, difficulty, duration, and optional project override. +- Child entries inherit the parent project when no override is supplied. +- Missing priority remains null so children without explicit priority can preserve row insertion order. +- Missing reward remains `RewardLevel.notSet`, distinct from `RewardLevel.veryLow`. +- Added tests for explicit child fields, project inheritance/override, reward not-set behavior, and no-priority row order. +- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` passed. + BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the child-task edge-case regression suite. ## Chunk 7.3 — Child-task edge-case regression test suite diff --git a/lib/src/child_tasks.dart b/lib/src/child_tasks.dart index f94d949..27b96d0 100644 --- a/lib/src/child_tasks.dart +++ b/lib/src/child_tasks.dart @@ -6,6 +6,77 @@ import 'models.dart'; +/// Row-style input for creating one child task. +/// +/// Priority is nullable on purpose. A null priority means the user did not set +/// one, so callers should preserve row insertion order instead of treating the +/// child as medium priority. +class ChildTaskEntry { + const ChildTaskEntry({ + required this.id, + required this.title, + required this.createdAt, + this.priority, + this.reward = RewardLevel.notSet, + this.difficulty = DifficultyLevel.notSet, + this.durationMinutes, + this.projectId, + this.updatedAt, + }); + + /// Caller-generated child id. + final String id; + + /// User-entered child title. + final String title; + + /// Creation timestamp supplied by the caller. + final DateTime createdAt; + + /// Optional explicit child priority. + final PriorityLevel? priority; + + /// Optional explicit child reward. Missing reward stays `notSet`. + final RewardLevel reward; + + /// Optional explicit child difficulty. + final DifficultyLevel difficulty; + + /// Optional child duration estimate. + final int? durationMinutes; + + /// Optional project override. Null means inherit the parent project. + final String? projectId; + + /// Optional update timestamp. + final DateTime? updatedAt; + + /// Convert this entry into a child [Task] owned by [parent]. + Task toTask({ + required Task parent, + }) { + final trimmedTitle = title.trim(); + if (trimmedTitle.isEmpty) { + throw ArgumentError.value(title, 'title', 'Title is required.'); + } + + return Task( + id: id, + title: trimmedTitle, + projectId: projectId ?? parent.projectId, + type: TaskType.flexible, + status: TaskStatus.backlog, + priority: priority, + reward: reward, + difficulty: difficulty, + durationMinutes: durationMinutes, + parentTaskId: parent.id, + createdAt: createdAt, + updatedAt: updatedAt ?? createdAt, + ); + } +} + /// Read-only parent/child projection over a task list. /// /// This is intentionally not a scheduler. It answers ownership questions such diff --git a/test/child_tasks_test.dart b/test/child_tasks_test.dart index fab4c36..a1c70fa 100644 --- a/test/child_tasks_test.dart +++ b/test/child_tasks_test.dart @@ -112,6 +112,110 @@ void main() { expect(unchangedParent.scheduledStart, isNull); }); }); + + group('Child task entry defaults', () { + final now = DateTime(2026, 6, 19, 12); + + test('child creation supports explicit row fields', () { + final parent = task( + id: 'parent', + title: 'Clean kitchen', + createdAt: now, + ).copyWith(projectId: 'home'); + final child = ChildTaskEntry( + id: 'child', + title: ' Clear sink ', + createdAt: now, + priority: PriorityLevel.high, + reward: RewardLevel.high, + difficulty: DifficultyLevel.hard, + durationMinutes: 20, + ).toTask(parent: parent); + + expect(child.id, 'child'); + expect(child.title, 'Clear sink'); + expect(child.parentTaskId, parent.id); + expect(child.projectId, parent.projectId); + expect(child.priority, PriorityLevel.high); + expect(child.reward, RewardLevel.high); + expect(child.difficulty, DifficultyLevel.hard); + expect(child.durationMinutes, 20); + expect(child.status, TaskStatus.backlog); + }); + + test('child inherits parent project unless project override is supplied', + () { + final parent = task( + id: 'parent', + title: 'Plan trip', + createdAt: now, + ).copyWith(projectId: 'travel'); + + final inherited = ChildTaskEntry( + id: 'inherited', + title: 'Book train', + createdAt: now, + ).toTask(parent: parent); + final overridden = ChildTaskEntry( + id: 'overridden', + title: 'Ask partner', + createdAt: now, + projectId: 'relationships', + ).toTask(parent: parent); + + expect(inherited.projectId, 'travel'); + expect(overridden.projectId, 'relationships'); + }); + + test('missing reward remains not set and distinct from very low reward', + () { + final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); + final noReward = ChildTaskEntry( + id: 'no-reward', + title: 'Clear sink', + createdAt: now, + ).toTask(parent: parent); + final veryLowReward = ChildTaskEntry( + id: 'very-low', + title: 'Wipe counter', + createdAt: now, + reward: RewardLevel.veryLow, + ).toTask(parent: parent); + + expect(noReward.reward, RewardLevel.notSet); + expect(veryLowReward.reward, RewardLevel.veryLow); + expect(noReward.reward, isNot(veryLowReward.reward)); + }); + + test('children without priority preserve row insertion order', () { + final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); + final first = ChildTaskEntry( + id: 'first', + title: 'Clear sink', + createdAt: now, + ).toTask(parent: parent); + final second = ChildTaskEntry( + id: 'second', + title: 'Wipe counter', + createdAt: now, + ).toTask(parent: parent); + final third = ChildTaskEntry( + id: 'third', + title: 'Sweep floor', + createdAt: now, + ).toTask(parent: parent); + final view = ChildTaskView(tasks: [parent, first, second, third]); + + expect(first.priority, isNull); + expect(second.priority, isNull); + expect(third.priority, isNull); + expect(view.childrenOf(parent).map((child) => child.id), [ + 'first', + 'second', + 'third', + ]); + }); + }); } Task task({