docs(phase-09): complete phase execution
This commit is contained in:
@@ -27,7 +27,7 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
|
|||||||
|
|
||||||
- [x] **Phase 7: Mobile Test Harness** - Mobile-emulated, authenticated PWA browser harness so the assistant (and CI) can catch mobile-only defects (completed 2026-06-11)
|
- [x] **Phase 7: Mobile Test Harness** - Mobile-emulated, authenticated PWA browser harness so the assistant (and CI) can catch mobile-only defects (completed 2026-06-11)
|
||||||
- [x] **Phase 8: Gitea CI** - Full regression on PR to main (lint/typecheck/unit/API-integration vs a MariaDB service container **+ the Phase 7 mobile harness as a UI-regression step against a CI-hosted dev stack**) + Docker image publish on merge (completed 2026-06-11)
|
- [x] **Phase 8: Gitea CI** - Full regression on PR to main (lint/typecheck/unit/API-integration vs a MariaDB service container **+ the Phase 7 mobile harness as a UI-regression step against a CI-hosted dev stack**) + Docker image publish on merge (completed 2026-06-11)
|
||||||
- [ ] **Phase 9: Faster Write-Back** - Event-driven outbox drain so edits land in ~1-2s instead of ~15s, preserving every outbox durability guarantee
|
- [x] **Phase 9: Faster Write-Back** - Event-driven outbox drain so edits land in ~1-2s instead of ~15s, preserving every outbox durability guarantee (completed 2026-06-12)
|
||||||
- [ ] **Phase 10: Admin Role & Settings** - DB foundation (is_admin / reminder_lead / app_config) + role-gated admin UI to rotate app passwords and designate the shared calendar
|
- [ ] **Phase 10: Admin Role & Settings** - DB foundation (is_admin / reminder_lead / app_config) + role-gated admin UI to rotate app passwords and designate the shared calendar
|
||||||
- [ ] **Phase 11: Per-Event Reminders** - Reminder selector on the event form (incl. "None") serialized as VALARM, with a variable-lead scheduler that honors each event's choice
|
- [ ] **Phase 11: Per-Event Reminders** - Reminder selector on the event form (incl. "None") serialized as VALARM, with a variable-lead scheduler that honors each event's choice
|
||||||
- [ ] **Phase 12: Initial Setup Wizard** - First-run validated bootstrap of env/VAPID/DB/OIDC + first app password, reusing the admin route surface
|
- [ ] **Phase 12: Initial Setup Wizard** - First-run validated bootstrap of env/VAPID/DB/OIDC + first app password, reusing the admin route surface
|
||||||
@@ -145,7 +145,7 @@ Plans:
|
|||||||
|
|
||||||
**Wave 2** *(blocked on Wave 1 completion)*
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
|
|
||||||
- [ ] 09-02-PLAN.md — Four post-commit signalOutboxDrain() publish sites in events.ts (create / edit-as-move-after-transaction / same-cal update / delete) + initOutboxTrigger() startup wiring under isMainModule() in index.ts (Wave 2)
|
- [x] 09-02-PLAN.md — Four post-commit signalOutboxDrain() publish sites in events.ts (create / edit-as-move-after-transaction / same-cal update / delete) + initOutboxTrigger() startup wiring under isMainModule() in index.ts (Wave 2)
|
||||||
|
|
||||||
### Phase 10: Admin Role & Settings
|
### Phase 10: Admin Role & Settings
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ Plans:
|
|||||||
| 6. UX Polish | v1.0 | 6/6 | Complete | 2026-06-10 |
|
| 6. UX Polish | v1.0 | 6/6 | Complete | 2026-06-10 |
|
||||||
| 7. Mobile Test Harness | v1.1 | 4/4 | Complete | 2026-06-11 |
|
| 7. Mobile Test Harness | v1.1 | 4/4 | Complete | 2026-06-11 |
|
||||||
| 8. Gitea CI | v1.1 | 4/4 | Complete | 2026-06-11 |
|
| 8. Gitea CI | v1.1 | 4/4 | Complete | 2026-06-11 |
|
||||||
| 9. Faster Write-Back | v1.1 | 1/2 | In Progress| |
|
| 9. Faster Write-Back | v1.1 | 2/2 | Complete | 2026-06-12 |
|
||||||
| 10. Admin Role & Settings | v1.1 | 0/? | Not started | - |
|
| 10. Admin Role & Settings | v1.1 | 0/? | Not started | - |
|
||||||
| 11. Per-Event Reminders | v1.1 | 0/? | Not started | - |
|
| 11. Per-Event Reminders | v1.1 | 0/? | Not started | - |
|
||||||
| 12. Initial Setup Wizard | v1.1 | 0/? | Not started | - |
|
| 12. Initial Setup Wizard | v1.1 | 0/? | Not started | - |
|
||||||
@@ -331,7 +331,7 @@ Plans:
|
|||||||
|
|
||||||
**Goal:** [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern.
|
**Goal:** [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern.
|
||||||
**Requirements:** TBD
|
**Requirements:** TBD
|
||||||
**Plans:** 1/2 plans executed
|
**Plans:** 2/2 plans complete
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
|
|
||||||
|
|||||||
+10
-9
@@ -4,14 +4,14 @@ milestone: v1.1
|
|||||||
milestone_name: Operability & Polish
|
milestone_name: Operability & Polish
|
||||||
status: executing
|
status: executing
|
||||||
stopped_at: Phase 9 context gathered
|
stopped_at: Phase 9 context gathered
|
||||||
last_updated: "2026-06-12T20:54:10.373Z"
|
last_updated: "2026-06-12T21:08:34.135Z"
|
||||||
last_activity: 2026-06-12 -- Phase 09 execution started
|
last_activity: 2026-06-12
|
||||||
progress:
|
progress:
|
||||||
total_phases: 18
|
total_phases: 18
|
||||||
completed_phases: 5
|
completed_phases: 6
|
||||||
total_plans: 17
|
total_plans: 17
|
||||||
completed_plans: 16
|
completed_plans: 17
|
||||||
percent: 28
|
percent: 33
|
||||||
---
|
---
|
||||||
|
|
||||||
# Project State
|
# Project State
|
||||||
@@ -25,10 +25,10 @@ See: .planning/PROJECT.md (updated 2026-06-10)
|
|||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 09 (faster-write-back) — EXECUTING
|
Phase: 13
|
||||||
Plan: 2 of 2
|
Plan: Not started
|
||||||
Status: Ready to execute
|
Status: Ready to execute
|
||||||
Last activity: 2026-06-12 -- Phase 09 execution started
|
Last activity: 2026-06-12
|
||||||
|
|
||||||
### Deferred Checkpoint — Phase 15 Plan 15-03 Task 2 (human-action)
|
### Deferred Checkpoint — Phase 15 Plan 15-03 Task 2 (human-action)
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ Resume: after the operator completes the change, re-run `/gsd-execute-phase 15`
|
|||||||
|
|
||||||
**Velocity:**
|
**Velocity:**
|
||||||
|
|
||||||
- Total plans completed: 31
|
- Total plans completed: 33
|
||||||
- Average duration: -
|
- Average duration: -
|
||||||
- Total execution time: 0 hours
|
- Total execution time: 0 hours
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ Resume: after the operator completes the change, re-run `/gsd-execute-phase 15`
|
|||||||
| 13 | 3 | - | - |
|
| 13 | 3 | - | - |
|
||||||
| 14 | 1 | - | - |
|
| 14 | 1 | - | - |
|
||||||
| 15 | 3 | - | - |
|
| 15 | 3 | - | - |
|
||||||
|
| 09 | 2 | - | - |
|
||||||
|
|
||||||
**Recent Trend:**
|
**Recent Trend:**
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
phase: 09-faster-write-back
|
||||||
|
verified: 2026-06-12T17:07:00Z
|
||||||
|
status: passed
|
||||||
|
score: 5/5 must-haves verified
|
||||||
|
overrides_applied: 0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 09: Faster Write-Back Verification Report
|
||||||
|
|
||||||
|
**Phase Goal:** A created, edited, or deleted event reaches Fastmail within ~1-2 seconds (event-driven outbox drain) instead of waiting up to ~15s for the next interval tick — with every existing durability guarantee intact.
|
||||||
|
**Verified:** 2026-06-12T17:07:00Z
|
||||||
|
**Status:** passed
|
||||||
|
**Re-verification:** No — initial verification
|
||||||
|
|
||||||
|
## Goal Achievement
|
||||||
|
|
||||||
|
### Observable Truths
|
||||||
|
|
||||||
|
| # | Truth | Status | Evidence |
|
||||||
|
|---|-------|--------|----------|
|
||||||
|
| 1 | After creating/editing/deleting an event, the change is signalled to drain on enqueue (not waited-for on the interval) — the drain fires promptly, not on the 15s tick. | VERIFIED | `signalOutboxDrain()` called at all 4 enqueue sites in events.ts (lines 310, 434, 450, 525). Test SC-1 in outboxWorker.test.ts confirms drain fires via EventEmitter signal without advancing any timer. |
|
||||||
|
| 2 | The route handler still returns an optimistic 202 immediately and never makes a CalDAV call inline — the signal is fire-and-forget (never awaited). | VERIFIED | No `await signalOutboxDrain` in events.ts. All 4 sites call `signalOutboxDrain();` then immediately `return c.json(..., 202)`. events.test.ts (23 tests) confirms 202 + no-inline-CalDAV contract still holds. |
|
||||||
|
| 3 | Edit-as-move still writes the new event before deleting the old one (create-before-delete preserved); the signal fires AFTER the db.transaction resolves, never inside it. | VERIFIED | events.ts line 410 opens `await db.transaction(async (tx) => { ... });` closing at line 432. `signalOutboxDrain()` appears at line 434 — after the `await db.transaction(...)` statement, not inside the callback. Confirmed: no `signalOutboxDrain` between the two `tx.insert` calls. |
|
||||||
|
| 4 | No duplicate CalDAV PUTs for the same outbox row when signal and the 15s fallback overlap (exactly-once per uid preserved via the isDraining guard + drainRequested trailing-re-drain). | VERIFIED | `scheduleOutboxDrain` checks `if (isDraining) { drainRequested = true; return; }` before calling `runOutboxDrain()`. Test D-07 (Test C) verifies two concurrent `scheduleOutboxDrain()` calls result in `createCalendarEvent` called exactly once. Test D-05 (Test B) verifies mid-drain signals collapse to exactly one trailing re-drain. |
|
||||||
|
| 5 | The 15s setInterval fallback still runs and recovers missed rows. | VERIFIED | `startOutboxWorker` in outboxWorker.ts line 857: `setInterval(() => { scheduleOutboxDrain(); }, 15 * 1000)` — interval length unchanged, body routes through `scheduleOutboxDrain` wrapper. |
|
||||||
|
|
||||||
|
**Score:** 5/5 truths verified
|
||||||
|
|
||||||
|
### Required Artifacts
|
||||||
|
|
||||||
|
| Artifact | Expected | Status | Details |
|
||||||
|
|----------|----------|--------|---------|
|
||||||
|
| `apps/api/src/lib/outboxTrigger.ts` | Zero-dependency EventEmitter signal (signalOutboxDrain, onOutboxDrain) | VERIFIED | Exists. Imports only `node:events`. Exports `signalOutboxDrain` and `onOutboxDrain`. No `setMaxListeners`. No internal project imports. |
|
||||||
|
| `apps/api/src/broker/outboxWorker.ts` | scheduleOutboxDrain + drainRequested + initOutboxTrigger | VERIFIED | Exports `scheduleOutboxDrain`, `initOutboxTrigger`. Declares `let drainRequested = false` at line 172. `drainRequested = false` reset precedes recursive `scheduleOutboxDrain()` call (line 199 before 200 — Pitfall 3 correct). |
|
||||||
|
| `apps/api/src/routes/events.ts` | Four post-commit signalOutboxDrain() publish sites | VERIFIED | Exactly 4 calls (lines 310, 434, 450, 525). Import at line 37. None awaited. None inside transaction callback or catch blocks. |
|
||||||
|
| `apps/api/src/index.ts` | initOutboxTrigger() wired under isMainModule(), after startOutboxWorker() | VERIFIED | Line 138: `startOutboxWorker()`. Line 139: `initOutboxTrigger(); // subscribe drain signal listener (D-01)`. Both inside the `if (isMainModule())` block. |
|
||||||
|
| `apps/api/tests/broker/outboxWorker.test.ts` | Trigger-wiring test block (SC-1, D-05, D-07) — 3 new tests | VERIFIED | `describe('scheduleOutboxDrain — trigger wiring (D-09)', ...)` block at line 747 with `beforeAll(() => { initOutboxTrigger(); })` and 3 tests. All 30 outboxWorker tests pass. |
|
||||||
|
|
||||||
|
### Key Link Verification
|
||||||
|
|
||||||
|
| From | To | Via | Status | Details |
|
||||||
|
|------|----|-----|--------|---------|
|
||||||
|
| `outboxWorker.ts (initOutboxTrigger)` | `outboxTrigger.ts (onOutboxDrain)` | `onOutboxDrain(() => scheduleOutboxDrain())` | WIRED | Line 845: `onOutboxDrain(() => scheduleOutboxDrain())`. Import at line 39. |
|
||||||
|
| `outboxWorker.ts (scheduleOutboxDrain)` | `outboxWorker.ts (runOutboxDrain)` | isDraining guard + drainRequested trailing-re-drain | WIRED | Lines 187-203: guard checks `isDraining`, calls `runOutboxDrain()`, finally resets `drainRequested` before recursive call. |
|
||||||
|
| `routes/events.ts` | `outboxTrigger.ts (signalOutboxDrain)` | import + call after each enqueue commit | WIRED | Import at line 37. 4 call sites verified present and not awaited. |
|
||||||
|
| `index.ts (isMainModule block)` | `outboxWorker.ts (initOutboxTrigger)` | `initOutboxTrigger()` after `startOutboxWorker()` | WIRED | Lines 138-139 inside `isMainModule()` guard. |
|
||||||
|
| `startOutboxWorker setInterval` | `scheduleOutboxDrain` | bare `scheduleOutboxDrain()` call in 15s interval | WIRED | Lines 857-859: `setInterval(() => { scheduleOutboxDrain(); }, 15 * 1000)`. |
|
||||||
|
|
||||||
|
### Data-Flow Trace (Level 4)
|
||||||
|
|
||||||
|
Not applicable — phase adds a signal/trigger path, not a data-rendering path. The signal carries no payload; it triggers an existing drain that queries the DB for pending rows. The drain's data path (outbox rows → CalDAV) is unchanged and was verified in prior phases.
|
||||||
|
|
||||||
|
### Behavioral Spot-Checks
|
||||||
|
|
||||||
|
| Behavior | Command | Result | Status |
|
||||||
|
|----------|---------|--------|--------|
|
||||||
|
| SC-1: signalOutboxDrain() triggers drain without timer advance | `npx vitest run tests/broker/outboxWorker.test.ts` — Test A | 30/30 passed | PASS |
|
||||||
|
| D-05: mid-drain signals collapse to one trailing re-drain | Test B in same run | 30/30 passed | PASS |
|
||||||
|
| D-07: concurrent scheduleOutboxDrain() calls are exactly-once | Test C in same run | 30/30 passed | PASS |
|
||||||
|
| 202 + no-inline-CalDAV route contract preserved | `npx vitest run tests/routes/events.test.ts` | 23/23 passed | PASS |
|
||||||
|
| tsc clean across all phase-9 files | `npx tsc --noEmit` | No output (clean) | PASS |
|
||||||
|
|
||||||
|
### Probe Execution
|
||||||
|
|
||||||
|
No conventional `scripts/*/tests/probe-*.sh` probes declared or found for this phase.
|
||||||
|
|
||||||
|
### Requirements Coverage
|
||||||
|
|
||||||
|
| Requirement | Source Plan | Description | Status | Evidence |
|
||||||
|
|-------------|------------|-------------|--------|----------|
|
||||||
|
| CAL-15 | 09-01-PLAN.md, 09-02-PLAN.md | Event reaches Fastmail within ~2s (event-driven drain) instead of up to ~15s, while preserving optimistic-202 and all outbox durability guarantees. | SATISFIED | `signalOutboxDrain()` fires immediately post-enqueue. SC-1 test confirms no timer advance needed. isDraining guard + drainRequested preserve exactly-once. 15s fallback intact. REQUIREMENTS.md marks CAL-15 as Complete. |
|
||||||
|
|
||||||
|
### Anti-Patterns Found
|
||||||
|
|
||||||
|
No debt markers (TBD/FIXME/XXX/TODO/HACK/PLACEHOLDER) found in any phase-9 modified file. No stub patterns found. No `return null`, empty handlers, or hardcoded empty data in the signal path.
|
||||||
|
|
||||||
|
### Human Verification Required
|
||||||
|
|
||||||
|
None. The phase goal (latency reduction via event-driven drain) is fully verifiable through automated tests and static code inspection:
|
||||||
|
|
||||||
|
- SC-1 (no 15s wait): automated by the trigger-wiring Vitest test using `setImmediate` flush — no stopwatch needed.
|
||||||
|
- D-04 (fire-and-forget): verified by static grep — no `await signalOutboxDrain` anywhere.
|
||||||
|
- D-03 (signal after transaction): verified by static line-number inspection — `signalOutboxDrain()` at line 434 is after `await db.transaction(...)` closes at line 432.
|
||||||
|
- Fallback interval: verified by reading the `startOutboxWorker` body.
|
||||||
|
|
||||||
|
### Gaps Summary
|
||||||
|
|
||||||
|
No gaps. All 5 must-have truths are VERIFIED with direct codebase evidence:
|
||||||
|
|
||||||
|
1. All 4 enqueue sites in events.ts call `signalOutboxDrain()` post-commit, pre-202.
|
||||||
|
2. No call is awaited; the signal is synchronous EventEmitter emit.
|
||||||
|
3. The edit-as-move signal is provably outside the transaction callback at the source level.
|
||||||
|
4. The `isDraining` guard + `drainRequested` boolean collapses concurrent/mid-drain signals correctly; verified by 2 dedicated automated tests.
|
||||||
|
5. The 15s setInterval fallback routes through `scheduleOutboxDrain()` at the same 15-second cadence.
|
||||||
|
|
||||||
|
CAL-15 is marked Complete in REQUIREMENTS.md (line 78) and all its behavioral guarantees are implemented and tested.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Verified: 2026-06-12T17:07:00Z_
|
||||||
|
_Verifier: Claude (gsd-verifier)_
|
||||||
Reference in New Issue
Block a user