Market is now a first-class slice in the Coaching ops surface; legacy Airtable rec_ org IDs being rekeyed
Why
The Coaching operator surface has been conflating two distinct concepts in its organization_id column. Pre-migration Airtable modeled each market (Oahu Swim, Dallas Swim) as a separate "organization" base. The lift-and-shift onto Postgres carried those legacy record IDs forward, and scripts/rekey-legacy-scope-ids.ts has been collapsing them to canonical sguild-swim since the Phase-2-readiness milestone. But the script was last extended before eligibility_consistency_sample, dispatcher_inbox_dedup, and dispatcher_event landed, so the rekey was incomplete in those tables. The operator dashboard surfaced rows labeled "Oahu Swim" and "Dallas Swim" alongside "Sguild Swim" as if they were peer organizations, plus the e2e fixture org org_e2e_operator_visual for any operator with that seed loaded.
Separately, Coaching has lacked any way to slice within a single organization. Markets are real in Platform's canonical catalog (platform.canonical_market, joined to canonical_service_area by market_id), and Coaching has been reading the market name through to display "Honolulu, Oahu" as a service-area label, but never exposing market as a filter or rollup axis. Operators asking "how is Oahu doing this week" had no answer they could click to.
This memo records that both gaps are now closed.
What
Two phases of work, both landed in the coaching repo in the same session.
Phase 1: rekey hygiene
scripts/rekey-legacy-scope-ids.ts now also rewrites organization_id (and dispatcher_event.tenant_id) in:
coaching.eligibility_consistency_samplecoaching.dispatcher_inbox_dedupcoaching.dispatcher_event
A new scripts/check-legacy-scope-ids.ts smoke check walks every Coaching table with an organization_id, tenant_id, home_service_area_id, or service_area_ids column and exits non-zero if any value matches the Airtable record-ID shape ^rec[A-Za-z0-9]{14}$. Backstop for the rekey; can be wired to CI in a later pass.
The operator dashboard now filters out unresolved organization labels by default. Coaching tables may contain organizationId values that have no matching row in platform.organization (legacy rec_ values and the e2e fixture org); these no longer surface in the operator org dropdown. Set COACHING_OPS_SHOW_UNRESOLVED_ORGS=1 in the e2e environment if the e2e seed depends on the test org appearing in the dropdown.
The rekey itself still needs to be run against the live database. The script is idempotent; re-running it on an already-clean dataset is a no-op.
Phase 2: Market as a first-class slice
modules/ops now models Market explicitly:
- New
MarketLabelDTO andloadMarketLabels()reading fromplatform.canonical_marketwith HTTP-catalog fallback. LoadOpsDashboardOptionstakes an optionalmarketId; when set, coach/window/template/exception/projection queries are narrowed to service areas in that market via a service-area-to-market lookup againstplatform.canonical_service_area. A staleservice_area_idURL param that doesn't belong to the selected market is silently dropped rather than rendering coaches from the wrong market.loadRegionalEvidenceis rewritten to group by (organization, market) rather than by organization. Each row is now keyed by market, joined to its parent organization for context. Coaches are attributed to a primary market viahome_service_area_idthen anyservice_area_idsassignment. Eligibility samples and active windows are attributed via their ownservice_area_id. Projection rows fall back to coach attribution (sinceprojection_statecarries noservice_area_idtoday; denormalization candidate for Phase 3). Unattributed coaches and samples (service areas not in the canonical catalog) surface in a syntheticmarketId: null"Unattributed" row per organization so data-quality issues stay visible.OpsRegionalEvidenceDTO gainsmarketId: string | nullandmarketName: string | null; the existingorganizationId/organizationNamefields are retained as parent-org context.
UI:
app/operator/layout.tsxadds a Market selector. Organization selector is now hidden when only one organization is resolved.app/page.tsx(Coaching home) replaces the "Organization / discipline" label with separate "Organization" (hidden when only one) and "Market" selectors. Service-area<option>elements are grouped under<optgroup>by market when no market is pinned; flat list when a market is selected.app/health-panel.tsxProducer-Eligibility-Response-Conformance table renames the leading column from "Organization" to "Market" and renders the market name with the organization as subtext.
All operator pages thread marketId from the URL through loadOpsDashboard and forward markets and selected market to OperatorNav.
What this is not
This is not a contract change. The coach-availability contract still requires organization_id and service_area_id on every read endpoint and does not add market_id. Sales and Delivery consumers are unaffected. If a consumer use case for market-wide reads emerges (e.g., the Sales calendar-roster app from 2026-05-16-sales-coaching-calendar-roster-app-position), an additive market_id query parameter on the eligibility-by-description and availability endpoints would be a contract minor-bump and follow the standard memo + 2-week consumer window. No such consumer ask is on file today.
This is not a tenancy-axis change. ADR-0001's multi-org claim stands; Coaching remains tenant-keyed on organization_id. Market is an intra-organization grouping above Service Area, not a replacement for the tenancy boundary.
Asks
No action required from Sales or Delivery. Filed as FYI so the rename of the conformance-table column from "Organization" to "Market" doesn't surprise anyone reviewing the operator surface.
Platform may want to consider whether the legacy recAbU7cdF25zx3Qe and receu2hzz85FkloBt rows still exist in platform.organization. They currently lend the "Oahu Swim" and "Dallas Swim" human names to the conformance display when those legacy IDs surface; once the Coaching rekey runs in production, no Coaching row will reference them and they become orphan platform-side records. Cleanup is at Platform's discretion.
A Phase 3 candidate (not committed): denormalize service_area_id onto coaching.projection_state so per-market projection rollups don't fall back to coach attribution. Worth scoping if per-market freshness SLOs become an explicit ask.
References
- ADR-0001 (tenant ID shape; multi-org tenancy invariant).
- ADR-0003 amended 2026-05-01 (Coach relocated to Coaching).
- ADR-0008 (Coaching domain stand-up).
coordination/contracts/coach-availability/README.mdv1.0.0 (current contract surface; unchanged).coaching/scripts/rekey-legacy-scope-ids.ts(extended).coaching/scripts/check-legacy-scope-ids.ts(new smoke check).coaching/modules/ops/repo.ts(loadOpsDashboard, loadRegionalEvidence).2026-05-09-coaching-airtable-sunset-scoping-commit(Phase-2-readiness milestone that the original rekey script was scoped against).