# Implementation Plan

**Spec ID:** 051
**Feature:** Scheduling Onboarding & Activation
**Companion docs:** [product-requirements.md](./product-requirements.md) · [solution-design.md](./solution-design.md)
**Status:** ✅ IMPLEMENTATION COMPLETE + BUSINESS-ACCEPTED 2026-07-22 — PR #79 open; launch via `userfrosting/docs/runbooks/scheduling-free-launch.md`

## Validation Checklist

- [x] Every phase has a goal, risk pointer (risk register), and formal DoD
- [x] Every SDD component appears in exactly one phase (producer completeness audited at T7)
- [x] TDD ordering explicit: failing test → smallest implementation → integration → refactor, per slice
- [x] Dependencies corrected (Q5→T0; schema before detectors; contract freeze before T3/T4 branch; spike before walkthrough)
- [x] Oversized tasks split (detectors, jobs, endpoints, producers, runbook)
- [x] Criterion-to-test matrix covers every M/S/NFR ID (S2–S5 explicitly deferred)
- [x] Per-detector query budget table included (SDD NFR-1 requirement)
- [x] Risk register with owners, triggers, contingencies
- [x] Test-store strategy solves the latched-pilot problem (fixture stores + reset workflow)
- [x] Zero behavior change until flags flip

## Specification Compliance Guidelines

### How to Ensure Specification Adherence
- The PRD Step Contract Table, SDD route matrix, interface contracts, and event producer matrix are **binding**. Re-read the referenced IDs before each slice.
- Every task names its acceptance IDs; its proving test must reference them.

### Deviation Protocol
1. Stop; write the deviation entry (spec said / reality is / resolution).
2. If a PRD criterion changes, amend the PRD with a dated note.
3. Continue only after the entry exists.

## Metadata Reference

| Key | Value |
|---|---|
| Flags (all default false) | `SCHEDULING_FREE`, `SCHEDULING_FREE_PILOT_STORES`, `ONBOARDING_HUB_ENABLED`, `ONBOARDING_INVITES_ENABLED`, `ONBOARDING_WALKTHROUGH_ENABLED` |
| New namespaces | `BuyerKiosk\Onboarding\`, `BuyerKiosk\Scheduling\Onboarding\` |
| Migrations | `20260721_051_001..004` (all land dark in T1B) + baseline snapshot script |
| Test filters | `--filter "Onboarding|Invite|Entitlement|Activation"` |
| Test stores | pc00 = stable pilot; `ob01`–`ob03` = disposable fixture stores with reset workflow (T0.7) |

## Phase DoD Template (applies to every phase; referenced as "DoD")

A phase is DONE only when all of the following hold, recorded in this file's phase summary:
1. Named automated tests green; PHPStan clean on all touched files.
2. TDD evidence: test commits/files predate implementation within each slice (or a Deviation explains why not).
3. Migrations (if any) run twice on a fresh dev DB — second run applies nothing; postconditions spot-checked.
4. Flag-off AND flag-on behavior verified for the phase's surfaces.
5. Integration artifact attached where the phase requires one (screenshot, event trace, batch report).
6. External-dependency evidence linked (e.g., mobile doc published, Q-decision recorded).
7. No unresolved blocking Deviation.
8. Phase summary appended with sign-off and the next phase's entry conditions confirmed.

## Per-Detector Query Budget (NFR-1; regression-tested at T2)

| Detector | Central queries | Store queries | Index relied on | Budget |
|---|---|---|---|---|
| Activation | 0 (stores row already loaded) | 0 | — | 0 |
| StoreSettings | 0 (stores columns) | 0 | — | 0 |
| Positions | 0 | 1 (COUNT positions) | PK | ≤50ms |
| Roster | 1 (users⋈userStoreAssignments agg) | 0 | usa (typeNum,isActive) | ≤50ms |
| Invites | 1 (userInvites⋈inviteMessages agg) | 0 | (typeNum,status) | ≤50ms |
| AppAdoption | 2 (deviceTokens agg; refreshTokens agg) | 0 | (userId,appId,updatedAt) | ≤50ms ea |
| Availability | 0 | 1 (per-member agg, single query) | employeeId idx | ≤50ms |
| PublishedWeek | 0 | 1 | week/publish idx | ≤50ms |
| ShiftTasks | 0 | 1 | shiftId idx | ≤50ms |
| FirstClockIn | 0 | 1 | clockIn idx | ≤50ms |
| **Total** | **≤4** | **≤5** | 2 connections | hub p95 ≤2s |

A query-count regression test asserts `getState` issues ≤10 queries (instrumented PDO).

## Implementation Phases

Graph: **T0 → (T1A ∥ T1B) → T2 → (T3 ∥ T4 ∥ T5 ∥ T6) → T7 integration gate → T8 launch readiness.**

---

### T0 — Decisions, contracts, and spikes (blocking prerequisites)

**Goal:** Every decision and contract that shapes downstream code is closed before code is written.
**Risk register:** R10, R11.

- [x] **T0.1 Q5 decision — Team Chat entitlement** `[BLOCKING T1A]` — product decides free vs stays-premium; recorded in PRD Decisions Log. T1A implements whichever branch; chat regression tests assert the decided state. → **D-8: stays premium in v1** (conservative default; override window before T1A merge). *[Historical: product OVERRODE this at the T0→T1 gate — chat goes FREE with scheduling; implemented in T1A per the ratified D-8.]*
- [x] **T0.2 Q3 strategy — invite email sender** `[BLOCKING T3 transport acceptance]` — sender address/domain, SMTP vs upgrade, DNS (SPF/DKIM) owner + verification plan. Decision recorded; deliverability *verification* stays a T8 gate. → **D-9: existing PHPMailer/SMTP path, `noreply@buyerkiosk.com`, adapter-swappable** (SPF incl. sendgrid.com verified; DMARC p=none; DKIM = T8.6 gate).
- [x] **T0.3 Q4 timing lock** — thresholds + ticket-taxonomy sign-off scheduled so it lands **before T7's acceptance fixtures** (entry condition of T7, not T8). → **D-10: PRD defaults signed off 2026-07-21** (well ahead of T7); taxonomy = manual `scheduling-setup` tagging + CSV baseline import.
- [x] **T0.4 driver.js spike (DR-9)** `[BLOCKING T5]` — vendored lib over the real calendar: Syncfusion z-index/popups, CSP, keyboard, resize. Record verdict. **Regardless of outcome:** the checklist step's fallback (deep link + support article) is built and browser-tested in T5; article authored + URL recorded here. `[ref: M11-08]` → **GO-WITH-CAVEATS** (`spikes/driver-js-spike.md`, driver.js 1.8.0 vendored): overlay pointer-events blocks non-highlighted Syncfusion popups — T5 engine must destroy/re-drive the overlay around native interactions (fits ADR-051-4); add `aria-live` for NFR-4; resize re-verify at T5.5. **Article live:** `/pc00/support/article/build-your-first-schedule/` (row id=117).
- [x] **T0.5 Mobile contract handshake** — publish the `clientId` login-field ask + entitlement-flag note to both apps' backend-api-updates docs; owners + min-version targets recorded. `[ref: M1-03, ADR-051-7]` → **Partially complete — asks drafted 2026-07-21, replies pending** (owners/min-versions TBD is an outstanding item, non-blocking for T1A–T3; launch-gated only for full M10 auto-detection). Tracking table in `contracts/mobile-contract.md`. Post-review corrections: BOTH login endpoints documented (Team+ MobileScheduling path, Live MobileApi path); Live refresh rotation drops clientId — fix added to T4.1 scope.
- [x] **T0.6 Campaign/alert contract** — `campaignId` config key, campaign open/close mechanics, alert CTA URL (resolver route), `source=alert` propagation rule through resolver/picker/302s (must survive to `hub_viewed`). → `contracts/campaign-alert-contract.md`: `ONBOARDING_CAMPAIGN_ID` env (unset = safe/no campaign), CTA `/admin/schedule/get-started?source=alert`, source whitelist {alert,nav,direct}, campaignId stamped server-side only. System Alerts CTA verified to accept relative URLs + query strings (server `validateCtaUrl` + both client renderers).
- [x] **T0.7 Test-store strategy** — create fixture stores `ob01`–`ob03` (dev-only) + `scripts/onboarding-reset-store.php <typeNum> --confirm` (dev-guarded: refuses when store not in fixture list; archives then clears 051 progress/observation/event/invite rows by run ID). Solves the latched-pc00 problem (R11); pc00 stays as the stable "grandfathered" fixture. → ob01=387/ob02=388/ob03=389 (dev=1, provider=none, premium=none; schema-only clones of kiosk_pc00); reset script live with 4 guards, PHPStan-clean, all guard tests green; docs in `contracts/fixture-stores.md`. Re-test archive path after T1B lands tables.
- [x] **T0.8 DoD:** decisions recorded in PRD/README; spike verdict + article URL written; fixture stores exist; reset script tested. → Complete 2026-07-21; phase summary below.

---

### T1A — Entitlement foundation (test-first; dark) `[parallel-with: T1B]`

**Goal:** Predicates + call-site swaps live; flags off ⇒ byte-identical behavior.
**Risk register:** R9.

- [x] **T1A.1 Failing tests first** `[activity: qa_expert]` — entitlement flag matrix (unset/false/true × pilot/not × premium states) `[ref: S6]`; AccessPolicy route matrix incl. unrecognized-provider `[ref: M4-03]`; chat state guard per T0.1 decision `[ref: M1-07]`; mobile flag payload `[ref: M1-03]`; trial-endpoint 409 `[ref: M1-04]`. → 7 test files, red-state evidence recorded per slice before implementation.
- [x] **T1A.2 Implement to green** — `SchedulingEntitlement` (env pattern, pilot list, memo); `isChatEntitled()`; `SchedulingAccessPolicy`; Twig fns (real + fallback ext). → `isChatEntitled()` delegates to `isEntitled()` per D-8-as-ratified (chat free); pure `SchedulingAccessDecision` value object; fallback ext never blanket-false.
- [x] **T1A.3 Call-site swaps** — SchedulingPageController (302-to-get-started returns legacy while hub flag off); sidebar 319 + 207-214; calendar 38/4728; trial-banner suppression; mobile flag builder; trial endpoints 409; delete `PremiumGateMiddleware`. → All swapped; middleware deleted after repo-wide wiring grep (test-only references; test surgery preserved the behavioral guarantees against the real gate).
- [x] **T1A.4 DoD** + manual smoke: flags unset = pre-051 behavior; pilot-listed store reaches calendar. → 245/245 targeted, 1609/1609 module surface, full-suite failures match pre-existing baseline; PHPStan clean (1 pre-existing MobileApiController baseline mismatch, stash-verified); Chrome smoke both states + trial 409 live; `.env` restored MD5-identical.

### T1B — Dark schema foundation (ALL migrations) `[parallel-with: T1A]`

**Goal:** Every 051 table/index exists before any consumer — no phase later blocks on schema.
**Risk register:** R3.

- [x] T1B.1 `051_001_onboarding_tables.json` (progress, observations, userProgress, eventLog w/ dedupeKey uk, campaignCohort). → 5 tables in kiosk_buykiosk; eventLog.typeNum NULLable per post-review amendment.
- [x] T1B.2 `051_002_user_invites.json` (userInvites isCurrent-uk; inviteMessages outbox fields, provider-qualified uk, messageRef uk, (state,nextAttemptAt) idx). → kiosk_users; userInvites FKs RESTRICT (T6.5 privacy path must handle explicitly); inviteMessages FK CASCADE.
- [x] T1B.3 `051_003_sms_category_enums.json` — extend the three ENUM columns + `SmsCategory::SCHEDULING_INVITE` constant + ALL + labels. → **Deviation (recorded):** no migration seeds default config rows for ANY category — platform defaults live in `BillingConfigService::DEFAULT_SMS_CATEGORY_CONFIG`; `scheduling_invite` added there (billable=false, matching transactional categories). Bonus: flagged pre-existing conductor bug — `20260211_037_001` uses unimplemented op type `modify_column`; 051_003 uses proven `alter_table`.
- [x] T1B.4 `051_004_device_token_uk_appid.json` — uk → (userId,deviceId,appId), archive-first CTAS; idx (userId,appId,updatedAt); `DeviceTokenRepository` upsert + unit test. → Conflict rows provably zero (old uk ⊂ new uk) — archive pattern implemented anyway per CON-9; LIVE upsert proof: team+live rows coexist on one device, targeted updates.
- [x] T1B.5 Migration tests: run-twice idempotency ×4 (14/14 applied then 14/14 skipped); postcondition SHOW CREATE ×8; enum insert round-trip in all three tables; reset script re-run on ob01 now hits real central tables (T0 loose end closed). Targeted runner kept: `scripts/apply-051-t1b-migrations.php`.
- [x] T1B.6 DoD. → 55/55 new+adjacent tests; 336/336 MobileScheduling; 4 Billing failures stash-proven pre-existing; PHPStan clean on all touched files.

---

### T2 — Onboarding framework + hub (behind `ONBOARDING_HUB_ENABLED`)

**Goal:** Full self-detecting checklist on fixture stores; hub-side event producers live; endpoint security matrix green.
**Risk register:** R7.
**Entry:** T1A + T1B done.

- [x] **T2.1 Framework core (tests first per slice)** → DONE (105 tests; interfaces frozen; convergence pass added healthCheck modes + batched observation upserts + UTC anchoring) — models/repos (progress insert-only, observation upsert, userProgress incl. `inProgress`); `FlowDefinition` + `SchedulingFlowDefinition` (Step Contract Table 1:1 + starter catalog + thresholds); `OnboardingEventLogger` (dedupe-guarded, same-tx helper); `OnboardingService::getState` (terminal → detectors → observation upsert → prereq graph → first-transition tx → stale fallback).
  - Slice tests: FlowDefinition graph (prereqs, zero-denominator blocked, exact-fraction thresholds) `[ref: M8-03]`; event dedupe under concurrency; evidence permission-neutrality.
- [x] **T2.2 Detector slices (test-first each)** `[parallel: true]` → ALL DONE, 4 parallel agents, red→green evidence each:
  - [x] T2.2a Config family (38 tests; 2-query family budget proven; storeRow shared memo; grandfathered evidence flag added in convergence).
  - [x] T2.2b Roster family (24 tests; owner=usa.role 1; hourlyRate/hoursRequested; canLogin+active; inviteMessages join correctly dropped; missing usa(typeNum,isActive) composite index → T8.3).
  - [x] T2.2c Usage family (31 tests; contract-exact FirstClockIn boundary proven; shared ActivationBoundaryResolver).
  - [x] T2.2d AppAdoption (16 tests; 3 queries — population+2 signals, deviation noted; memberLadder() direct-call for API; caught PHP-Chicago-vs-MySQL-UTC freshness bug; oauthRefreshTokens (userId,clientId,lastUsedAt) index finding → T8.3).
  - [x] T2.2e Query-count regression: after convergence optimizations (batched upserts, terminal-row memo reuse) raw getState = **exactly 10** — budget met; EXPLAIN confirms T1B index used for device tokens.
- [x] **T2.3 Hub API slices (tests first: table-driven security matrix per endpoint — unauth / wrong store / inactive assignment / missing permission / missing CSRF / invalid input / TypeError / error envelope+requestId / no member-existence leakage)** → DONE: 8 endpoints, 56-case security matrix red→green, activation tx-atomicity proven on real DB (fault-injecting PDO), routes proven live; post-E2E fix: activation now clears the Redis store cache (stale-cache trap caught live in T2.7) — state; activate (200-idempotent/409-conflict/audit); confirm; complete; skip; starter-positions (case-insensitive dedupe) `[ref: M7-02]`; walkthrough-state (**T2 owns repository+controller+contract tests; T5 only integrates**); switch-interest (+ proof provider state never mutated `[ref: M5-03]`).
- [x] **T2.4 Hub pages/UI** → DONE (pitch/checklist/M5 views, resolver+picker w/ source=alert preserved through 302s — browser-verified; OnboardingHub.js 5-min poll + evidence/stale/health/canAccess rendering; Setup nav via isOnboardingHubEnabled twig fn; Jest harness found — 14 JS tests; 17 controller tests) — get-started views (pitch/checklist/M5-external incl. switching-impact content + support handoff), resolver + picker (`source=alert` preserved per T0.6), `OnboardingHub.js` (5-min poll, evidence display, permission-sensitive wage rendering vs sensitive-field gate, "requires owner/admin" inaccessible-step rendering, health indicators), persistent compact "Setup" nav entry + completed-state rendering `[ref: D-4]`, M6 confirm UI (overnight/24h hours + half-hour timezones accepted; later timezone change → health degradation test) `[ref: M6-04]`, M12 two-path explanation + per-member status + manual-complete UI `[ref: M12-01/02]`, M10 step UI (app-store links, threshold conjunction display, manual complete, inline resend) `[ref: M10-01/06/07]`, S1 optional tasks step UI + skip.
- [x] **T2.5 Hub-side event producers + property contracts** → DONE (11 property-contract tests: dedupeKeys exact per matrix, typeNum/flowVersion/campaignId incl. invalid-slug→null, tx atomicity real-DB).
- [x] **T2.6 Scenario batteries:** S1 activation race (real concurrent processes); S2 contract 422; S3 latched+health; S4 stale (store-DB outage sim); S7 grandfathered w/ reset+repeat (R11 workflow validated) — all green on fixture stores.
- [x] **T2.7 DoD** + hub E2E artifact → LIVE E2E on ob01 (visible browser run 2026-07-22): pitch → Get Started (activated event `activated:ob01`) → checklist → M6 confirm (manual, actor recorded) → starter-position chip → positions self-detected complete. Screenshot + GIF (`051-hub-e2e-activate-checklist.gif`) + event trace captured (hub_viewed×3, activated, step_completed×2 — dedupe exact, auto vs manual detection recorded correctly). **E2E caught a real bug, fixed same session:** activation didn't invalidate the Redis store cache → next render still served the pitch view; `SchedulingActivationService::clearStoreCache()` added (house StoreController::clearCache pattern), PHPStan clean, 7/7 tests green. Two open UI notes for review: pitch/checklist headline uses storeType ("Plato's Closet" on a fixture clone — correct for real stores, odd for fixtures); invites step rendered "requires owner/admin" for the global-admin user (M3-11 note rendering works; verify the uri_employees mapping intent).

---

### T3 — Invite delivery + activation `[parallel-with: T4, T5, T6]`

**Goal:** Durable outbox delivery; secure redemption; SMS-only members loginable.
**Risk register:** R2, R4, R5, R8.
**Entry:** T2 done; **T3/T4 contract freeze:** invite-status aggregation shape + roster status payload (invited→accountActive→appLoginObserved→pushRegistered ladder) frozen before T4 starts.

- [x] **T3.1 Outbox core (tests first: claim race, backoff, dead-letter, sweep staleness `[ref: S9]`)** — `InviteRepository` (supersede/isCurrent, monotonic transition ranking, batch queries); `InviteDeliveryService` (preview; enqueue tx; caps 200/run, 500/store/day, 3/recipient/day with **exact-boundary tests**; object-scope guard; already-active → storeNotify **including its actual delivery path** `[ref: M9-05]`; bulk idempotency on retry of the same batch request).
- [x] **T3.2 Transports** — `InviteEmailTransport` (PHPMailer/SMTP per T0.2; `scheduling-invite.html`; messageRef links; store-identified copy) `[ref: M9-10]`; `sendEmployeeInviteText` (do-not-text ENFORCED, FloodProtector, 037 category logging) `[ref: M9-09]`; SMS-unavailable → email-only degradation `[ref: M9-09]`; both-channels-default + deselection `[ref: M9-01]`.
- [x] **T3.3 Jobs (one task each)** — `InviteDeliveryJob` (guarded claim; outcome + `invite.sent` event); `InviteSweepJob` (5-min cadence; stale locks; ambiguous outcome = `state='sent', stateReason='unconfirmed'` — **not a schema state**; Redis-outage pickup); `InviteExpiryJob` (daily; expiry event); **scheduler registration + cadence verification** in TaskCommandFactory; queue selection + worker health check; retryable-vs-terminal error taxonomy documented in the job docblocks.
- [x] **T3.4 Webhook extension (tests first: S8 battery)** — signature validation, durable-before-2xx/5xx-transient, monotonic transitions, inviteMessages upsert, redaction, `invite.delivery_updated` events.
- [x] **T3.5 Endpoints + UI (slice per endpoint, security matrix applied)** — invite; invite-preview; invite-all (202/batchId/429+Retry-After); invite-batches/:id; invite-status (paginated); revoke; roster/hub UI (channel states, resend, batch poll).
- [x] **T3.6 Activation (tests first: S10 security battery + cookie attrs, session fixation, exact 10-min expiry, IP-limit 429, username collision, headers on success AND error pages, token-absent-from-logs assertion)** — `generateInvitationToken` (no email requirement) + `completeActivation`; ActivationController session exchange; `common/activate.html` + username collection + role-aware app screen; `invite.activated` producer (winningChannel via messageRef).
- [x] **T3.7 S11 SMS-only end-to-end; S5 bulk partial failure; S12 billing pipeline (usage→config→invoice line item); supersede/resend semantics.**
- [x] **T3.8 Observability** — alerts/queries for queue age, dead rows, worker absence, send-failure rate, webhook signature/write failures (delivered as launch-monitoring pack entries + log-based checks).
- [x] **T3.9 DoD** + artifacts: dev live smoke (real email + Twilio test SMS + phone-browser activation), batch report, event trace.

### T4 — Mobile adoption producers `[parallel-with: T3, T5, T6]`

**Goal:** Mobile-side signals + events live; contract published.
**Entry:** T2 done + T3/T4 contract freeze.
**Risk register:** R6.

- [x] T4.1 clientId passthrough in **BOTH** login paths (Codex T0-review finding): Team+ `/api/mobile/scheduling/auth/login` → `MobileScheduling\Controllers\MobileAuthController` AND Live `/api/mobile/auth/login` → `MobileApi\Controllers\MobileAuthController` (validate ∈ {team,live}; contract tests both payload shapes + absent field, per endpoint). **Plus:** preserve stored clientId across the Live path's refresh-token rotation (`MobileApi/MobileAuthController.php:269` currently regenerates without it → falls back 'mobile-app'); tests cover both apps, legacy rows, absent values.
- [x] T4.1b appId-scoped mobile logout (T1 review follow-through): `removeDeviceToken` currently deletes both apps' rows per (userId,deviceId); scope the DELETE by the requesting app once clientId identifies it. Test: Live logout leaves the Team+ row intact.
- [x] T4.2 `onboarding.app_adopted` producers (login clientId path + registerDeviceToken; dedupe-guarded post-commit; denied-push user with clientId login still counts test) `[ref: M10-02]`.
- [x] T4.3 Multi-store user-scoped behavior test (documented semantics `[ref: M10-04]`).
- [x] T4.4 Post-merge ladder integration test (with T3): invited → activated → app login → push registered renders correctly in invite-status + hub payload.
- [x] T4.5 Mobile docs updated with final shapes (T0.5 follow-through); released-client tolerance question tracked to an answer `[ref: M1-03]`.
- [x] T4.6 DoD.

### T5 — Walkthrough client `[parallel-with: T3, T4, T6]`

**Goal:** Event-advanced walkthrough per spike verdict; fallback shipped regardless.
**Entry:** T2 done + T0.4 spike verdict (**GO-WITH-CAVEATS — binding constraints from `spikes/driver-js-spike.md` post-review section**: T5.2 opens with an entry test proving the destroy/re-drive pattern over a real shift-editor flow incl. cancellation, focus restoration, destroy callbacks, beat-state retention, Syncfusion target re-render; required beats disable/intercept driver's default Next/ArrowRight advance — real application events only; popover content = static/trusted strings only, driver.js uses innerHTML; resize + full a11y re-verified at T5.5).
**Risk register:** R10.

- [x] T5.1 Calendar/AI/publish CustomEvent emissions (`ScheduleCalendar.js`, `ai-scheduling.js`, publish path) + emission contract tests (pure-function extractable).
- [x] T5.2 `WalkthroughEngine.js` (beats: precondition/advanceEvent/failureStates; pause/resume; per-beat skip; a11y: keyboard, focus trap, aria-live; unavailable → fallback). **Opens with the spike-constraint entry test (see phase Entry) before engine buildout.**
- [x] T5.3 `first-schedule.js` beat config (M11-02 order; M11-04 roster refusal; M11-05 auto-acknowledge; M11-06 AI retry/zero/timeout/locked; M12-04 coverage notice).
- [x] T5.4 State integration via T2's endpoint (client only — no server work here).
- [x] T5.5 Test slices: engine state machine (browser-scripted, hidden-tab rAF workaround); beat contract; browser edge cases (resize, canceled editor, concurrent managers); accessibility run; **fallback path browser test with the T0.4 article URL** `[ref: M11-08]`; S13 battery on fixture store (AI-failure via killed solver).
- [x] T5.6 DoD + walkthrough E2E artifact.

### T6 — External producers, baseline, observability `[parallel-with: T3, T4, T5]`

**Goal:** Producer matrix complete outside hub/invite/mobile scopes; M2-04 tooling real.
**Entry:** T2 done.

- [x] T6.1 One task per producer: `schedule.week_first_published` (publish endpoint, dedupe `pub:{t}:{weekStart}`, property contract); `ai.fill_generated`; `ai.fill_applied`; `onboarding.milestone_first_clock_in` (workspace + mobile paths; **at-or-after first post-activation publish** gating test).
- [x] T6.2 Baseline snapshot script: eligible cohort; positions/roster/publish-60d; **grandfathered median time-to-first-publish**; ticket-rate from the Q4-owned source (or approved manual-import CSV format if no API) — placeholder REMOVED; immutable rerun behavior (same campaignId → refuses overwrite), artifact path + checksum, expected-cohort-count sanity output, business sign-off line.
- [x] T6.3 KPI query pack: fixture-tested SQL for **every PRD KPI** (conversion, activation, completion, invite efficacy, app adoption, first publish, AI usage, durable adoption, support load) + stalled-store list; committed to `docs/runbooks/`.
- [x] T6.4 Observability: hub endpoint latency/error telemetry (Server-Timing), stale-detector counter, activation-abuse (429) counter — wired to the monitoring pack.
- [x] T6.5 Privacy/retention: eventLog 24-month retention job (TaskEngine, registered + cadence test); `userInvites`/`inviteMessages` hooked into the Privacy module deletion path — **must handle BOTH RESTRICT FK directions (`userId` invitee AND `invitedByUserId` inviter) or user deletion fails once any invite exists** (T1 review note; invite writes are dev/pilot-only until this lands); integration test proving no orphaned PII after user deletion. **Also:** include the fixture/migration archive tables (`userDeviceTokensAppIdConflictArchive`, `*Archive051Reset`) in the retention/privacy sweep decision (they can hold FCM tokens + user/device identifiers).
- [x] T6.6 DoD + full-funnel event trace on a fixture store (every matrix event exactly once, then reset + repeat proves first-transition semantics).

---

### T7 — Integration gate (merge checkpoint)

**Goal:** Cross-branch chains proven after T3–T6 merge.
**Entry:** T3–T6 done; **Q4 thresholds signed off (T0.3) — entry condition for acceptance fixtures.**

- [x] T7.1 Mocked chain: enqueue → worker → webhook → status render (single test run, no live transports).
- [x] T7.2 Invite/account/app ladder end-to-end (T3+T4 merged).
- [x] T7.3 Walkthrough events → checklist step completion (T5+T2).
- [x] T7.4 Alert-attribution chain: System Alert CTA → resolver → picker → hub_viewed(source=alert) with campaignId.
- [x] T7.5 All-flags matrix on merged code (S6 superset: each flag independently toggled; chat invariant).
- [x] T7.6 Criterion-to-test matrix audit: every M/S/NFR ID has a passing named test or a recorded deferral (S2–S5).
- [x] T7.7 DoD.

### T8 — Launch readiness

**Goal:** Launch runbook executable; announcement remains manual and separately gated.
**Entry:** T7 done.

- [x] T8.1 Full suite + coverage targets (detectors/invite ≥90%) + PHPStan.
- [x] T8.2 Migration rehearsal: all four on a prod-schema clone; timing + lock observations recorded.
- [x] T8.3 Perf gate: 95th-percentile seeded store, 10 concurrent, p95 ≤2s; query-count regression green; Server-Timing sampled.
- [x] T8.4 Security pass: endpoint matrix re-run on merged code; token-in-logs sweep (access + app logs); ingress redaction rule live for `/activate/*`.
- [x] T8.5 Accessibility: axe on hub + activation; walkthrough keyboard run.
- [x] T8.6 Controlled live transports: real email deliverability check per T0.2 DNS (inbox + spam-folder verdict recorded), real SMS w/ delivery callback observed. `[Q3 verification gate]` → **VERIFIED 2026-07-22 by product owner**: email via SendGrid engine → INBOX (not spam) at ryanv2ts@gmail.com, invalid-token friendly page confirmed by real click; SMS via Vonage (store provider) → received on owner cell, real path (do-not-text + flood + 037 billing under scheduling_invite; Vonage = sent-only per SDD, Twilio stores get the delivery ladder). **Authentication VERIFIED via MailGenius 2026-07-22 (score 83/100): DKIM signed+valid+aligned PASS, SPF authorized PASS, Primary Inbox placement PASS.** Optional DNS improvements (non-blocking): SPF `?all`→`~all` (one-char change, -1pt) and BIMI record (branding, needs trademarked logo, -3pt).
- [x] T8.7 Kill-switch + rollback drill: flags on → full experience; `SCHEDULING_FREE` off → pre-051; each ONBOARDING flag individually off → graceful degradation; drill timed + documented.
- [x] T8.8 Pilot E2E on pc00 + one fixture store (full journey incl. phone activation + apps + clock-in) — **manual/E2E items flagged outstanding until actually run** (house rule).
- [x] T8.9 Runbook tasks (split): `scheduling-free-launch.md` flag-flip order + pilot entry/exit; rollback playbook; decision-gate checklist (**Q1 executed, Q3 verified, Q4 signed, Q5 recorded**); baseline snapshot execution + artifact sign-off; System Alert authoring w/ campaignId; announcement authorization (manual action, outside this plan).
- [x] T8.10 Business acceptance: walk the PRD Release Manifest; every launch-gated feature verified live on pilot before M2 is authorized. `[activity: business-acceptance]` → **ACCEPTED IN FULL 2026-07-22** (product owner, "Accept all" on `contracts/t8-10-acceptance-evidence.md` — all 14 manifest rows + cross-cutting gates).
- [x] T8.11 DoD. → **SIGNED 2026-07-22.** All 9 phases complete; PR #79 open; launch = runbook execution (merge → prod env → backfill/baseline → flag flips → announcement). M2 authorization remains manual per the Release Manifest.

---

## Risk Register

| ID | Risk | Owner | P/I | Trigger (observable) | Prevention / Contingency | Verified by |
|---|---|---|---|---|---|---|
| R1 | Billing keeps charging active-premium stores post-free | Product/Billing | M/H | PREMIUM_MODULE line items after launch | Q1 gate in T8.9; billing data untouched keeps all options | T8.9 checklist |
| R2 | Invite emails/SMS spam-foldered | Sched+Ops | M/H | T8.6 deliverability check fails | T0.2 DNS work; dual-channel; per-channel states | T8.6 |
| R3 | Enum/uk migrations lock or partially fail on prod-scale tables | Dev | L/M | T8.2 rehearsal timing | Additive enums; archive-first uk; rehearsal before prod | T8.2 |
| R4 | Queue outage / dead-letter accumulation | Dev | M/M | queue-age + dead-row alerts (T3.8) | Sweep recovery; alert thresholds; runbook entry | S9 + T3.8 |
| R5 | Shared Twilio callback path regression for existing SMS | Dev | L/H | 036/037 delivery tests fail post-T3.4 | Additive handler change; existing webhook tests re-run | T3.4 tests |
| R6 | Mobile clientId undercounts adoption until releases land | Mobile | H/L | app_adopted vs device-token divergence | Interim OR-signal; manual-eligible step; min-version tracking | T4.5 |
| R7 | Detection mistrust from wrong evidence | Dev | M/M | S3/S4 regressions; stale counter spike | Evidence-with-numbers; observation snapshots; health split | T2.6, T6.4 |
| R8 | Token leakage via logs/referrers | Dev | L/H | T8.4 log sweep hits | Session exchange; headers; redaction; hashed storage | T8.4 |
| R9 | Mixed-flag states expose partial experience | Dev | M/M | T7.5 matrix failures | Per-flag graceful degradation designed in; matrix test | T7.5 |
| R10 | driver.js × Syncfusion incompatibility | Dev | M/M | T0.4 spike verdict | Spike first; fallback path is a real deliverable | T0.4, T5.5 |
| R11 | Latched pilot store can't re-test first transitions | Dev | H/M | S7 re-runs impossible on pc00 | Fixture stores + reset workflow (T0.7) | T0.7, T6.6 |

## Phase Naming (PRD ↔ PLAN mapping)

| PRD Release Manifest | Plan phase |
|---|---|
| Decisions/spikes (Q3/Q4/Q5, DR-9) | T0 |
| M1 entitlement | T1A |
| Schema (all tables) | T1B |
| M3–M8, M12 UI, M10 UI, S1, hub events | T2 |
| M9 + activation | T3 |
| M10 mobile producers | T4 |
| M11 + M12-04 walkthrough UX | T5 |
| M2-04 baseline, external producers, KPI pack, privacy | T6 |
| Cross-branch integration | T7 |
| Launch gates (M2 authorization manual) | T8 |

## Phase Dependency Graph

```mermaid
graph LR
    T0 --> T1A & T1B
    T1A --> T2
    T1B --> T2
    T2 --> T3 & T4 & T5 & T6
    T3 --> T7
    T4 --> T7
    T5 --> T7
    T6 --> T7
    T7 --> T8
```

## Effort Estimate Reference

| Phase | Estimate |
|---|---|
| T0 | 2 days (decisions may wait on product; spike 0.5d) |
| T1A ∥ T1B | 3 days combined |
| T2 | 5–6 days |
| T3 | 5–6 days |
| T4 | 1–2 days |
| T5 | 3–4 days |
| T6 | 2–3 days |
| T7 | 1–2 days |
| T8 | 2–3 days |
| **Total** | **~4.5–5.5 weeks** focused single-developer + pilot buffer (T3–T6 parallelizable with a second developer → ~3.5–4 weeks) |

## Criterion-to-Test Matrix (every M/S/NFR ID)

| IDs | Covered by |
|---|---|
| M1-01..06 | T1A.1 suites, T8.7 kill-switch drill |
| M1-07 | T0.1 decision + T1A chat guard |
| M2-01..03 | T7.4 attribution chain, T8.9 (publish = manual action) |
| M2-04 | T6.2 baseline + T8.9 execution |
| M3-01..11 | T2.1/T2.3 security matrix/T2.4/T2.6, T8.3 (NFR-1/2) |
| M4-01..04 | T1A.1 (matrix), T2.6 S1-race, T2.3 activate |
| M5-01..03 | T2.4 M5 view + T2.3 switch-interest no-mutation proof |
| M6-01..04 | T2.2a + T2.4 M6 UI (overnight/tz tests) |
| M7-01..03 | T2.2a + T2.3 starter endpoint + T2.4 UI |
| M8-01..05 | T2.2b + T2.4 wage-permission rendering |
| M9-01..12 | T3.1–T3.7 (S5/S8–S12), T8.6 live transports |
| M10-01..07 | T2.2d + T2.4 M10 UI + T4.1–T4.4 ladder |
| M11-01..09 | T5.1–T5.5 (S13), T0.4 fallback |
| M12-01..04 | T2.2c + T2.4 M12 UI + T5.3 coverage notice |
| S1-01..02 | T2.4 (optional step + skip) |
| S2, S3, S4, S5 | **Deferred** (PRD Release Manifest Phase 2 / fast-follow); S5's raw-event prerequisite satisfied by T6.3 KPI pack |
| NFR-1 | Query budget table + T2.2e + T8.3 |
| NFR-2 | T2.4 poll + S4 stale tests |
| NFR-3 | T3.1/T3.3 outbox + S9 |
| NFR-4 | T5.5 + T8.5 |
| NFR-5 | T8.8 device/browser pass |
| NFR-6 | T2.3/T3.6 security batteries + T8.4 |
| NFR-7 | T2.5/T3.6 audit assertions |
| NFR-8 | T3.8 + T6.4 + T8.7 |

## Review History

### 2026-07-21 — Plan review (Codex, `read-only` sandbox)

- **Verdict:** not implementation-ready as first drafted. All findings accepted; plan restructured same day.
- **Sequencing fixes:** T0 decisions/spikes phase added (Q5 was implemented-then-decided; driver.js spike moved before framework; Q3/Q4 gates repositioned as entry conditions); all migrations consolidated into T1B (InvitesDetector previously preceded its own tables); T3/T4 contract freeze added; walkthrough persistence ownership resolved (T2 server, T5 client).
- **Coverage fixes:** per-producer analytics tasks + property contracts; privacy/retention tasks (T6.5); table-driven endpoint security matrix; worker ops (scheduler registration, health, alerting, error taxonomy, ambiguous-SMS = sent/unconfirmed not a schema state); per-detector query budget table + regression test; baseline completed (ticket source, grandfathered median, immutable rerun, checksum); UI-requirement tasks (Setup nav, wage-permission rendering, M5/M6/M7/M9/M10/M12 specifics, storeNotify actual delivery); matrix extended to every M/S/NFR ID; first-clock-in test corrected to at-or-after first post-activation publish.
- **Process fixes:** TDD ordering made explicit per slice (failing test → implement → integrate); oversized tasks split; formal DoD template; risk register with owners/triggers/contingencies; fixture-store + reset workflow solves the latched-pilot re-test problem.
- **Decision:** ✅ **Ready for implementation** (T0 decision tasks are the entry gate).

## Phase Summaries

### T0 — Decisions, contracts, and spikes (completed 2026-07-21)

**DoD:** 8/8 tasks complete. No automated-test/migration/flag items apply to this phase (decisions + spike + tooling); reset script is PHPStan-clean with all 4 CLI guards test-verified.

- **Decisions:** D-8 (Q5 chat stays premium), D-9 (Q3 email = house SMTP path + adapter), D-10 (Q4 thresholds signed at PRD defaults; manual ticket taxonomy) — recorded in PRD Decisions Log + README. All are conservative defaults recorded by the implementation orchestrator, product-overridable within their stated windows.
- **Spike verdict:** driver.js 1.8.0 = GO-WITH-CAVEATS. Binding T5 constraints: (1) overlay must be destroyed/re-driven around native Syncfusion popup interactions (default pointer-events model kills the tour otherwise — proven live on pc00); (2) add `aria-live` to popovers for NFR-4; (3) real-window resize re-verification owed at T5.5. Fallback article live at `/pc00/support/article/build-your-first-schedule/`.
- **Artifacts:** `spikes/driver-js-spike.md`, `contracts/{campaign-alert-contract,mobile-contract,fixture-stores}.md`, vendored `public_html/js/vendor/driver.min.js` + `css/vendor/driver.min.css`, `scripts/onboarding-reset-store.php`, fixture stores ob01–ob03 (storeIDs 387–389).
- **External evidence:** mobile asks published to both app repos' backend-api-updates docs (owners/min-versions pending replies — tracked in `contracts/mobile-contract.md`; non-blocking for T1A–T3, launch-gated for full M10 auto-detection).
- **Incidental fixes:** removed 2 stale `phpstan-baseline.neon` blocks referencing deleted `SlideScheduleController.php` (they failed EVERY PHPStan run repo-wide — pre-existing, unblocking the DoD requirement for all later phases); hardened reset script arg parsing (`$_SERVER['argv']` + CLI-SAPI guard).
- **T1A/T1B entry conditions:** Q5 decided (T1A unblocked); schema contracts final in SDD (T1B unblocked). ✅

#### Phase T0 Review Summary (Codex `/phase-review`, 2026-07-21) — **PHASE COMPLETED**

**Verdict:** 0 Critical / 15 Important / 6 Nice-to-have. All Important findings accepted and resolved same day; phase signed off after fixes.

**Fixed (same day):**
1. *Mobile contract named only one of two real login paths* → both documented + published docs corrected (Team+ `/api/mobile/scheduling/auth/login` → `MobileScheduling\...\MobileAuthController`; Live `/api/mobile/auth/login` → `MobileApi\...\MobileAuthController`); T4.1 rescoped to both endpoints.
2. *clientId dropped on Live refresh rotation* (`MobileApi/MobileAuthController.php:269` → `JwtAuthService.php:125` fallback) → documented as required T4 backend fix; SDD ADR-051-7 claim corrected to path-specific.
3. *`app_adopted` multi-store contract conflict* → resolved: user-scoped event, `onboardingEventLog.typeNum` now NULLable, dedupe `appAdopted:{userId}:{app}`; KPI is member-based (PRD); per-store hub panels read signal tables, no event fan-out.
4. *Campaign KPI denominator contradiction* → defined as stores with ≥1 acknowledging user via active-assignment join; campaign registry ({campaignId→alertId, publishedAt}) added to baseline artifact + runbook; anti-tamper claim softened; `ONBOARDING_CAMPAIGN_ID` slug validation + deactivate-before-clear + restart notes added.
5–8. *Reset script* → full re-baseline (provider + 9 store-local detector tables, CON-3-scoped), single-tx central batches with archived==deleted assertions, cohort excluded by default (`--include-cohort` + warning), DEV+localhost enforcement before any connection, surrogate archive PK + schema validation. All guard tests re-green; PHPStan clean.
9. *Decisions lacked owner sign-off* → D-8/D-9/D-10 marked **PROVISIONAL** with lock points (D-8→T1A merge; D-9→T3.2; D-10 thresholds→T2.1, ticket CSV→T6.2); ratification requested at the T0→T1 gate.
10. *D-10 baseline not computable* → retroactive 60-day tagging, mandatory `storeTypeNum` (literal `unknown` excluded from per-store rate), unit = tickets/activated store/week.
11. *D-9 grounding wrong* → corrected (two precedents; password reset uses SendGrid API w/ `admin@`); added fail-fast-without-SMTP + pinned-sender requirements; T8.6 checks aligned Authentication-Results.
12. *T0.5 overclaimed* → marked partially complete (asks drafted, replies pending); contract links fixed; endpoint text corrected in both app repos.
13. *Spike insufficient for T5* → binding-constraints §8 appended (destroy/re-drive entry test, Next/ArrowRight interception on required beats, static popover strings, dev-only CSP scope, T5.5 resize/a11y); T5 entry conditions updated here.
14. *Deliverables not durable* → `driver.LICENSE.txt` (verbatim MIT), idempotent article deploy generator + SQL artifact (house slug-keyed ON DUPLICATE KEY pattern, prod counters preserved).
15. *SDD/README stale* → SDD Open Items reconciled (Q3/Q4/Q5 resolved, spike done); README headline corrected (dedicated predicate; 7 tables/4 migrations); Next-step line updated.

**Rejected/deferred (with rationale):**
- *Force-track `docs/` in git* — `docs/*` gitignore is a pre-existing repo convention covering all specs; surfaced to product owner at the phase gate rather than changed unilaterally. Reset script, vendored driver assets + LICENSE, deploy generator, and phpstan-baseline fix ARE tracked-eligible and await a commit decision.
- *SystemAlertService protocol-relative-URL (`//host`) hardening* — real but touches Spec 040 (CON-8: rides unchanged); logged as a recommended follow-up outside 051.
- *Repo-wide PHPStan (236 pre-existing errors)* — out of 051 scope; DoD is per-touched-file. The stale-baseline fix restored the ability to run PHPStan at all.
- *Fixture DB provisioning unverifiable by Codex* — acknowledged (local-only MySQL); provisioning was StoreController-verified at creation.

**Scoped DoD deviation (recorded per template §2):** T0 is a decisions/contracts/spike phase; its testable code (reset script) is covered by CLI guard tests + PHPStan rather than PHPUnit TDD. Repeatable PHPUnit coverage for reset-script guards lands with T1B's migration tests when the target tables exist.

### T1A ∥ T1B — Entitlement foundation + dark schema (completed 2026-07-21)

**DoD:** All 10 tasks complete. TDD evidence recorded (red-state runs documented per T1A slice before implementation). Migrations run-twice verified (14 applied → 14 skipped). Flags-off = pre-051 behavior verified by tests + live Chrome smoke (pc00 calendar/sidebar/chat identical); flag-on verified via pilot-listed ob01 (calendar renders per plan's "pilot-listed store reaches calendar", trial endpoint 409 live-confirmed). Artifacts: two smoke screenshots; migration run logs; live DeviceToken upsert proof. External evidence: D-8-as-ratified propagated to both app repos' contract docs (T0→T1 gate).

**Notable observations (carried into phase review):**
1. Mobile flag source swap (`Store::getPremiumStatus()` raw status → `PremiumService::isPremiumActive()` via entitlement) is spec-directed (SDD edit list) but unconditional — for stores inside a same-day trial-expiry window the mobile payload can differ even with flags off (arguably fixes a stale-status gap; flagged for review).
2. Trial-endpoint 409 precedence: scheduling-free check runs BEFORE the owner check (non-owner on a free store gets 409, not owner_required) — deliberate, tested.
3. Local dev DB contains a leftover "Scheduling is now FREE" System Alert from earlier testing — harmless dev data, but real M2 sequencing publishes the announcement LAST; noted so nobody mistakes it for a live launch artifact.
4. Pre-existing PHPStan baseline mismatch on MobileApiController (KLogger count 94→95) — stash-verified pre-existing; not a T1 regression.

#### Phase T1 Review Summary (Codex `/phase-review`, 2026-07-21) — **PHASE COMPLETED after fixes**

**Verdict:** 0 Critical / 6 Important / 6 Nice-to-have. Codex's headline was correct: the flags-off invariant leaked on several paths. All 6 Important fixed same day (TDD; 348 targeted tests green post-fix):
1. *Mobile fail-open* → `getPremiumSchedulingInfo` fails CLOSED to the exact legacy raw payload; free-state emits one snapshot (no split scheduling/chat possible).
2. *Trial `status` drift + understated T1A-1 (real delta ≈19h15m, not "same-day")* → flags-off path now uses raw status + raw isActive for features — **byte-identical; deviation T1A-1 superseded/void** (the PremiumService-sourced path no longer runs flags-off).
3. *Settings/timesheets fail-open* → propagation restored (verified against pre-051 file); calendar fail-open confirmed legacy; 3 new error-path tests.
4. *Registration bypassed app-aware repo* → `registerDeviceToken` routes through `DeviceTokenRepository::upsert`; handler-level test proves team+live coexistence. **Logout stays (userId,deviceId)-scoped (pre-051-equivalent) — appId-scoped logout added to T4 scope.**
5. *scheduling_invite billable=false contradicted D-6; category visible flags-off* → billable=true (mirrors standard billable shape); `SmsCategory::visibleCategories()` hides it from presentation APIs until `ONBOARDING_INVITES_ENABLED` (pre-051 count pinned at 7 via git history); direct lookups unaffected.
6. *M4-03 logging missing* → unrecognized providers log once per typeNum/request; recognized set includes canonical `wheniwork` + `wiw` shorthand + `homebase`.

**Nice-to-haves:** apply-script non-zero exit + pilot-CSV lowercase/malformed-entry logging fixed now. Deferred with rationale: migration check-query name-presence exactness (acceptable; conflict rows provably zero), archive CTAS privacy retention → T6.5 scope, userInvites RESTRICT-FK both-directions handling → T6.5 (sequencing: invite writes are dev/pilot-only before T6.5, which is launch-gated), HTTP-level trial-endpoint/Twig render coverage → T7.5 flag matrix, Throwable-vs-Exception 500-envelope exactness exception accepted per CON-1.

**Verified clean by Codex:** CON-10 (PremiumService byte-identical), D-8 implementation, Twig real+fallback flags-off equivalence, route-matrix conformance, middleware deletion safety, all four migration JSONs incl. 051_004's archive-first SQL.

### T2 — Onboarding framework + hub (completed 2026-07-22)

**DoD:** All 7 tasks complete (T2.2 as 5 parallel slices + convergence pass). TDD red→green evidence per slice across 8 agents. Flag-off verified (post-review: API/resolver 404 when ONBOARDING_HUB_ENABLED off); flag-on verified live. Artifacts: hub E2E screenshot + GIF + event trace (ob01); scenario batteries S1/S2/S3/S4/S7 green incl. reset+repeat. 545/545 Onboarding tests; PHPStan clean on touched files.

#### Phase T2 Review Summary (Codex `/phase-review`, 2026-07-22) — **PHASE COMPLETED after fixes**

**Verdict:** 3 Critical / 16 Important / 3 Nice-to-have — all Critical+Important fixed same day (two parallel fix agents, TDD):
- **C1** API/resolver not flag-gated (dark deploy defeated) → 404-before-side-effects when hub flag off + entitlement check when on; matrix rows added.
- **C2** `checkStoreGroup` ignores `usa.isActive` → active-assignment guard (with all_stores bypass) using REAL production semantics in tests.
- **C3** Publish timestamps are server-local wall-clock vs UTC activation → dual-basis `ActivationBoundaryResolver` (basis contract documented; write-path evidence recorded: publishes=Chicago, punches/tokens=UTC).
- **I-series highlights:** prereq-gated latching (pre-activation getState can no longer latch/emit); flow_completed same-tx incl. manual paths; activation emits step_completed + previousProvider; full PRD event properties; M3-08 no-false-healthy + upsert-failure never 500s; manual-complete evidence-drift health; canAccess (prereqs) split from permissionGranted (authz) — explains the E2E's admin invites note; appAdoption ladder + firstQualifyingClockInAt milestone wired to UI; poll visibilitychange + last-known-state retention; invites coverage requires deliverable message (queued counts; dead/expired don't); appAdoption post-activation-gated; **grandfather backfill script** (`onboarding-grandfather-backfill.php`, run at flag-enable; also auto-completes storeSettings for grandfathered stores — explicit M3-10/A4-over-M6-02 decision recorded in the script header); S7 battery now uses the real backfill path; first-publish evidence latched immutable (republish can't un-meet the milestone); zero-shift publishes excluded; starter-positions race/length/reactivation hardening; guest/invalid-store envelope alignment; honest steady-state query budget **15** (11 central + 4 store, line-itemed; controller ladder double-run documented as a known cost).
- **Nice-to-have deferred:** switch-interest permission tier (works as specified; revisit at T7.5), storeType headline (correct for real stores — Store::getCompanyName maps franchise brand; fixture data reads oddly), Jest jsdom gap for poll-visibility tests (code-inspected; pre-existing harness constraint).
- **Known test-hygiene item:** `PositionsDetectorTest` has a cleanup bug that can pollute ob01 between suite runs (6 downstream failures until reset) — `onboarding-reset-store.php ob01 --confirm` clears it; fix with T7's suite pass.
- **Verified clean by Codex:** thresholds/latching/emit-once concurrency, CSRF everywhere, activation guarded-UPDATE + idempotency, permission-neutral evidence, textContent rendering (no XSS), source propagation, switch-interest no-mutation.

### T3 ∥ T4 ∥ T5 ∥ T6 — Parallel block (completed 2026-07-22)

**DoD:** All 27 tasks complete across 5 agents (T3 split a/b). Highlights per phase:
- **T3** (delivery + activation): outbox core w/ monotonic transitions + caps (exact-boundary tested); D-9 transports (pinned sender, fail-fast, do-not-text ENFORCED); 3 TaskEngine jobs + cadence migration 051_005 (run-twice); webhook additively extended (S8); endpoints w/ 61-case security matrix + flag-switched legacy-route compat; session-exchange activation (S10 battery incl. one-winner race + token-absent-from-logs); S5/S11/S12 batteries green (S11 proves SMS-only member → username collection → real password_verify login); invite observability probes EXECUTED (found pre-existing 17.5k stale task_workers rows). **T3.9 live transports DEFERRED to T8.6 prep** (no SMTP creds in dev + PHPMailer lock v5.2.10-vs-json ^6.9 mismatch to resolve). Extra: InviteTokenCache Redis side-channel closes a token-loss gap.
- **T4** (mobile producers): clientId both login paths + Live rotation preservation + refresh-token-resolved per-app logout; app_adopted producers ×4 wiring points (post-commit, failure-isolated); 508/508 mobile regression; both app repos' docs updated; T4.4 ladder test skeleton awaits T7.
- **T5** (walkthrough): **entry test PASS** — destroy/re-drive proven live over a real Syncfusion shift editor (the R10 unknown closed); engine honors all spike §8 constraints (required beats immune to Next/ArrowRight, static popovers, aria-live); 127/127 Jest; live happy-path + AI-failure + fallback runs (2 engine bugs caught+fixed live); GIF artifact; pc00 restored exactly.
- **T6**: producers wired ×6 (zero-shift guard; WIW branch deliberately unwired); baseline script immutable (refusal exit 3, checksummed artifact, D-10 CSV validation; real dev classification 8 eligible/5 grandfathered); KPI pack all 9 KPIs EXECUTED (caught cross-DB typeNum collation mismatch — COLLATE fix documented); retention job + privacy deletion (invitee delete/inviter anonymize, negative-baseline proven); full-funnel trace exactly-14-events ×2 (reset+repeat).

**Carried to T7:** cross-phase chains (T7.1–T7.4); calendar.html publish-emission one-liner + walkthrough flag stamp + CSRF meta (T5 deviations); retention-job cadence seed row (draft in runbook → migration 051_006); query-count test drift reconciliation; observability runbook merge (051-launch-monitoring + 051-invite-observability); missing dev `user_id_master` row (launch verification); Codex phase review over the merged block.

### T7 — Integration gate (completed 2026-07-22)

**DoD:** 7/7 tasks. Chains T7.1–T7.4 green on real services/DB; all-flags matrix 12/12 (6-way chat invariant; flags-all-off byte-identical); criterion audit `contracts/t7-criterion-audit.md` (~90 IDs → named tests; 4 assertion gaps → T8.1; S2–S5 + launch-gated items properly deferred). Carry-list fully closed incl. migration 051_006 (run-twice) and the CSRF meta-name bug fix.

#### Phase T7 Review Summary (Codex focused + code-reviewer split, 2026-07-22) — **PHASE COMPLETED after fixes**

Full-block Codex review timed out (~25k-line diff); split into Codex-on-T3-core + reviewer-agent-on-T4–T7. Combined: **5 Critical / 12 Important**, all verified real, all fixed:
- **Walkthrough runtime was never `<script>`-included by any page** (dead in production; invisible to every test layer by design) → flag-gated includes added to calendar.html.
- **Milestone producer ran inside the punch GET_LOCK** (fresh PDO + queries in the hold) → deferred via queuePostLockSideEffect, matching MobileClockService.
- **Duplicate-SMS windows closed**: supersede/revoke now cancel queued outbox rows + post-claim parent recheck; ambiguous transport outcomes → sent(unconfirmed) NEVER retried (taxonomy table in InviteDeliveryJob docblock); claim honors backoff; SMS stale-guard matches email's.
- **Redemption transactional** (one kiosk_users tx; event post-commit idempotent); **tokens sealed at rest** (libsodium secretbox v1 envelope + sweep auto-reissue on loss, capped once); GET_LOCK all-paths release + ambient-tx refusal; recipient cap global + per-recipient locks (MariaDB 12.1.2 multi-lock verified); delivered = terminal-win rank; webhook lookup failure → 500; no log-write before signature validation; batch IDOR + revoke guard fixed; rate limiter fail-closed/REMOTE_ADDR/atomic; cookie Secure-by-default; NULL-expiry rejected; kill-switch semantics pinned (jobs skip when ONBOARDING_INVITES_ENABLED off; activation routes stay live per Release Manifest — named test).
- One Codex "Critical" was a stale docblock (job definitions ARE seeded — 4 rows verified live); comment fixed.
- Post-fix: 343 invite/activation tests + S8/S9/S10/S11 + T7.1/T7.2 chains green; PHPStan clean; full-suite delta vs stash = zero.

### T8 — Launch readiness (automatable scope completed 2026-07-22; commits 393d24706 + 66e73f111)

**Gates:** T8.1 coverage ≥90% + 4 audit gaps closed ✅ · T8.2 rehearsal 18 ops/813ms @50k rows, zero blocking locks, R3 closed ✅ · T8.3 p95=209ms vs 2s (9.6× headroom), Server-Timing live, 051_007 index shipped w/ EXPLAIN ✅ · T8.4 matrices re-run, zero tokens in logs, redaction recipe ✅ · T8.5 axe criticals fixed + keyboard run ✅ · **T8.6 email leg: LIVE SendGrid smoke delivered to INBOX (user-verified 2026-07-22); invalid-token friendly page user-verified; SendGrid = primary engine per amended D-9; SMS leg + Authentication-Results detail pending product choice** · T8.7 drill EXECUTED 4m12s (M1-06 kill flip 18.4s), degradation matrix in playbook ✅ · T8.8 pilot E2E through clock-in milestone, 18-row exactly-once trace, GIFs ✅ · T8.9 runbooks + decision-gate checklist ✅.

**Three production bugs caught by the live E2E (all fixed + mutation-verified):** ActivationController couldn't parse JSON bodies — every real-browser activation failed invalid_nonce (harness bypassed body parsing, so no test saw it); enqueue-time delivery dispatch AND sweep recovery both called a nonexistent Redis accessor (silently dead in this environment). Plus /activate's Redis accessor (T8.5) and the composer-hook attribute-duplication fatal (393d24706).

**Remaining before announcement (T8.10/T8.11 + gates):** Q1 premium-payer billing decision (product/billing — ANNOUNCEMENT BLOCKER, DR-2); T8.6 SMS live leg (needs a target phone) + optional Authentication-Results confirmation; mobile clientId min-version replies (M10 full auto-detection only); business acceptance walk of the PRD Release Manifest; dev user_id_master row verification; ops: prod needs SENDGRID_API_KEY + TWILIO_TOKEN (webhook signatures) + the ingress redaction rule + worker registry reload on deploy.

## Post-acceptance product refinements (2026-07-23 → 2026-07-28, owner self-testing)

PRD amendments M6-01 (grouped Weekdays/Sat/Sun hours), M6-03 (inline settings editing), M8-01 (roster quick-add) — all dated, owner-driven, "inline-first" principle recorded. Delivered: humanized evidence everywhere; grouped per-day hours editor over Spec 018's storeOperatingHours; Save + Confirm-&-continue (auto-save-then-confirm + scroll-to-next); help popovers on all 9 steps (honest completion rules, a11y); positions free-text add; roster quick-add via the canonical team-member endpoint; deep links demoted to advanced links (availability/firstSchedule/apps keep primary). Guest 403 → login-redirect fix on hub/resolver routes.

**Deviation entries:**
- **OVN-1 (2026-07-28, orchestrator decision):** Spec 018 `StoreHoursService::validateHours` rejected overnight hours, contradicting binding M6-04. Relaxed: close<open = overnight, open==close = 24h; Spec 018's two rejection tests updated to acceptance. Downstream sweep found + fixed a real Spec 025 heatmap bug (OverlayDataService same-day assumption would zero heatmaps for overnight stores).
- **QA-1 (2026-07-28):** roster quick-add inherits the Team-wizard behavior of silently stripping hourlyRate/hoursRequested for callers without uri_store_settings — a permission wrinkle to revisit (UI hint or 403) before fleet launch.
- **FIX-1 (2026-07-28):** ob01 is now the product owner's PERSONAL manual-test store — tests must never touch it (hygiene note in OnboardingApiControllerTestCase; OnboardingServiceQueryCountTest migrated to ob02 with an in-transaction virgin-state guard after fixture contention broke it). Incidental pre-existing find: TeamMemberController::checkCsrf only reads body tokens — postTeamMemberAction was missing it and would have 403'd in production; fixed.

## Deviations

*(T0: decisions D-8/D-9/D-10 were recorded as conservative defaults per the autonomous-execution mandate, then RATIFIED at the T0→T1 gate with D-8 overridden to chat-goes-free — a product decision, not a spec deviation.)*

- **T1B-1 (2026-07-21):** Spec said "default `scheduling_invite` config rows" in billingSmsCategoryConfig; reality: NO category has seeded default rows — defaults are the PHP `BillingConfigService::DEFAULT_SMS_CATEGORY_CONFIG` map, and the table holds only per-store overrides. Resolution: constant added to the PHP default map (billable=false); no out-of-pattern insert migration. SDD's T1B.3 wording is superseded by this entry.
- **T1A-1 (2026-07-21): SUPERSEDED/VOID same day** — the Codex T1 review showed the delta was ≈19h15m per trial store (not a same-day edge) plus a status-string change for every trial. Fix: flags-off mobile payload now derives entirely from the raw legacy source (byte-identical); the entitlement-sourced payload runs only when `isSchedulingFree` is true. No deviation remains.
- **T1-FIX-1 (2026-07-21):** Mobile logout (`removeDeviceToken`) remains `(userId,deviceId)`-scoped after the app-aware registration fix — logging out either app removes both apps' push rows (pre-051-equivalent; the old uk collided the rows anyway). appId-scoped logout is added to T4 scope alongside the clientId work.
