From 4d6172889c2a38f98d2aaf2ef20e0d34d08b6db9 Mon Sep 17 00:00:00 2001 From: MP2EZ <182439403+MP2EZ@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:03:40 -0700 Subject: [PATCH] chore: MAINT-307 resequence sorting deck + correct false perf-enforcement claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two post-batch corrections, both "the record is wrong". A — SORTING DECK SEQUENCING (FEAT-293 follow-up) FEAT-293 promoted the control-sorting drill to a primary surface reachable from Home. The philosopher approved the rewritten card wording but flagged POSITION separately: `medical-test-anxiety` was card 3 of 12, and on a wrong answer the screen announces "Not quite. How you respond to the anxiety is in your control…". For someone with a real pending medical result that lands about thirty seconds into a cold entry from Home, before they have learned what the drill is asking. Moved to position 9, after the user meets several low-stakes cards (traffic, unanswered text, work effort, weather) and has the in-control / not-in-control convention. Sequencing only — no wording changed, and a test asserts the wording is untouched so a future edit can't smuggle a copy change in as a reorder. The new test pins the PROPERTY rather than the arrangement: sensitive cards (medical-test-anxiety, event-interpretation — the latter invites the user to supply "a difficult event", so it's the one most likely to be loaded with real material) must stay out of the opening five. Authoring a new scenario won't spuriously fail; a card drifting back toward the front will. B — CORRECT TWO FALSE PERF-ENFORCEMENT CLAIMS The repo asserted performance controls that do not exist, which is worse than a documented gap because it stops anyone from building the real one. - runbook §5 claimed "That gate already exists: the on-device Maestro flow enforcing <200ms / 60fps". Verified across all 8 flows in app/.maestro/: not one ms or fps assertion, only `timeout:` failure ceilings. crisis-button- reachability.yaml:34-35 says so itself. - The `Performance regression` CI job comment claimed "production budgets still enforced via perf:crisis/perf:breathing/perf:launch scripts". MAINT-166 PR 7 deleted those scripts. Both replaced with what is actually true, stated precisely rather than swapping one wrong claim for another: crisis DETECTION <200ms is a strict CI gate (assessment-performance.test.ts); the crisis BUTTON has only a coarse jest proxy that measures synthetic dispatch; breathing 60fps, app launch <2s and check-in transition <500ms are enforced by nothing. 60fps is tracked as INFRA-306. Also records that performance-regression-reporter.js does NOT gate — it is non-strict unless PERF_REGRESSION_STRICT=true — so crisis_response_ms: 9.38 in performance-baselines.json is a recorded baseline, not a threshold. This weakens INFRA-282 AC5's deferral rationale, which leaned on that Maestro gate. The defer still stands on its other leg (no traffic pre-launch), and §5 now says so explicitly. The matching .claude/CLAUDE.md Validation Matrix claim is corrected separately on _bare, where that file lives. Verification: npm run precommit exit 0 — safety 108, clinical 106, unit 592 (+5), privacy 51. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 11 ++- .../unit/practices/sortingDeckOrder.test.ts | 70 +++++++++++++++++++ .../modules/module-3-sphere-sovereignty.json | 40 +++++------ .../post-launch-monitoring-runbook.md | 30 ++++++-- 4 files changed, 125 insertions(+), 26 deletions(-) create mode 100644 app/__tests__/unit/practices/sortingDeckOrder.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57b083d6..5fc353fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -310,8 +310,15 @@ jobs: # - assessment-store API drift (primaryTrigger vs triggerType). # - SecureStorageService cleanup setInterval kept Jest alive past # completion — now skipped in NODE_ENV=test. - # - CI threshold variance recalibrated; production budgets still - # enforced via perf:crisis/perf:breathing/perf:launch scripts. + # - CI threshold variance recalibrated. + # CORRECTED 2026-07-25 (MAINT-307): this comment used to claim "production + # budgets still enforced via perf:crisis/perf:breathing/perf:launch scripts". + # Those scripts were deleted in MAINT-166 PR 7 (they ran zero matching tests), + # so the claim was false. What this job actually enforces is the crisis + # DETECTION <200ms budget, via assessment-performance.test.ts. The crisis + # BUTTON budget has only a coarse jest proxy (CollapsibleCrisisButton. + # behavioral.test.tsx), and breathing 60fps / app launch <2s / check-in + # transition <500ms are enforced by nothing — 60fps is tracked as INFRA-306. # Deferred (testPathIgnorePatterns in jest.config.js): # - sync-performance-validation + week3-analytics-performance: # tests reference older service APIs (new SyncCoordinator() when diff --git a/app/__tests__/unit/practices/sortingDeckOrder.test.ts b/app/__tests__/unit/practices/sortingDeckOrder.test.ts new file mode 100644 index 00000000..456812a0 --- /dev/null +++ b/app/__tests__/unit/practices/sortingDeckOrder.test.ts @@ -0,0 +1,70 @@ +/** + * MAINT-307 — sorting-deck sequencing. + * + * FEAT-293 promoted the control-sorting drill to a primary surface reachable + * from Home. The philosopher's review approved the rewritten card wording but + * flagged POSITION separately: `medical-test-anxiety` was card 3 of 12, and on a + * wrong answer SortingPracticeScreen announces "Not quite. How you respond to + * the anxiety is in your control…". For someone with a real pending medical + * result that lands roughly thirty seconds into a cold entry from Home, before + * they have learned what the drill is even asking of them. + * + * The fix is ordering, not copy. This pins the PROPERTY (sensitive cards are not + * in the opening run) rather than the exact arrangement, so authoring a new + * scenario doesn't spuriously fail — but a card drifting back toward the front + * does. + */ + +import moduleThree from '../../../assets/modules/module-3-sphere-sovereignty.json'; + +/** + * Cards that presuppose a live, personally-loaded situation. They are fine once + * the in-control / not-in-control convention is understood, and unkind as an + * opener. + * + * - medical-test-anxiety: the user may literally be awaiting a result. + * - event-interpretation: invites the user to supply "a difficult event", so it + * is the card most likely to be loaded with real material. + */ +const SENSITIVE_CARDS = ['medical-test-anxiety', 'event-interpretation']; + +/** How many opening cards must stay low-stakes. */ +const OPENING_RUN = 5; + +const sortingPractice = moduleThree.practices.find((p) => p.type === 'sorting'); +const scenarios = (sortingPractice?.scenarios ?? []) as Array<{ id: string }>; + +describe('MAINT-307 — control-sorting deck order', () => { + it('still has all 12 scenarios with unique ids', () => { + expect(scenarios).toHaveLength(12); + expect(new Set(scenarios.map((s) => s.id)).size).toBe(12); + }); + + it.each(SENSITIVE_CARDS)( + 'keeps %s out of the opening run', + (id) => { + const position = scenarios.findIndex((s) => s.id === id) + 1; + expect(position).toBeGreaterThan(0); // card still exists + expect(position).toBeGreaterThan(OPENING_RUN); + } + ); + + it('opens with a low-stakes card', () => { + // Whatever else changes, card 1 teaches the convention on something + // impersonal. Asserted as "not sensitive" rather than a hardcoded id so the + // opener can be re-authored freely. + expect(SENSITIVE_CARDS).not.toContain(scenarios[0]?.id); + }); + + it('leaves the philosopher-approved wording untouched', () => { + // MAINT-307 is sequencing only. If this ever fails, the copy changed — which + // needs a philosopher pass, not a reorder. + const card = scenarios.find((s) => s.id === 'medical-test-anxiety') as + | { text: string; correctAnswer: string } + | undefined; + expect(card?.text).toBe( + 'How you respond to anxiety while waiting for a medical test result.' + ); + expect(card?.correctAnswer).toBe('in-control'); + }); +}); diff --git a/app/assets/modules/module-3-sphere-sovereignty.json b/app/assets/modules/module-3-sphere-sovereignty.json index e7f65716..944739f3 100644 --- a/app/assets/modules/module-3-sphere-sovereignty.json +++ b/app/assets/modules/module-3-sphere-sovereignty.json @@ -85,26 +85,6 @@ "Other drivers on the road" ] }, - { - "id": "medical-test-anxiety", - "text": "How you respond to anxiety while waiting for a medical test result.", - "correctAnswer": "in-control", - "explanation": "How you respond to the anxiety is in your control. The first wave of fear is not — it arrives before you can choose. Neither is the result itself; it was never yours to decide.", - "inControl": [ - "How you work with the anxiety once you notice it (breathing, grounding)", - "Your self-talk and interpretations", - "Whether you seek support while waiting", - "Activities you choose while you wait (steadying yourself, not avoiding)", - "Thinking through what you'd do next, whichever way it goes" - ], - "notInControl": [ - "The first wave of fear (it appears automatically)", - "The medical test result itself", - "When you receive the results", - "The medical facts of your condition", - "Others' reactions to your news" - ] - }, { "id": "friend-no-reply", "text": "A friend hasn't replied to your text message for three days.", @@ -228,6 +208,26 @@ "Timing and external factors" ] }, + { + "id": "medical-test-anxiety", + "text": "How you respond to anxiety while waiting for a medical test result.", + "correctAnswer": "in-control", + "explanation": "How you respond to the anxiety is in your control. The first wave of fear is not — it arrives before you can choose. Neither is the result itself; it was never yours to decide.", + "inControl": [ + "How you work with the anxiety once you notice it (breathing, grounding)", + "Your self-talk and interpretations", + "Whether you seek support while waiting", + "Activities you choose while you wait (steadying yourself, not avoiding)", + "Thinking through what you'd do next, whichever way it goes" + ], + "notInControl": [ + "The first wave of fear (it appears automatically)", + "The medical test result itself", + "When you receive the results", + "The medical facts of your condition", + "Others' reactions to your news" + ] + }, { "id": "interview-preparation", "text": "How thoroughly you prepare for the promotion interview.", diff --git a/docs/development/post-launch-monitoring-runbook.md b/docs/development/post-launch-monitoring-runbook.md index 33fe127c..58d421d2 100644 --- a/docs/development/post-launch-monitoring-runbook.md +++ b/docs/development/post-launch-monitoring-runbook.md @@ -29,7 +29,7 @@ code-side alert (the subscription-verification watchdog) and documents the other | 2 | API / edge error rate > 5% | Sentry metric alert (+ Supabase logs for edge) | Sentry dashboard | ⏳ operator to create — see [§2](#2-error-rate-alert-sentry) | | 3 | Sustained Supabase connection failures | Supabase project notifications + external watcher | Supabase dashboard | ⏳ operator to enable — see [§3](#3-supabase-connection-failure-alert) | | 4 | Subscription-verification automation dead | `subscription_verification_watchdog()` pg_cron + Resend | **code (this PR)** | ✅ migration shipped; needs deploy + Vault bootstrap — see [§4](#4-subscription-verification-failure-watchdog-shipped) | -| 5 | Crisis-button / 60fps perf regression | on-device Maestro budgets (prod RUM deferred) | Maestro / app | ✅ covered by pre-merge gate; prod RUM deferred — see [§5](#5-performance-degradation-alert-deferred-prod-rum) | +| 5 | Crisis-button / 60fps perf regression | crisis **detection** <200ms: strict CI gate. Crisis **button**: coarse jest proxy. 60fps: **nothing** (INFRA-306) | jest / CI | ⚠️ partially covered — Maestro enforces none of these; prod RUM deferred — see [§5](#5-performance-degradation-alert-deferred-prod-rum) | **Grounding (verified live 2026-06-18 against `being-production` = `yliycxslzdsgjtpxggtf` and Sentry org `being-prod` / project `javascript-react`):** @@ -239,9 +239,31 @@ runbook takes the documented-defer path. The rationale is substantive, not a dod 1. **A crisis-button latency regression is a safety contract, and the strongest control for a contract is a gate that blocks it pre-merge — not a prod alert that fires after users felt it.** - That gate already exists: the on-device Maestro flow enforcing `<200ms` / 60fps - (`app/.maestro/`, gated by `/b-close` Phase 2.5). Prod RUM is a *trailing* indicator; for a - safety budget the *leading* gate dominates. + Prod RUM is a *trailing* indicator; for a safety budget the *leading* gate dominates. + + > [!WARNING] + > **CORRECTED 2026-07-25 (MAINT-307).** This point previously read *"That gate already exists: + > the on-device Maestro flow enforcing `<200ms` / 60fps."* **It does not exist.** Verified across + > all 8 flows in `app/.maestro/`: there is not one ms or fps assertion, only `timeout:` failure + > ceilings. `crisis-button-reachability.yaml:34-35` says so itself — *"on-device timing budgets + > live in CLAUDE.md's Performance Budgets section and are validated by hand until a real perf + > harness exists."* + > + > What actually enforces what, as of 2026-07-25: + > - **Crisis detection `<200ms` — strict CI gate.** `__tests__/performance/assessment-performance.test.ts` + > asserts `toBeLessThan(200)` for suicidal-ideation detection, run by the `Performance regression` job. + > - **Crisis button `<200ms` — coarse jest proxy only.** `CollapsibleCrisisButton.behavioral.test.tsx:51`; + > its own comment concedes it measures synthetic event dispatch, not tap→render. + > - **Breathing 60fps — nothing.** Tracked as **INFRA-306**. + > - **App launch `<2s`, check-in transition `<500ms` — nothing.** Hand-validated. + > + > Note `__tests__/reporters/performance-regression-reporter.js` does **not** gate: it is non-strict + > unless `PERF_REGRESSION_STRICT=true`, and its own comment says it is for "really slow test" + > warnings. `performance-baselines.json`'s `crisis_response_ms: 9.38` is a recorded baseline, not a + > threshold. + > + > The deferral above still stands on its other leg — reason 3, that pre-launch there is no traffic + > to alert on or tune against. But it can no longer lean on a leading gate that was never built. 2. **The two AC5 budgets need app-code work, not a config flip.** The crisis-tap `<200ms` measurement needs a **custom span** wired into the tap handler (real app-code instrumentation). Sentry *does* auto-emit app-start and slow/frozen-frame metrics as config (no custom spans) —