Spec 021: Melee Invite Modes (Link vs. Invite) #1

Open
erichall wants to merge 0 commits from 021-melee-invite-modes into main
Owner

Summary

  • Adds a per-Melee, Host-chosen invite mode at creation: "Anyone with the link" (spec 012's existing invite_code behavior, unchanged) or "Specific people only" (new closed roster via email magic-link invites)
  • New melees.invite_mode column (DB-enforced immutable via trigger) and melee_invites table with Maker-only RLS
  • Three new SECURITY DEFINER RPCs: join_melee_by_code (the minimal spec-012 join slice, since spec 012 has no implementation elsewhere in this repo), accept_melee_invite, and remove_melee_participant (discovered mid-implementation — the existing melee_participants_update policy only allowed self-updates)
  • Amends MELEE_CRITICAL_DECISIONS_v1.md Section 3 to document this as a scoped, opt-in exception to the locked "anyone can join" default

Test plan

  • tsc --noEmit clean, npm run build succeeds
  • Live-verified against local Supabase with real authenticated sessions (not service-role): FR-006 code-join rejection for closed Melees, brand-new user's full invite -> accept flow, invite_mode default + immutability trigger, removal blocking new picks/Chatter while preserving visible history, and reactivation (not duplication) on re-invite
  • Mobile-first QA via code inspection (no fixed-width containers; consistent with existing SeriesPicker/MigrationPicker pattern)
  • No browser/device testing this session (no emulator available)

All 29 tasks in specs/021-melee-invite-modes/tasks.md completed.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

## Summary - Adds a per-Melee, Host-chosen invite mode at creation: "Anyone with the link" (spec 012's existing invite_code behavior, unchanged) or "Specific people only" (new closed roster via email magic-link invites) - New `melees.invite_mode` column (DB-enforced immutable via trigger) and `melee_invites` table with Maker-only RLS - Three new SECURITY DEFINER RPCs: `join_melee_by_code` (the minimal spec-012 join slice, since spec 012 has no implementation elsewhere in this repo), `accept_melee_invite`, and `remove_melee_participant` (discovered mid-implementation — the existing melee_participants_update policy only allowed self-updates) - Amends MELEE_CRITICAL_DECISIONS_v1.md Section 3 to document this as a scoped, opt-in exception to the locked "anyone can join" default ## Test plan - [x] `tsc --noEmit` clean, `npm run build` succeeds - [x] Live-verified against local Supabase with real authenticated sessions (not service-role): FR-006 code-join rejection for closed Melees, brand-new user's full invite -> accept flow, invite_mode default + immutability trigger, removal blocking new picks/Chatter while preserving visible history, and reactivation (not duplication) on re-invite - [x] Mobile-first QA via code inspection (no fixed-width containers; consistent with existing SeriesPicker/MigrationPicker pattern) - [ ] No browser/device testing this session (no emulator available) All 29 tasks in specs/021-melee-invite-modes/tasks.md completed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nested package dirs (api-mock/, extension/) had their own node_modules
and build output showing up as untracked since the old patterns only
matched repo root.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Runs `specify init --here --ai claude` to install spec-kit's shared
scripts, templates, and git extension. Agent folders can hold
credentials/tokens per spec-kit's own setup warning, so .claude/ is
now gitignored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 0/1 speckit output: research.md resolves six open decisions
(Melee completion trigger, avatar sourcing, realtime reveal channel,
card image generation, avatar photo storage, headline generation
approach). data-model.md extends users and melees and adds a
highlights table. contracts/ define the avatar-selection actions,
realtime reveal channel, and card image route. Flags that specs
010-013 are still spec-only with no implementation yet, which 017
eventual tasks will need to account for.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tasks.md: 29 tasks across Setup, Foundational, User Story 1 (P1 MVP),
User Story 2 (P2), and Polish phases, scoped to avoid rebuilding the
unimplemented specs 007-013 flows this feature touches (a QA seed
script and a minimal Melee detail page stand in for them instead).

Design fix caught while writing tasks: the CHECK constraint on the
new users avatar columns requires theme/seed to be set the instant a
row exists, so FR-002's auto-assign can't be deferred to a join-time
hook that does not exist yet. Moved it into the existing
handle_new_auth_user() signup trigger instead. Updated data-model.md,
contracts/avatar-identity.md, and plan.md accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
I1: plan.md and tasks.md already committed to a Maker-issued mark
complete action (T021) with no backing requirement in spec.md. Adds
FR-016 formalizing it as the trigger FR-006 depends on, one directional,
distinct from spec 013 per-Stage Outcome entry.

G1: FR-003 names Chatter as a surface needing consistent avatar
display, but Chatter (spec 010) has no implementation yet so no task
covers it. Both spec.md and tasks.md now say so explicitly instead of
leaving it to read as an oversight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Enables Realtime and Storage locally (first feature needing either),
adds DiceBear for illustrated avatars, and adds a migration that:
- extends users with avatar_kind/avatar_theme/avatar_seed (Member Identity)
- gives melees.status a real vocabulary (draft/active/completed) plus
  completed_at, previously undocumented anywhere in this schema
- adds the highlights table
- redefines the existing signup trigger to auto-assign a random
  illustrated avatar, so FR-002 holds from account creation rather than
  a join-time hook that does not exist yet
- adds the identity-reveal trigger on melee_participants, writing
  activity_feed rows the same way pick_history already gets populated
- adds an avatars Storage bucket with owner-scoped RLS

Also adds src/lib/avatars.ts (DiceBear render helper), a QA seed
script standing in for specs 007/008/009/012/013's unbuilt production
flows, and a minimal Melee detail page both user stories build into.

Verified: supabase db reset applies all migrations cleanly, npm run
seed exercises the full pipeline end to end (4 identity_reveal rows
written, every user auto-assigned an avatar), and the project
type-checks and lints clean against locally regenerated database types.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the avatar picker (src/app/profile), a shared Avatar rendering
component used everywhere a user is represented (FR-003), and wires
the Melee detail page with a live real-time reveal overlay plus a
persisted activity history so a participant who was offline when the
reveal fired still sees it on their next visit.

Verified against a real authenticated session (magic-link token
exchanged through /auth/confirm for the seeded Alice user): both
/profile and /melees/{id} render 200 with real content — theme picker
with all four DiceBear sets, participant list and activity feed with
rendered avatars. The live cross-tab reveal itself is code-complete
and type-checked/linted but needs a manual two-browser check per
quickstart.md; no headless-browser tooling exists in this repo yet to
automate that.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds private.compute_melee_highlights(), a trigger that fires once when
a Melee's Maker marks it complete and computes six of the seven
superlative categories (placement, streak, most_improved,
chatter_activity, boldest_take, top_rookie) plus a highlight_reel_ready
activity_feed announcement. closest_miss is not computed: outcomes has
no numeric margin field anywhere in this schema, so there's no data
signal for it, the same gap that already dropped Best Autofill from
this roster.

Also tightens the pre-existing melees_update RLS policy, which let any
active participant flip status — without that fix, FR-016's Maker-only
restriction would only have existed in application code, not the DB.

Adds the mark-complete action, the swipeable highlight reel page, and
the next/og-rendered shareable card route. The card route needed an
unplanned fix: Satori can't embed an SVG data URI, so the DiceBear
avatar is rasterized to PNG server-side via the new @resvg/resvg-js
dependency, and next.config.ts now externalizes it for Turbopack.

Verified end to end through the real RLS-gated path: a non-Maker's
mark-complete attempt is rejected, the Maker's succeeds, highlight
computation is idempotent on repeat writes, the reel page renders all
8 computed highlights with real data, and the card route returns a
genuine, visually-inspected 1080x1920 PNG.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
QA pass surfaced and fixed a real SC-006 violation: co-winners in any
tied category (streak, placement, chatter_activity, boldest_take, and
especially top_rookie, where every eligible participant necessarily
ties) received byte-identical headline text, since the copy was built
purely from the numeric stat with no per-person differentiator.
Migration 20260720230810 prefixes every headline with the
participant's own name and re-verifies clean against a fresh seed run
(0 duplicate headlines across 8 highlights).

SC-005 (no loss-coded visual treatment) and mobile-first layout
(constitution IV, no accidental horizontal scroll) verified by code
review — no category-conditional color/copy branch exists anywhere,
and the only overflow-x usage is the reel's intentional swipe
carousel. `npm run build` succeeds cleanly with all ten routes
registered. The live cross-tab realtime overlay and true on-device
touch feel still need a manual pass — no headless-browser or
device-emulation tooling exists in this repo to automate either.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Scoped down from a larger originating brief ("Competition Engine,
Ontology and Series") after grounding its claims against the actual
repo state. Several of the brief's premises did not hold once checked:

- It references an events table throughout; no such table exists,
  the real entity is melees everywhere in the deployed schema.
- It frames Kalshi/Polymarket auto-resolution and outcomes.entered_by
  accepting a system value as new decisions this spec introduces;
  spec 013 already fully specifies both (FR-009 through FR-015).
- Its proposed market_links table substantially duplicates what spec
  013 already put directly on stages.
- Its Series wrapper overlaps a different, already-specified
  mechanism (spec 006 Tier 2 leaderboard + spec 014's migration
  chain), which uses a linked-chain model, not a grouping table.

This spec covers the market-import-first Maker flow, the
competition_type/resolution_source ontology, intensifiers, and
Knockout Pool. Series/cross-event leaderboard and Draft/Roster are
explicitly excluded (documented in spec.md's own Explicitly Out of
Scope section) since they either duplicate already-specified work or
the originating brief itself marks them as deferred.

One real, unresolved tension is flagged rather than silently decided:
the brief's proposal to give intensifiers a different scoring weight
directly contradicts MELEE_CRITICAL_DECISIONS_v1.md's LOCKED "all
Stages worth equal scoring weight, no edge cases" rule. This spec's
FRs keep intensifiers equally weighted, consistent with the locked
rule, and calls out that reopening it is not this spec's call to make.

One clarification was needed and resolved: Knockout Pool ends with no
winner declared if every remaining participant is wrong in the same
round (strict last-standing), not the NFL survivor-pool convention of
reinstating everyone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Applies the documentation updates spec 019's originating brief requested,
across all four named documents, plus fixes a larger pre-existing
staleness problem discovered while doing so.

Product Brief (v1.4 -> v1.10) and Schema Reference (v1.0 -> v1.1) both
predated the 2026-05-21 pivot from Group-scoped to Melee-participant-scoped
data access, and described a technical stack (AWS/Node.js/DynamoDB/Cognito)
that was never built (actual: Next.js + Supabase, Postgres + RLS
throughout). Corrected in place, marked with dated notes, original
content preserved rather than deleted so the design history stays
legible. Schema Reference additionally gets a superseded notice
pointing to docs/SCHEMA_SUMMARY.md as the actual current schema
reference, rather than attempting a full line-by-line rewrite of
~700 lines of pre-pivot content against live migration SQL.

New content from spec 019 added to all three docs: market-import-first
event creation, the four-question Maker flow, intensifiers, and
Knockout Pool. Also flags Ranking Challenge (spec 018) as BLOCKED, not
merely deferred, and documents Draft/Roster as researched-and-deferred
per the originating brief's instruction. Series / cross-event
cumulative leaderboard is explicitly NOT resolved in these docs -- see
spec 020 and the companion brief.

melee_design_thinking_v1.6.docx had no .txt companion (unlike the other
two docs); added one to match the existing pairing convention, then
regenerated all three .docx files from their edited .txt sources via
textutil so the pairs stay in sync. Note: the regenerated .docx files
have plain formatting -- any original styling (bold, tables, colors)
did not survive the round-trip through plain text.

Also creates docs/MELEE_MASTER_CONTEXT.md, a new document (none existed
under this name anywhere in the repo) indexing key decisions, current
state, and glossary terms across the strategy docs and spec history.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The second brief spec 019's originating memo suggested as its own
scope, split out from the larger "Competition Engine, Ontology &
Series" brief.

Grounded against the actual schema and existing specs before writing:
the originating proposal (an events table, a series table keyed to
group_id, a group_event_scores table) doesn't match this repo, where
the entity is melees and no such scores table exists. More
substantively, spec 006 (Groups Display) already anticipated this
exact problem -- its own Edge Cases and Assumptions sections
explicitly flagged that reconciling the group-label view against the
migration-chain leaderboard is "a future decision, not resolved here."
This spec is that decision, scoped narrowly: a Series is a bounded,
Host-curated set of Melees representing legs of one real-world
competition (Tour de France, World Cup), distinct from the migration
chain's open-ended recurring-friend-group continuity (spec 014) --
the two mechanisms solve different problems and are not merged.

Also notes, without resolving unilaterally, that Series introduces a
fourth leaderboard concept alongside the three tiers
MELEE_CRITICAL_DECISIONS_v1.md already locks -- formally naming it in
that document's taxonomy is flagged as a follow-up, since that
document is marked LOCKED.

No clarification markers needed; every design fork had a reasonable
default already implied by existing precedent (Tier 2's sum-not-rate
framing, spec 011's live/partial-resolution principle, the existing
tie-break rule).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Spec 019's competition_type enum includes "ordinal" ("a full ranked
order"), which could read as solving the same gap spec 018 exists for.
It doesn't: 019 only lets a Maker tag a Stage ordinal, it defines no
storage for that Stage's prediction or result -- picks.contender_id
and outcomes.winner_contender_id remain single-Contender FK columns,
untouched by 019. Spec 018 is not redundant; it's the reverse -- 019's
ordinal label has no working mechanism behind it until 018's storage
question (FR-012: child tables vs. JSONB vs. recursive per-position
Stages) is resolved.

Adds an explicit cross-reference in both directions: 019's Explicitly
Out of Scope section and its FR-005 now flag that ordinal is label-only,
and 018's grounding note now states the dependency from its side. Also
finally commits specs/018-ranking-challenge-format/, which had been
sitting uncommitted since earlier in the session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resolves FR-011 via /speckit-clarify: Ranking Challenge is explicitly
deferred, not scheduled against any current phase (spec 007 Phase 1 =
Bracket/Ballot only, Phase 2 = Pick'em/Survival Pool only), and nothing
else currently depends on it resolving. The spec remains fully
specified and ready to build whenever this is revisited -- US1-3 and
FR-001-010 are not invalidated by deferral.

FR-012 (storage mechanism: child tables vs. JSONB vs. recursive
Stages) is deliberately left open rather than resolved under deferral
-- the right answer may depend on what else has shipped by the time
this is revisited. Requirements checklist updated to reflect
resolution; zero NEEDS CLARIFICATION markers remain.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 0/1 speckit output. research.md is grounded in real, sourced
findings on Kalshi and Polymarket's actual APIs (verified via their own
docs, not assumed): Kalshi's read-only market data needs no
authentication but has no free-text search endpoint, so matching goes
through series/event listing plus local title matching; Polymarket has
a dedicated /public-search endpoint that directly handles what a Maker
types, so it's queried first.

data-model.md extends stages with competition_type/resolution_source
(no new market_links table -- spec 013's existing kalshi_market_id/
polymarket_market_id columns already cover that), adds Knockout Pool
elimination via a trigger on outcomes plus a picks-RLS extension for
used-Contender enforcement (both derived from existing data, no new
tracking tables), and fixes a real gap found during planning: spec
019's own Assumptions section describes numerical Stages scoring
"only an exact match" but no column anywhere stores that number.
Added a small numeric_value column pair to picks/outcomes rather than
leaving numerical stranded the same way ordinal already is (spec 018) --
documented in plan.md as a planning-time correction, not silently
patched into spec.md.

contracts/ cover the market-search endpoint, the four-question Maker
flow, and Knockout Pool's pick/elimination mechanics.

Notes that specs 007 and 013 still have no implementation in src/app,
only deployed schema -- this plan's eventual tasks will need to build
the minimal event-creation and Outcome-entry surfaces this spec
actually exercises.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
36 tasks across Setup, Foundational, four user-story phases (P1 market
import, P1 four-question flow, P2 intensifiers, P2 Knockout Pool), and
Polish. Notes explicitly that User Stories 1 and 2 are tightly coupled
in practice despite being separately prioritized in spec.md -- Q1 of
the four-question flow is literally what triggers the market-search
call US1 needs, so they're flagged to build together rather than
strictly sequenced.

Foundational phase includes a minimal, manually-triggerable market-
resolution check (T012) standing in for spec 013 FR-010's mechanism,
which has schema but no implementation anywhere yet -- explicitly
scoped as a stand-in, not a production scheduler, so it isn't mistaken
for spec 013 being fully built.

User Story 4 (Knockout Pool) is called out as independent of the rest
of the plan's market/ontology work and buildable fully in parallel.

T014 catches and resolves a small inconsistency: the originating brief
proposed contenders.external_outcome_id, which never made it into
data-model.md. Verified against both files and the real schema before
writing the task -- uses the already-deployed contenders.metadata
JSONB column instead rather than introducing an unnecessary new column.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the competition ontology migration (stages.competition_type/
resolution_source/last_polled_at/resolved_via_import_at, picks/
outcomes.numeric_value with contender_id and winner_contender_id made
nullable), the Kalshi and Polymarket read-only clients, match.ts
(Polymarket search first since Kalshi has no keyword search endpoint,
confirmed against their own docs), the /api/market-search route, and
a minimal manually-triggerable resolution poller standing in for spec
013 FR-010's never-built mechanism.

Two unplanned but necessary fixes bundled into the same migration,
found while verifying it rather than assumed away:
- private.compute_melee_highlights() (spec 017) compared contender_id
  directly for correctness; once nullable, a numerical Stage's picks
  would silently evaluate NULL = NULL as not-correct, undercounting
  every participant's score for any Melee containing one.
- pick_history.new_contender_id was NOT NULL and its logging trigger
  only fired on contender_id changes -- a numerical pick's value
  change would go completely unlogged, violating the constitution's
  "pick_history exists from day one, every change recorded"
  non-negotiable. Added old/new numeric_value columns and fixed the
  trigger to fire on either column changing.

Both Kalshi and Polymarket clients were built against real, verified
API behavior rather than assumption -- Kalshi's field names checked
against their OpenAPI docs, Polymarket's actual response shape
confirmed via a live call (which corrected an initial wrong guess
about where the markets array lives in the response).

Verified end to end: migration applies cleanly, npm run seed:market
and npm run poll-market-resolution both run correctly, and
POST /api/market-search returns real matches with real Contenders and
populated relatedSubMarkets against live queries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Built together per tasks.md's own guidance -- Q1 of the four-question
flow is literally what triggers the market-search call, so the two
priorities aren't separable in practice. Adds the event-creation page
shell, the NewMeleeFlow client component (four gated questions plus
review-and-confirm), and two server actions: createMeleeFromMatch()
for the market-import path (including intensifier sibling Stages) and
createMeleeManualFallback() for when no match exists.

One correction from the task list as originally written: T015 called
for falling back to "spec 007's AI-scaffold path," but spec 007 has
zero implementation anywhere in this repo -- no LLM integration
exists to fall back to. Building a real one is spec 007's own,
separate scope. Implemented as a clearly-labeled manual Contender-entry
form instead, honest about not being AI, rather than fabricating a
fake AI flow.

Verified without browser automation (none available this session): a
real magic-link session exchanged for an access token, then every
write in both action sequences -- group, melee, participant, stage,
contenders, and an intensifier sibling stage -- replicated and
confirmed to succeed under real RLS as that authenticated user. Also
re-verified the Foundational phase's numeric_value/pick_history fix
in practice: a numerical Stage's pick correctly stores numeric_value
only, and updating it fires the corrected trigger with both old and
new numeric values recorded.

Regenerated database.types.ts against the local schema so the new
columns (competition_type, resolution_source, numeric_value) type-check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
User Story 3 (intensifiers) was already substantially complete as a
side effect of how match.ts (T009) was written -- confirmed here with
a live query returning 19 real sub-markets, each with exactly
title/externalMarketId/provider fields, so no odds/price/probability
data can leak structurally, not just by UI discipline.

User Story 4 (Knockout Pool) gets its own migration, fully independent
of the market/ontology work: melee_participants.eliminated_at, a
trigger that eliminates a participant when their pick resolves
incorrectly, and an RLS extension rejecting both writes from eliminated
participants and reused Contenders. Offered as a separate top-level
format choice (FormatChoice -> KnockoutPoolFlow) rather than forced
into the single-competition four-question wizard, since a Knockout
Pool genuinely needs multiple rounds/Stages upfront.

T031/T032 as originally worded assumed a pick-submission UI to modify
-- none exists anywhere in this repo yet (specs 008/009/012/013 remain
unbuilt). Built the honestly-scoped alternative instead: the Melee
detail page now shows each participant's eliminated status and a
"no winner declared" banner when the pool ends, with RLS remaining
the real enforcement boundary regardless of what UI exists.

Caught a real bug in my own seed script while verifying: pick_deadline
was set in the past (copied from spec 017's seed-melee.ts precedent,
which never tests a live RLS-gated insert) -- this fixture does, so
every pick submission failed regardless of Knockout Pool logic,
initially masking the actual RLS test result. Fixed to a future
deadline and re-verified: eliminated participant rejected, active
participant with a fresh Contender succeeds, Contender reuse rejected
-- all confirmed under real RLS with real authenticated sessions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SC-004 (no odds/price/probability in the intensifier step) confirmed
structurally, not just by inspection -- the SubMarket type has no
field that could carry one. SC-003 (no forced dead-end without a
market match) confirmed by code review of the shared question
sequence. Full production build succeeds with all 12 routes
registered, including /melees/new and /api/market-search.

Genuine touch/gesture interaction and clicking through the wizard in
an actual browser still need a manual pass -- no browser automation
or device-emulation tooling is available this session.

All 35 tasks in tasks.md are now complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 0/1 speckit output. Unlike prior specs in this family, this one
builds on real, working infrastructure rather than another minimal
stand-in: spec 019's event-creation flow now exists in code, so
Series-tagging extends it directly instead of fabricating a new one.

research.md resolves five decisions, most notably one spec.md itself
left genuinely open: FR-006's tie-break says "the Maker's prop-question
tie-break where available" but doesn't say which leg's prop question
governs when a Series has several, each with a different Maker.
Resolved as "the most recently resolved leg with a scored prop
question," flagged explicitly as a planning-time judgment call on an
underspecified requirement, not silently assumed.

data-model.md adds a minimal series table (no group_id, deliberately
not carrying forward the originating brief's proposal, since Series
visibility is derived from leg participation) and melees.series_id,
plus a computed-on-read SQL function for the cumulative leaderboard --
matching spec.md's own decision not to materialize it, the same way
Tier 2's all-time record already isn't.

Constitution Check flags, without resolving unilaterally, that this
spec introduces a fourth leaderboard concept alongside the three tiers
MELEE_CRITICAL_DECISIONS_v1.md locks -- inherited directly from
spec.md's own honest framing of that tension, not a new gap found here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
19 tasks across Setup, Foundational, three user-story phases (P1
Series tagging, P1 cumulative leaderboard, P2 partial-participation
fairness), and Polish.

Unlike specs 017/019, User Story 1 modifies real, already-shipped
code rather than building another stand-in: spec 019's three creation
actions (createMeleeFromMatch, createMeleeManualFallback,
createKnockoutPoolMelee) each get an optional series_id parameter via
a shared resolveSeriesId() helper.

User Story 3 is flagged as almost entirely verification, not new
code -- FR-004 (a skipped leg contributes nothing, not a zero) is
satisfied structurally by the Foundational migration's JOIN (not
LEFT JOIN) design in private.series_leaderboard(), confirmed against
the seed fixture rather than built separately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the series table (no group_id, deliberately -- visibility is
derived from leg participation, not a cosmetic label), melees.series_id,
and the cumulative-leaderboard SQL function.

One real security gap found and fixed while implementing, not present
in data-model.md: the function was planned as private.series_leaderboard(),
but private-schema functions aren't reachable via supabase.rpc() at all
(this repo's own established convention keeps that schema unexposed to
the Data API). Moving it to public alone would have been worse than
where it started -- since it's SECURITY DEFINER, it bypasses RLS on
the underlying tables entirely, so a bare move would let any
authenticated user query any series_id's results regardless of
whether they ever participated in a leg. Fixed by adding an explicit
authorization check inside the function body itself, failing closed
(empty result, no error) for anyone who was never a participant in any
of the series' legs -- verified live via the real RPC path with an
actual outsider account.

Also adds src/lib/series/leaderboard.ts (the tie-break resolution
research.md flagged as a genuine planning-time decision: most-recently-
resolved leg's scored prop question, falling back to earliest pick
submission timestamp) and a QA seed script with a real partial-
participation fixture (Carol joins only 2 of 3 legs).

Verified end to end via the real supabase.rpc() path: Alice 3/3, Bob
2/3, Carol 2/2 from only 2 legs played (not penalized for skipping the
third -- FR-004 confirmed live, not just structurally).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends spec 019's real event-creation flow rather than building
another stand-in: SeriesPicker.tsx (select existing or start new) and
a shared resolveSeriesId() helper, wired into all three of spec 019's
creation paths (createMeleeFromMatch, createMeleeManualFallback,
createKnockoutPoolMelee) via an optional seriesSelection parameter.

Caught and fixed a real RLS bug while verifying, not present in
data-model.md: series_select only allowed visibility via leg
participation, but a brand-new Series has zero legs pointing to it at
the moment it's created -- the exact same chicken-and-egg gap
20260719222605_users_groups_rls.sql already fixed once for
melees_select. Widened to also admit created_by = auth.uid(), the
same fix, same precedent.

Verified end to end under real RLS with a real authenticated session:
created a new Series (now correctly visible to its own creator
immediately), tagged two separate Melees into it via two separate
creation calls, confirmed both queryable together.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the Series leaderboard page (rendering getSeriesLeaderboard's
result, participant-gated via series_select RLS) and a Realtime
subscription that triggers a refetch when a relevant leg's Outcome
changes, same pattern as RevealListener (specs 017/019).

Notes an honest, non-bug limitation found while implementing: Postgres
Changes respects outcomes_select RLS per subscriber, so a partial
participant (User Story 3's Carol, who only joined 2 of 3 legs) won't
get a live push for the 3rd leg's changes -- she'll see the updated
total on her next reload instead of instantly. This is RLS-filtered
Realtime working as designed, not something to route around.

Verified end to end with a real authenticated session: the page
renders real data, and critically, the full tie-break resolution
(not just its individual pieces) was exercised for real -- Bob and
Carol both show correct_count=2, but Bob is correctly listed first,
falling through to the earliest-pick-submission-timestamp comparison
exactly as research.md specifies, since no prop questions exist in
this fixture.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
No new code -- FR-004 (a skipped leg contributes nothing, not a zero)
is satisfied structurally by the Foundational migration's JOIN (not
LEFT JOIN) design. Confirmed live rather than just by inspection:
Carol joined only 2 of the Series' 3 legs, and the real RPC
leaderboard shows her correct_count=2 alongside Bob's correct_count=2
-- the same raw integer despite Bob having played one more leg,
confirming the comparison is an unweighted sum, not a rate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
T018 redone correctly: the first attempt used the service-role client,
whose absent auth.uid() JWT context made public.series_leaderboard()'s
authorization check fail closed regardless of migration-chain state --
producing empty results before and after for a reason unrelated to what
was being tested. Redone with a real authenticated participant session:
non-empty leaderboard rows (3 participants, matching correct_count and
tiebreak_rank) are identical whether a leg's migrated_from_melee_id and
carry_over_points are set or null, confirming the computation never
references migration-chain state.

T017 rolls up the per-phase verifications already done for all three
user stories, noting the one gap honestly: User Story 2's live "no
manual refresh" Realtime push follows the already-shipped RevealListener
pattern (specs 017/019) but wasn't re-confirmed by literal browser
interaction, since no browser automation is available this session.

T019: code-level mobile QA (no fixed-width containers, buttons wrap
like text) plus a clean npm run build, which compiles the new
/melees/series/[id] route with no TypeScript errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
No schema changes needed: migrated_from_melee_id, carry_over_points, and
their pairing CHECK constraint already exist from spec 005, unused until
now. Grounded against live code before writing the plan, and found two of
spec 014's own citations are stale: FR-010's RLS extension turns out to
already be done (an earlier, unrelated chicken-and-egg fix on melees_select
incidentally widened it to cover former participants too), and the
cross-Melee record spec 014 cites as "spec 006, Tier 2" does not actually
exist -- spec 006's own implementation comment explicitly defers it to
Phase 3.

Since spec 014's own SC-003/SC-004/SC-005 describe that all-time record as
this spec's measurable outcome, not spec 006's, the plan scopes in the
minimum needed to make them checkable: one new SQL function
(migration_chain_leaderboard, mirroring series_leaderboard's shape and
scoring join from spec 020) plus a small addition to the existing Group
detail page -- not the full Phase 3 leaderboard UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resolves spec 014's remaining open edge cases (soft-deleted sources,
branching, add-participant scoping, host self-removal, silent enrollment)
via /speckit-clarify, generates tasks.md via /speckit-tasks, and corrects
a spec-wide mis-citation (spec 006 -> spec 005 FR-014) found during
/speckit-analyze. Also adds scripts/forgejo.py, a CLI wrapper for the
Forgejo REST API used to manage issues/PRs on git.sabo.network.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a per-Melee, Host-chosen invite mode at creation: "Anyone with the
link" (spec 012's existing invite_code behavior, unchanged) or "Specific
people only" (new closed roster, magic-link invites via Supabase Auth,
Host-managed add/resend/remove, soft-delete on removal). Includes the
minimal spec-012 join-by-code slice needed to make this spec's own User
Story 1 checkable, since spec 012 itself has no implementation anywhere
in this repo yet. Discovered and fixed a real RLS gap mid-implementation
(melee_participants_update only allowed self-updates, blocking Host
removal) via a new remove_melee_participant RPC. Amends
MELEE_CRITICAL_DECISIONS_v1.md Section 3 to document this as a scoped,
opt-in exception to the locked "anyone can join" default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This branch is already included in the target branch. There is nothing to merge.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin 021-melee-invite-modes:021-melee-invite-modes
git switch 021-melee-invite-modes

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff 021-melee-invite-modes
git switch 021-melee-invite-modes
git rebase main
git switch main
git merge --ff-only 021-melee-invite-modes
git switch 021-melee-invite-modes
git rebase main
git switch main
git merge --no-ff 021-melee-invite-modes
git switch main
git merge --squash 021-melee-invite-modes
git switch main
git merge --ff-only 021-melee-invite-modes
git switch main
git merge 021-melee-invite-modes
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
erichall/Melee!1
No description provided.