Skip to content

Commit c11b699

Browse files
os-zhuangclaude
andauthored
fix(example-todo): remove the inert is_completed/is_overdue flags and repair every filter that read them (#8295)
* fix(example-todo): remove the inert is_completed/is_overdue flags and repair every filter that read them * fix(example-todo): remove the inert is_completed/is_overdue flags and repair every filter that read them Both were readonly booleans defaulting to false that nothing ever wrote, while twelve view/dashboard/report/flow filters read them as if maintained. Every is_completed:true surface and the whole Overdue Tasks view were permanently empty; the eight is_completed:false filters matched completed tasks too. Removed rather than derived as formulas: a formula field is virtual, so a filter naming one matches nothing -- measured at 0 rows with no error, where the stored boolean returned every row. Deriving would have silently emptied the Due Today view, the reminder flow and both open-task reports. Every consumer now asks status / due_date directly. The hook's unreachable overdue branch is removed rather than re-armed: becoming overdue is the passage of time, not a record write, and the overdue_escalation scheduled flow already covers it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARidKDYSCD56LaygrvDPnk --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1dc25e8 commit c11b699

12 files changed

Lines changed: 429 additions & 42 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
"@objectstack/example-todo": patch
3+
---
4+
5+
fix(example-todo): remove the inert `is_completed` / `is_overdue` flags and repair every filter that read them (#7226)
6+
7+
`examples/app-todo/src/objects/task.object.ts` declared `is_completed` and
8+
`is_overdue` as `readonly: true` booleans defaulting to `false`. Nothing in the
9+
app ever wrote either one — no hook leg, no flow node, no action handler, and
10+
the seed data set neither — so both were `false` on every row for the life of
11+
the app, while **twelve** view / dashboard / report / flow filters read them as
12+
if they were maintained.
13+
14+
The consequence was not cosmetic. Every surface asking `is_completed: true` was
15+
permanently empty: the "Completed Today" tile, the "Weekly Task Completion"
16+
trend, and both the "Completed Tasks" and "Time Tracking" reports. So was the
17+
whole "Overdue Tasks" list view, which asked `is_overdue: true`. The eight
18+
surfaces asking `is_completed: false` were vacuously true instead — they matched
19+
completed tasks too. `task.hook.ts` also carried an `afterUpdate` branch gated
20+
on `data.is_overdue && previous && !previous.is_overdue`, which could never run.
21+
Since #7036 started stamping `completed_date` on the completion transition, the
22+
divergence was directly readable in the shipped app: a task could carry a
23+
completion date and `is_completed: false` at the same time.
24+
25+
**Removed rather than derived as formula fields, for a measured reason.** A
26+
`Field.formula(...)` computes both correctly — including the temporal one
27+
(`date(record.due_date) < today()` evaluates per read, with a per-call `now`
28+
snapshot) — so deriving looks like the obvious repair. It is not: a `formula`
29+
field is virtual, no driver materialises a column for it, and so a *filter*
30+
naming one matches nothing. Measured on this app's own sqlite-wasm driver,
31+
`where { is_completed: false }` against a formula field returns **0 rows with no
32+
error**, where the stored boolean returned every row. Deriving would therefore
33+
have silently emptied the "Due Today" view, the daily reminder flow and both
34+
open-task reports — trading a wrong answer for an invisible one.
35+
36+
`status` and `due_date` are stored, indexed columns that already carry the
37+
information, and both are declared dimensions on the `task_metrics` dataset, so
38+
every consumer now asks the semantic layer's own vocabulary directly:
39+
40+
| was | is now |
41+
|---|---|
42+
| `is_completed == true` | `status equals 'completed'` |
43+
| `is_completed == false` | `status not_equals 'completed'` |
44+
| `is_overdue == true` | `due_date less_than '{today}'` AND `status not_equals 'completed'` |
45+
46+
Updated across `task.object.ts`, `task.hook.ts`, `task.view.ts`,
47+
`task.dashboard.ts`, `task.report.ts`, `task.flow.ts`, the three translation
48+
bundles and the README. The hook's dead overdue branch is removed rather than
49+
re-armed against `due_date`: becoming overdue is the passage of time, not a
50+
record write, so a record hook is structurally the wrong instrument — the
51+
clock-driven `overdue_escalation` scheduled flow already covers it.
52+
53+
Pinned by `examples/app-todo/test/derived-flag-removal.test.ts`, which walks the
54+
app's real `defineStack` for any surviving reference, drives the replacement
55+
filters across **both** sides of the completion transition (so a filter cannot
56+
pass for the same reason the old flag did — everything being false), and records
57+
the formula-filter measurement that decided the route.

examples/app-todo/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ examples/app-todo/
5656
-**Select** (`status`, `priority`, `category`) — Single-select with colors
5757
-**Multi-Select** (`tags`) — Multiple tag selection
5858
-**Date / DateTime** (`due_date`, `reminder_date`, `completed_date`)
59-
-**Boolean** (`is_completed`, `is_overdue`, `is_recurring`)
59+
-**Boolean** (`is_recurring`)
6060
-**Number** (`estimated_hours`, `actual_hours`, `recurrence_interval`)
6161
-**Percent** (`progress_percent`) — Progress tracking
6262
-**Lookup** (`owner`) — User assignment
@@ -90,8 +90,9 @@ examples/app-todo/
9090
### Validations & Automation
9191
- Completed date required when status is "completed" (validation rule)
9292
- Recurrence type required for recurring tasks (validation rule)
93-
- Auto-set `is_completed`, `completed_date`, `progress_percent` on status
94-
change (data hook)
93+
- Auto-set `completed_date` on the completion transition, cleared on reopen
94+
(data hook). Completion and overdue state are read from `status` / `due_date`
95+
directly — the app declares no derived boolean flags (#7226)
9596
- Auto-detect overdue tasks and send urgent notifications (flow)
9697

9798
## 💡 How to Run

examples/app-todo/src/dashboards/task.dashboard.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const TaskDashboard: Dashboard = {
3232
id: 'completed_today',
3333
title: 'Completed Today',
3434
type: 'metric',
35-
filter: { is_completed: true, completed_date: { $gte: '{today}' } },
35+
filter: { status: 'completed', completed_date: { $gte: '{today}' } },
3636
dataset: 'task_metrics',
3737
values: ['task_count'],
3838
layout: { x: 3, y: 0, w: 3, h: 2 },
@@ -42,7 +42,7 @@ export const TaskDashboard: Dashboard = {
4242
id: 'overdue_tasks',
4343
title: 'Overdue Tasks',
4444
type: 'metric',
45-
filter: { is_overdue: true, is_completed: false },
45+
filter: { due_date: { $lt: '{today}' }, status: { $ne: 'completed' } },
4646
dataset: 'task_metrics',
4747
values: ['task_count'],
4848
layout: { x: 6, y: 0, w: 3, h: 2 },
@@ -66,7 +66,7 @@ export const TaskDashboard: Dashboard = {
6666
id: 'tasks_by_status',
6767
title: 'Tasks by Status',
6868
type: 'pie',
69-
filter: { is_completed: false },
69+
filter: { status: { $ne: 'completed' } },
7070
dataset: 'task_metrics',
7171
dimensions: ['status'],
7272
values: ['task_count'],
@@ -78,7 +78,7 @@ export const TaskDashboard: Dashboard = {
7878
id: 'tasks_by_priority',
7979
title: 'Tasks by Priority',
8080
type: 'bar',
81-
filter: { is_completed: false },
81+
filter: { status: { $ne: 'completed' } },
8282
dataset: 'task_metrics',
8383
dimensions: ['priority'],
8484
values: ['task_count'],
@@ -92,7 +92,7 @@ export const TaskDashboard: Dashboard = {
9292
id: 'weekly_task_completion',
9393
title: 'Weekly Task Completion',
9494
type: 'line',
95-
filter: { is_completed: true, completed_date: { $gte: '{4_weeks_ago}' } },
95+
filter: { status: 'completed', completed_date: { $gte: '{4_weeks_ago}' } },
9696
dataset: 'task_metrics',
9797
dimensions: ['completed_date'],
9898
values: ['task_count'],
@@ -104,7 +104,7 @@ export const TaskDashboard: Dashboard = {
104104
id: 'tasks_by_category',
105105
title: 'Tasks by Category',
106106
type: 'donut',
107-
filter: { is_completed: false },
107+
filter: { status: { $ne: 'completed' } },
108108
dataset: 'task_metrics',
109109
dimensions: ['category'],
110110
values: ['task_count'],

examples/app-todo/src/flows/task.flow.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const TaskReminderFlow: Flow = {
2323
// `limit > 1` is the declared way to make this a LIST read (`find`, not
2424
// `findOne`) — the undeclared `getAll` that sat here was never read, so
2525
// this sweep silently fetched a single task (#4277 rejects the key now).
26-
config: { objectName: 'todo_task', filter: { due_date: '{tomorrow}', is_completed: false }, outputVariable: 'tasksToRemind', limit: 200 },
26+
config: { objectName: 'todo_task', filter: { due_date: '{tomorrow}', status: { $ne: 'completed' } }, outputVariable: 'tasksToRemind', limit: 200 },
2727
},
2828
{
2929
id: 'loop_tasks', type: 'loop', label: 'Loop Through Tasks',
@@ -75,7 +75,7 @@ export const OverdueEscalationFlow: Flow = {
7575
// `limit > 1` = LIST read; the undeclared `getAll` was never read (#4277).
7676
config: {
7777
objectName: 'todo_task',
78-
filter: { due_date: { $lt: '{3_days_ago}' }, is_completed: false, is_overdue: true },
78+
filter: { due_date: { $lt: '{3_days_ago}' }, status: { $ne: 'completed' } },
7979
outputVariable: 'overdueTasks', limit: 200,
8080
},
8181
},
@@ -245,7 +245,7 @@ export const TaskCompletionFlow: Flow = {
245245
// A whole-string token, so `interpolate()` hands the create the RAW
246246
// value the script node returned instead of a stringified copy.
247247
due_date: '{nextDueDate}',
248-
status: 'not_started', is_completed: false,
248+
status: 'not_started',
249249
},
250250
outputVariable: 'newTaskId',
251251
},

examples/app-todo/src/objects/task.hook.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,19 @@ const taskHook: Hook = {
105105
// Could trigger notifications or integrations here
106106
}
107107

108-
// Check if task became overdue
109-
if (data.is_overdue && previous && !previous.is_overdue) {
110-
logger?.info?.(`Task ${ctx.input.id} is now overdue`);
111-
}
108+
// [#7226] A "task became overdue" leg USED TO SIT HERE, gated on
109+
// `data.is_overdue && previous && !previous.is_overdue`. It could never
110+
// run: `is_overdue` was a `readonly` boolean nothing ever wrote, so
111+
// `data.is_overdue` was absent on every update and the branch was dead
112+
// code that read as working automation.
113+
//
114+
// It is not re-armed against `due_date`, and that is deliberate. Becoming
115+
// overdue is the passage of TIME, not a record write — a task nobody
116+
// touches crosses its due date with no update to observe, so a record hook
117+
// is structurally the wrong instrument and any version of this branch would
118+
// fire late, or never. The clock-driven sweep already exists in the right
119+
// place: `flows/task.flow.ts`'s `overdue_escalation`, a scheduled flow that
120+
// runs daily and selects on `due_date` directly.
112121
}
113122
}
114123
};

examples/app-todo/src/objects/task.object.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -125,19 +125,37 @@ export const Task = ObjectSchema.create({
125125
min: 1,
126126
}),
127127

128-
// Flags
129-
is_completed: Field.boolean({
130-
label: 'Is Completed',
131-
defaultValue: false,
132-
readonly: true,
133-
}),
134-
135-
is_overdue: Field.boolean({
136-
label: 'Is Overdue',
137-
defaultValue: false,
138-
readonly: true,
139-
}),
140-
128+
// [#7226] `is_completed` / `is_overdue` USED TO LIVE HERE, and were removed.
129+
//
130+
// Both were `readonly: true` booleans defaulting to `false` that nothing in
131+
// the app ever wrote — no hook leg, no flow node, no action handler, and the
132+
// seed data set neither. They were therefore `false` on every row for the
133+
// life of the app, while twelve view / dashboard / report / flow filters
134+
// read them as if they were maintained. `is_completed: true` tiles ("Completed
135+
// Today", "Weekly Task Completion", the two completed-task reports) were
136+
// permanently empty, and the divergence became visible once #7036 started
137+
// stamping `completed_date` on the completion transition: a task could carry
138+
// a completion date and `is_completed: false` at the same time.
139+
//
140+
// They are GONE rather than derived, and the reason is measured, not
141+
// stylistic. `Field.formula(...)` computes correctly for both — including the
142+
// temporal one (`date(record.due_date) < today()` evaluates per read, with a
143+
// per-call `now` snapshot) — but a formula field is VIRTUAL: no driver
144+
// materialises a column for it, so a FILTER naming one matches nothing.
145+
// Measured on this app's own sqlite-wasm driver: `where { is_completed: false }`
146+
// against a formula field returns 0 rows with no error, where the stored
147+
// boolean returned every row. Deriving them would have silently emptied the
148+
// "Due Today" view, the reminder flow and both open-task reports — trading a
149+
// wrong answer for an invisible one.
150+
//
151+
// `status` and `due_date` are stored, indexed columns that already carry the
152+
// information, so every consumer now asks them directly:
153+
// is_completed == true -> status equals 'completed'
154+
// is_completed == false -> status not_equals 'completed'
155+
// is_overdue == true -> due_date less_than '{today}' AND status not_equals 'completed'
156+
// Consistent by construction, with no second writer that can drift — which is
157+
// the pattern a reference app should be teaching.
158+
141159
// Progress
142160
progress_percent: Field.percent({
143161
label: 'Progress (%)',

examples/app-todo/src/reports/task.report.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const TasksByPriorityReport = defineReport({
2828
dataset: 'task_metrics',
2929
rows: ['priority'],
3030
values: ['task_count'],
31-
runtimeFilter: { is_completed: false },
31+
runtimeFilter: { status: { $ne: 'completed' } },
3232
});
3333

3434
/** Tasks by Owner Report */
@@ -40,7 +40,7 @@ export const TasksByOwnerReport = defineReport({
4040
dataset: 'task_metrics',
4141
rows: ['owner'],
4242
values: ['est_hours', 'actual_hours'],
43-
runtimeFilter: { is_completed: false },
43+
runtimeFilter: { status: { $ne: 'completed' } },
4444
});
4545

4646
// ADR-0021 Phase 2: the former `OverdueTasksReport` (a flat record list, no
@@ -57,7 +57,7 @@ export const CompletedTasksReport = defineReport({
5757
dataset: 'task_metrics',
5858
rows: ['category'],
5959
values: ['est_hours', 'actual_hours'],
60-
runtimeFilter: { is_completed: true },
60+
runtimeFilter: { status: 'completed' },
6161
});
6262

6363
/** Time Tracking Report */
@@ -72,5 +72,5 @@ export const TimeTrackingReport = defineReport({
7272
dataset: 'task_metrics',
7373
rows: ['owner', 'category'],
7474
values: ['est_hours', 'actual_hours'],
75-
runtimeFilter: { is_completed: true },
75+
runtimeFilter: { status: 'completed' },
7676
});

examples/app-todo/src/translations/en.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ export const en: TranslationData = {
7171
},
7272
},
7373
recurrence_interval: { label: 'Recurrence Interval' },
74-
is_completed: { label: 'Is Completed' },
75-
is_overdue: { label: 'Is Overdue' },
7674
progress_percent: { label: 'Progress (%)' },
7775
estimated_hours: { label: 'Estimated Hours' },
7876
actual_hours: { label: 'Actual Hours' },

examples/app-todo/src/translations/ja-JP.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,6 @@ export const jaJP: TranslationData = {
7070
},
7171
},
7272
recurrence_interval: { label: '繰り返し間隔' },
73-
is_completed: { label: '完了済み' },
74-
is_overdue: { label: '期限超過' },
7573
progress_percent: { label: '進捗率 (%)' },
7674
estimated_hours: { label: '見積時間' },
7775
actual_hours: { label: '実績時間' },

examples/app-todo/src/translations/zh-CN.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,6 @@ export const zhCN: TranslationData = {
7474
},
7575
},
7676
recurrence_interval: { label: '重复间隔' },
77-
is_completed: { label: '是否完成' },
78-
is_overdue: { label: '是否逾期' },
7977
progress_percent: { label: '进度 (%)' },
8078
estimated_hours: { label: '预估工时' },
8179
actual_hours: { label: '实际工时' },

0 commit comments

Comments
 (0)