From 11f2df269b7bf6e000dd42bfc0ce013208aa3436 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 15 Jun 2026 04:45:36 +0500 Subject: [PATCH] fix(todo): stop overdue-flow spam, enforce urgent due date, reconcile charter todo is the reference template other templates copy, so its patterns must be exemplary and its charter must match reality. - Overdue flow re-fired on EVERY edit of an already-overdue task (it triggers on record-after-update with no delta guard), spamming the assignee. Add a `previous.*` guard so it fires once on the transition into overdue-and-open, and document that time-based crossing needs a scheduled-flow fork. - The `due_date_required_for_urgent` validation message shipped in all locales but no rule enforced it. Add the validation (urgent tasks need a due date). - Remove the dead `recent_overdue_list` dashboard-widget translation key from all four locales (the widget was migrated to the Overdue tab). - Reconcile CHARTER with what actually ships: objects are `task` + `label` (project / task_label / approval / sharing are labelled roadmap, not claimed as shipped); locales are en + zh-CN + ja-JP + es-ES (the "no second locale" non-goal was false). Honest docs matter most on the template others copy. typecheck + objectstack build + format:check clean (2 Objects, 2 Flows). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/todo/CHARTER.md | 30 ++++++++++--------- packages/todo/src/flows/task_overdue.flow.ts | 19 +++++++++--- packages/todo/src/objects/todo_task.object.ts | 9 ++++++ packages/todo/src/translations/en.ts | 4 --- packages/todo/src/translations/es-ES.ts | 4 --- packages/todo/src/translations/ja-JP.ts | 4 --- packages/todo/src/translations/zh-CN.ts | 1 - 7 files changed, 40 insertions(+), 31 deletions(-) diff --git a/packages/todo/CHARTER.md b/packages/todo/CHARTER.md index 94ab1f4..12af1cc 100644 --- a/packages/todo/CHARTER.md +++ b/packages/todo/CHARTER.md @@ -17,25 +17,27 @@ It is the canonical *"hello world that isn't a toy"* for ObjectStack. | Capability | How | |---|---| -| Multi-object schema with relationships | `project` → `task`, `task` ↔ `label` (junction) | +| Object + lookup relationship | `task` with a multi-value `label` lookup | | State machine | `task.status`: `todo → doing → done` (+ `cancelled`) | -| Validation rules | due-date sanity, sub-task ownership | -| Workflow / flow | overdue notification, status-change side effects | -| Approval | high-priority tasks require lead approval | -| Sharing rule | project-level row sharing | -| Cube + Report | task throughput, overdue tasks | +| Validation rules | due-date sanity; urgent tasks require a due date | +| Workflow / flow | overdue notification (delta-guarded), assignment notification | | Dashboard | "My work" landing page | | Pages & Views | list, kanban, record page with related lists | | Permission set | `contributor` vs `lead` | -| i18n | English (single locale; this is a starter, fork to add) | -| Seed data | one demo project, a few tasks, two labels | +| i18n | Multi-locale: English + 简体中文 + 日本語 + Español | +| Seed data | a set of demo tasks across states, plus labels | + +> **Roadmap (not yet shipped):** a `project` grouping object, a `task_label` +> junction, an approval process for urgent tasks, and a project-scoped sharing +> rule. The schema and caps leave room for them; they are intentionally absent +> from the minimal starter. Earlier charter drafts described them as shipped — +> they are not. ## What it deliberately does NOT do - **No re-implementation of platform objects.** Comments, attachments, activity feed, users, teams, roles, audit logs, notifications, emails — all come from `sys_*` objects shipped by the platform. The template *uses* them via the polymorphic pattern (`parent_object` / `parent_id` or `thread_id = "{object}:{id}"`). - **No tutorials.** The template assumes you already read the docs. Comments in code stay short. -- **No fictional "advanced" features** that aren't in `@objectstack/spec@5.2`. If the spec doesn't validate it, it doesn't ship. -- **No second locale.** Users fork-and-localize; multi-locale is demonstrated by `hotcrm`. +- **No fictional "advanced" features** that aren't in the spec. If the spec doesn't validate it, it doesn't ship. - **No fake AI flair.** Where AI helps a real workflow (e.g. summarise an overdue task), it appears as an `*.action.ts`. No "AI assistant" shells. ## Hard limits @@ -44,13 +46,13 @@ These exist so the template stays a **template**, not a half-finished product: | Metric | Cap | Rationale | |---|---|---| -| Business objects | ≤ 4 (`project`, `task`, `label`, `task_label`) | Above this and it becomes a domain app | +| Business objects | ≤ 4 (`task`, `label` ship; `project` + `task_label` are roadmap, within cap) | Above this and it becomes a domain app | | Total `.ts` LOC under `src/` | ≤ 2,500 | Readable in one sitting | -| Locales | 1 (`en`) | Forkable starting point | +| Locales | 4 (`en`, `zh-CN`, `ja-JP`, `es-ES`) | Demonstrates the i18n bundle layout | | Dashboards | ≤ 2 | Demonstrates, doesn't overwhelm | | Flows | ≤ 3 | Same | -| Approval processes | 1 | Same | -| Sharing rules | 1 | Same | +| Approval processes | ≤ 1 | Roadmap (none ship today) | +| Sharing rules | ≤ 1 | Roadmap (none ship today) | | Permission sets | 2 | `contributor` + `lead` | If you find yourself exceeding any of these, the right move is to **fork into a new template** (e.g. `helpdesk`), not bloat `todo`. diff --git a/packages/todo/src/flows/task_overdue.flow.ts b/packages/todo/src/flows/task_overdue.flow.ts index fcedcd9..77cabdc 100644 --- a/packages/todo/src/flows/task_overdue.flow.ts +++ b/packages/todo/src/flows/task_overdue.flow.ts @@ -4,9 +4,17 @@ import type * as Automation from '@objectstack/spec/automation'; type Flow = Automation.Flow; /** - * Overdue notification — fires when a task crosses its due date while still - * open. Sends an in-app notification + email to the assignee via the - * platform's notification/email services (no per-app email infra). + * Overdue notification — fires when a task *enters* the overdue-and-open state + * (a past due date is set, or an overdue task is reopened to todo/doing). The + * `previous.*` delta guard makes it fire ONCE on that transition instead of on + * every subsequent edit of an already-overdue task — otherwise any field change + * re-spams the assignee. + * + * Limitation: a task that crosses its due date purely by time passing produces + * no update event, so this event-driven flow won't catch it. For unattended + * "it's now overdue" detection, fork this into a scheduled flow that scans open + * past-due tasks daily. Sends an in-app notification + email via the platform's + * notification/email services (no per-app email infra). */ export const TaskOverdueFlow: Flow = { name: 'todo_task_overdue_notify', @@ -24,7 +32,10 @@ export const TaskOverdueFlow: Flow = { config: { objectName: 'todo_task', triggerType: 'record-after-update', - condition: "record.due_date < today() && record.status in ['todo', 'doing']", + // Fire only on the transition INTO overdue-and-open (delta guard), + // not on every edit of an already-overdue task. + condition: + "record.due_date != null && record.due_date < today() && record.status in ['todo', 'doing'] && (previous.due_date == null || previous.due_date >= today() || !(previous.status in ['todo', 'doing']))", }, }, { diff --git a/packages/todo/src/objects/todo_task.object.ts b/packages/todo/src/objects/todo_task.object.ts index 8bd4e9c..6704e33 100644 --- a/packages/todo/src/objects/todo_task.object.ts +++ b/packages/todo/src/objects/todo_task.object.ts @@ -128,6 +128,15 @@ export const Task = ObjectSchema.create({ message: 'completed_at is set automatically when status becomes done.', condition: P`record.status != "done" && record.completed_at != null`, }, + { + // Urgent work needs a deadline. (The `due_date_required_for_urgent` + // message already shipped in translations; this is the missing rule.) + name: 'due_date_required_for_urgent', + type: 'script', + severity: 'error', + message: 'Urgent tasks must have a due date.', + condition: P`record.priority == "urgent" && record.due_date == null && record.status != "done" && record.status != "cancelled"`, + }, ], // `started_at` / `completed_at` are stamped by `todo_task.hook.ts` on diff --git a/packages/todo/src/translations/en.ts b/packages/todo/src/translations/en.ts index 992cf3b..dafb05c 100644 --- a/packages/todo/src/translations/en.ts +++ b/packages/todo/src/translations/en.ts @@ -110,10 +110,6 @@ export const en: TranslationData = { title: 'Done This Week', description: 'Tasks you completed since the start of the week', }, - recent_overdue_list: { - title: 'Overdue Tasks', - description: 'Your open tasks past their due date, sorted by oldest first', - }, }, }, }, diff --git a/packages/todo/src/translations/es-ES.ts b/packages/todo/src/translations/es-ES.ts index 1930855..b288c91 100644 --- a/packages/todo/src/translations/es-ES.ts +++ b/packages/todo/src/translations/es-ES.ts @@ -120,10 +120,6 @@ export const esES: TranslationData = { title: 'Completadas esta semana', description: 'Tareas completadas desde el inicio de la semana', }, - recent_overdue_list: { - title: 'Tareas vencidas', - description: 'Tus tareas vencidas ordenadas de más antigua a más reciente', - }, }, }, }, diff --git a/packages/todo/src/translations/ja-JP.ts b/packages/todo/src/translations/ja-JP.ts index b9e8fac..2737e15 100644 --- a/packages/todo/src/translations/ja-JP.ts +++ b/packages/todo/src/translations/ja-JP.ts @@ -109,10 +109,6 @@ export const jaJP: TranslationData = { }, my_overdue: { title: '期限超過', description: '期限を過ぎた自分の未完了タスク' }, done_this_week: { title: '今週の完了', description: '今週の始めから完了したタスク' }, - recent_overdue_list: { - title: '期限超過タスク', - description: '古い順にソートされた期限超過タスク', - }, }, }, }, diff --git a/packages/todo/src/translations/zh-CN.ts b/packages/todo/src/translations/zh-CN.ts index 0d8b70c..9001416 100644 --- a/packages/todo/src/translations/zh-CN.ts +++ b/packages/todo/src/translations/zh-CN.ts @@ -100,7 +100,6 @@ export const zhCN: TranslationData = { my_open_tasks: { title: '我的进行中任务', description: '分配给你的待办或进行中任务' }, my_overdue: { title: '逾期', description: '已超过截止日期的进行中任务' }, done_this_week: { title: '本周完成', description: '本周以来你完成的任务' }, - recent_overdue_list: { title: '逾期任务', description: '按最久优先排序的逾期任务列表' }, }, }, },