{ "description": "To-do app event vectors: ensures todo fold handlers produce consistent state. Source of truth: web/static/apps/todo.js", "app": "todo", "source": "web/static/apps/todo.js", "state_shape": { "items": [{ "id": "string", "text": "string", "done": "boolean", "doneAt": "number (epoch ms)", "by": "string (sender name)" }] }, "vectors": [ { "name": "todo_add", "description": "Add a to-do item (checked off = false by default).", "event": { "type": "todo.add", "id": "todo-1", "text": "buy milk", "name": "Alice" }, "expected_state_update": { "items": [ { "id": "todo-1", "text": "buy milk", "done": false, "doneAt": 0, "by": "Alice" } ] }, "expected_chat_projection": { "text": "added “buy milk”", "action": true }, "expected_group_chat_projection": { "text": "added “buy milk” to the to-dos", "action": true } }, { "name": "todo_done", "description": "Check off a to-do (mark as done with a timestamp for the grace period animation).", "event": { "type": "todo.done", "id": "todo-1", "ts": 1700000000000 }, "initial_state": { "items": [ { "id": "todo-1", "text": "buy milk", "done": false, "doneAt": 0, "by": "Alice" } ] }, "expected_state_update": { "items": [ { "id": "todo-1", "text": "buy milk", "done": true, "doneAt": 1700000000000, "by": "Alice" } ] }, "note": "doneAt drives the grace-period animation (the check fills immediately, the row stays visible ~750ms, then tucks into Completed — the Reminders-style completion flourish, no countdown ring). No chat projection for done/reopen/remove (only add shows as an action line)." }, { "name": "todo_reopen", "description": "Uncheck a to-do (mark as not done, clear the doneAt timestamp).", "event": { "type": "todo.reopen", "id": "todo-1" }, "initial_state": { "items": [ { "id": "todo-1", "text": "buy milk", "done": true, "doneAt": 1700000000000, "by": "Alice" } ] }, "expected_state_update": { "items": [ { "id": "todo-1", "text": "buy milk", "done": false, "doneAt": 0, "by": "Alice" } ] } }, { "name": "todo_remove", "description": "Delete a to-do item permanently.", "event": { "type": "todo.remove", "id": "todo-1" }, "initial_state": { "items": [ { "id": "todo-1", "text": "buy milk", "done": false, "doneAt": 0, "by": "Alice" }, { "id": "todo-2", "text": "call mum", "done": false, "doneAt": 0, "by": "Alice" } ] }, "expected_state_update": { "items": [ { "id": "todo-2", "text": "call mum", "done": false, "doneAt": 0, "by": "Alice" } ] } }, { "name": "todo_move_after", "description": "Reorder: move an item to immediately follow `after` in the array (drag-to-reorder).", "event": { "type": "todo.move", "id": "todo-1", "after": "todo-2" }, "initial_state": { "items": [ { "id": "todo-1", "text": "buy milk", "done": false, "doneAt": 0, "by": "Alice" }, { "id": "todo-2", "text": "call mum", "done": false, "doneAt": 0, "by": "Alice" }, { "id": "todo-3", "text": "water plants", "done": false, "doneAt": 0, "by": "Alice" } ] }, "expected_state_update": { "items": [ { "id": "todo-2", "text": "call mum", "done": false, "doneAt": 0, "by": "Alice" }, { "id": "todo-1", "text": "buy milk", "done": false, "doneAt": 0, "by": "Alice" }, { "id": "todo-3", "text": "water plants", "done": false, "doneAt": 0, "by": "Alice" } ] }, "note": "No chat projection — reorders are silent, like done/remove. The list renders newest-first (_index desc), so 'after' anchors in ARRAY order: following todo-2 in the array displays ABOVE it." }, { "name": "todo_move_to_start", "description": "Reorder: an empty `after` moves the item to the array's start (the visual bottom of a newest-first list).", "event": { "type": "todo.move", "id": "todo-3", "after": "" }, "initial_state": { "items": [ { "id": "todo-1", "text": "buy milk", "done": false, "doneAt": 0, "by": "Alice" }, { "id": "todo-2", "text": "call mum", "done": false, "doneAt": 0, "by": "Alice" }, { "id": "todo-3", "text": "water plants", "done": false, "doneAt": 0, "by": "Alice" } ] }, "expected_state_update": { "items": [ { "id": "todo-3", "text": "water plants", "done": false, "doneAt": 0, "by": "Alice" }, { "id": "todo-1", "text": "buy milk", "done": false, "doneAt": 0, "by": "Alice" }, { "id": "todo-2", "text": "call mum", "done": false, "doneAt": 0, "by": "Alice" } ] } }, { "name": "todo_move_unknown_anchor", "description": "Reorder resilience: moving after an id that no longer exists (removed concurrently) is a no-op.", "event": { "type": "todo.move", "id": "todo-1", "after": "todo-gone" }, "initial_state": { "items": [ { "id": "todo-1", "text": "buy milk", "done": false, "doneAt": 0, "by": "Alice" }, { "id": "todo-2", "text": "call mum", "done": false, "doneAt": 0, "by": "Alice" } ] }, "expected_state_update": { "items": [ { "id": "todo-1", "text": "buy milk", "done": false, "doneAt": 0, "by": "Alice" }, { "id": "todo-2", "text": "call mum", "done": false, "doneAt": 0, "by": "Alice" } ] } }, { "name": "todo_grace_period_partition", "description": "Grace period: active (not done or recently checked within 750ms) vs. archived (done + >750ms ago).", "note": "This is derived in render from Date.now() and doneAt, never by a timer mutating state. Native should implement the same partitioning logic: a done item with doneAt > (now - 750) stays in 'active', others move to 'Completed' section.", "constants": { "TODO_GRACE_MS": 750 } } ] }