---
description: CLI, MCP, and share must pass the same format opts (tz, locale, flags)
globs: packages/{cli,mcp,core}/**/*
alwaysApply: false
---

# Surface parity (CLI / MCP / share)

Any user-visible formatter change must land on **all three surfaces** before merge:

| Surface | Verify |
|---------|--------|
| CLI | `packages/cli` command + `packages/cli/test/` |
| MCP | `toolGet*` in `packages/mcp/src/tools.ts` + `packages/mcp/test/tools.test.ts` |
| Share | `formatShare*` + `packages/mcp/test/share.test.ts` |

`scripts/release-qa.sh` exercises CLI + share rendering across locales and timezones; MCP arg threading is **not** rendered there — guard it with unit tests.

## Knockout surfaces live-resolve — never read the skeleton (the #1 recurring bug)

The bundled schedule's knockout slots are **resultless placeholders** (codes like `2A`/`2B`, flag `🏳️`). So a team-facing surface that reads the static bundle is **silently** blind to a confirmed knockout tie — no crash, no wrong data, just a stale placeholder. **Knockout live-resolve regressions** shipped repeatedly in different surfaces (root cause: skeleton reads, cache gaps, or both):

- v0.8.2 — R32 pre-draw seeds rendered static, didn't live-resolve.
- v0.8.6 — bracket third-place slots never read the merged live `match.home/away`.
- v0.8.7 — `next` / `share next` / MCP next-fixture resolvers read only the static bundle.
- v0.8.8 — statusline next-match countdown read the static bundle (`🏳️ vs 🏳️` / `⚽ —` in knockouts).
- v0.8.9 — empty fixtures cache stamped the full 15min TTL at phase boundaries; stale-cache `live · syncing…` still read the skeleton.

**Rule:** every team-facing surface must reach the live overlay (`getBracket` / `getNextFixtureForTeam` — the same `fetchWindow` the bracket uses), fail closed on outage, and attribute the provider only when the overlay served the chosen fixture. **Exception — statusline:** hot path cannot fetch; the cold-path refresher calls `getKnockoutFixtures` and caches to `CacheState.fixtures`; `renderPrompt` merges that slice over the bundle (countdown **and** `live · syncing…`), never leaking 🏳️. **Exception — MCP `fixtures://{date}` resource:** deliberately static ("Static fixture list"; a resource URI carries no tz and gets no live overlay) — do not "fix" it to live-resolve without a maintainer decision; agents use `get_today`/`get_bracket` for resolved pairings.

| Surface | Entry point | Must |
|---------|-------------|------|
| CLI `bracket` / `next` | `cmdBracket` / `cmdNext` | call `getBracket` / `getNextFixtureForTeam` |
| CLI `share bracket` / `share next` | `cmdShare` | same |
| MCP `get_bracket` / `get_next_fixture` | `toolGetBracket` / `toolGetNextFixture` | same |
| MCP `get_share_snippet{bracket,next}` | `toolGetShareSnippet` | same |
| **statusline** | `renderPrompt` + `runRefresh` | hot path, NO network — indirect: refresher caches `getKnockoutFixtures` → `CacheState.fixtures`; `renderPrompt` merges them, **failing closed to `⚽ —`** when the cache lacks the pairing; unresolved placeholders dropped in syncing (never 🏳️). Empty successful fetch uses a **short TTL** (~60s); provider errors keep prior cache. **`cmdPrompt` and `cmdHook`** both spawn fixtures refresh in knockout phase. |

**Executable guard:** `packages/{cli,mcp}/test/knockout-surface-coverage.test.ts` pins one fake resolved tie (Mexico vs Ecuador) and asserts every surface renders the real nations, not `🏳️` (statusline from a seeded cache). **`statusline.test.ts` / `refresh.test.ts`** cover syncing + empty-cache TTL. **Adding a team-facing surface? Add it to that test and this table.**

## Format opts invariant

If a core formatter accepts `tz`, `locale`, `flags`, or `date`, **every wrapper** passes them from `CliConfig` / `CommonOpts`.

```typescript
// ❌ BAD — locale only; tz silently drops to server local
formatBracketList(view, { footer: false, locale: args.lang });

// ✅ GOOD
formatBracketList(view, { footer: false, locale: args.lang, tz: args.tz });
// or reuse fmtOpts(args) where shapes align
```

## Regression tests

When adding a format option (e.g. `date: true` on `formatKickoff`):

- Add a **core** unit test for the option.
- Add an **MCP tool test** proving `tz` changes output (e.g. UTC vs `America/Mexico_City` on a cross-midnight kickoff).
- Update share tests if `formatShareBracket` / compact lines are affected.
