Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions app/__tests__/unit/practices/sortingDeckOrder.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
40 changes: 20 additions & 20 deletions app/assets/modules/module-3-sphere-sovereignty.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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.",
Expand Down
30 changes: 26 additions & 4 deletions docs/development/post-launch-monitoring-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):**
Expand Down Expand Up @@ -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) —
Expand Down
Loading