Skip to content

feat(ai): assist ladder — free tier, Socratic mode, community handoff, self-reset (#864) - #887

Merged
thomgabriel merged 2 commits into
mainfrom
feat/864-assist-ladder-30-07-2026
Jul 30, 2026
Merged

feat(ai): assist ladder — free tier, Socratic mode, community handoff, self-reset (#864)#887
thomgabriel merged 2 commits into
mainfrom
feat/864-assist-ladder-30-07-2026

Conversation

@thomgabriel

@thomgabriel thomgabriel commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Closes #864 (epic #838, unified spec item 32; design source: docs/superpowers/specs/2026-07-25-ai-tutor-economics.md §4 + P2-1/P2-5; owner decision batch D-4/D-5/D-6/D-8 recorded on the issue, 2026-07-30).

⚠️ MIGRATION FOR THE GATE — NOT APPLIED

supabase/migrations/20260730120000_assist_ladder.sql is NOT applied to any database. Per process, the gate applies it via the Supabase MCP before merge (and extends docs/DB-MIGRATION-LEDGER.md with the MCP-stamped version). It contains two DROP FUNCTION + re-CREATEs (reset_challenge_assists, get_challenge_assist_state — return types changed), three new challenge_assists columns, and two new RPCs. House pattern throughout: SECURITY DEFINER, SET search_path = '', REVOKE PUBLIC/anon/authenticated, GRANT service_role only, RLS-on table with no policies. supabase/schema.sql mirrors everything.

The ladder (per (user, lesson), catalog-bounded, no calendar refills)

Tier Turns Meter Contract
0 — authored tutorNotes hints none free, client-side, no model call (already shipped)
1 — free AI turns 1–2 (2) hidden (D-4) full tutor
2 — metered 3–10 (8) visible (MAX_PAID_ASSISTS 4→10) full tutor
3 — Socratic 11–30 (20, D-5 — never framed "unlimited") lighter-tutor chip + count hint/ask flip to ONE diagnostic question at a 512-token cap; propose keeps its full diff contract at every tier incl. Socratic (§4.2 ruling); review untouched
turn 31 community handoff: forum link (/community/help), "lighter tutor" copy family — degrade, never block; no padlock, nothing paywall-shaped

Tier resolution is atomic and server-side in the new spend_assist_ladder_turn RPC (row-locked; denials don't touch updated_at, the reset-cooldown anchor). Refunds are tier-aware (refund_assist_ladder_turn). The #591 daily spend ledger remains the fail-closed ceiling above the ladder, unchanged. No drip/rollover/earning added (never-build bright lines held).

Self-serve reset (P2-5, owner D-8 KEEP)

reset_challenge_assists is now guarded inside the SECURITY DEFINER RPC (rule R-6), in the same migration that exposes the route:

  • once per (user, lesson)reset_used_at flag;
  • 7-day cooldown — a rolling duration since the lesson's last allowed spend (no calendar/timezone semantics anywhere; no São Paulo windows needed because no time-of-day logic was added).

Exposed via new auth'd self-only route POST /api/ai/partner/reset (user id from session; body names only the lesson). Reset zeroes the three tier counters, preserves chat_log + the never-decremented billed_assists audit. UI affordance appears only at exhaustion and states the once+cooldown honestly (ready / "available {date}" / already-used states).

Pieces

  • DB: supabase/migrations/20260730120000_assist_ladder.sql (+ schema.sql mirror, + lib/supabase/types.ts RPC types). Legacy single-meter RPCs left in place.
  • Server: app/api/ai/partner/route.ts (ladder spend, Socratic prompt + 512 cap for hint/ask, tier-aware refunds), lib/ai/partner-prompt.ts ([SOCRATIC_INSTRUCTIONS] suffix block — never in the cached prefix), lib/ai/assist-budget.ts (fail-closed wrappers), app/api/ai/partner/reset/route.ts (new), log/route.ts (full ladder state for rehydration).
  • Client: use-ai-partner.ts (tier mirror + reset), assist-meter.tsx (hidden on free tier; pips on metered; Socratic chip), ai-partner-pane.tsx (Socratic note, exhaustion block w/ community link + reset), quick-actions.tsx (old "all paid assists" wall message removed).
  • i18n: all new copy ×3 (en/es/pt-BR); old budgetExhausted/meter.label keys removed.
  • Analytics: socratic_mode_entered (session-deduped) + assist_reset_used (server-confirmed only), inventory in lib/analytics/README.md.
  • Model routing NOT touched (E-2/P1-1 + D-7 belong to the sibling issue; reachability curl not run) — Socratic cost containment here is the 512-token cap.
  • P2-7 docs leg (publish numbers in docs/): not in this PR; the numbers are in this table for the follow-up.

Tests (all green: pnpm typecheck, full web suite 2,167 tests / 239 files (post-rebase onto #885+#886: nudge + ladder suites green together), pnpm lint)

  • src/lib/supabase/__tests__/assist-ladder-execution.test.ts (pglite, real SQL from the migration lineage): free→metered at turn 3, →Socratic at turn 11, exhausted deny at turn 31; tier-exact refunds + floor-0; reset cooldown deny; once-after-cooldown then already_used forever; chat-log/billed-audit preserved across reset; denied spends don't restart the cooldown; nothing_to_reset.
  • src/app/api/ai/__tests__/partner-route.test.ts: Socratic hint gets [SOCRATIC_INSTRUCTIONS] + 512 cap; propose stays full-diff/full-budget at Socratic; no Socratic block below the tier; Socratic-tier refund is tier-aware; exhaustion reply carries counts.
  • src/lib/ai/__tests__/partner-prompt.test.ts: Socratic applies to hint/ask only; caps.
  • src/lib/ai/__tests__/assist-budget.test.ts: fail-closed spend (error/throw/empty/malformed/bogus-tier), reset relay, state mapping.
  • src/lib/ai/__tests__/use-ai-partner.test.tsx: client ladder-order progression across both boundaries, exhaustion sync, reset outcomes (allowed/cooldown/error).
  • src/lib/ai/__tests__/assist-copy.test.ts: literal scan of the aiPartner subtree ×3 locales — no buy/purchase/upgrade/premium/pay(ment)/subscription/unlimited (+ es/pt equivalents) wording, and the full new key set present per locale.

Notes for review

  • Two functions are DROPped because their return types changed — flag if any other caller of get_challenge_assist_state exists outside assist-budget.ts (grep found none).
  • #865 (attempt-gate nudge) touches ai-partner-pane.tsx/events.ts on its branch — RESOLVED: rebased onto feat(ai): replace hard think-first lock with attempt-gate nudge + logged override (#865) #885 — the ladder UI composes with the attempt-gate nudge state machine (guardAction wraps hint; nudge machinery, attempt_gate_* events and all 13 pane tests verified intact).

Do NOT merge before the gate has MCP-applied the migration.

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
superteam-academy-web Ready Ready Preview Jul 30, 2026 8:33pm

Request Review

…sist_ladder (prod-drift guard #708), prod-lineage pglite guard, refund tier-guard, honest reset_available_at
@thomgabriel

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown

Claude finished @thomgabriel's task in 2m 48s —— View job


Review

  • Gather context (PR body, changed files list)
  • Read migration SQL (supabase/migrations/20260730120000_assist_ladder.sql) + schema.sql mirror
  • Read server-side RPC wrapper (assist-budget.ts) and API routes (partner/route.ts, partner/reset/route.ts, partner/log/route.ts)
  • Read client hook (use-ai-partner.ts) and UI components (assist-meter.tsx, ai-partner-pane.tsx, quick-actions.tsx)
  • Read prompt/tier logic (partner-prompt.ts, partner-types.ts)
  • Spot-check tests (pglite execution suite, analytics events, i18n copy) + verify PR's "no other caller" claim
  • Post review feedback

This is a thorough, well-executed implementation. Summary of what I verified:

DB layer (supabase/migrations/20260730120000_assist_ladder.sql)

  • spend_assist_ladder_turn is correctly atomic: INSERT ... ON CONFLICT DO NOTHING followed by SELECT ... FOR UPDATE inside the same function invocation closes the concurrent-request race at a tier boundary.
  • Denials deliberately skip updated_at, keeping the reset-cooldown anchor pinned to the last real spend — verified by the dedicated pglite test (assist-ladder-execution.test.ts:354).
  • The prod-drift guard (folding 20260715160000_ai_partner_chat_log.sql in idempotently, section 0) is a smart catch — and it's red-proven by a test that applies the ladder migration without the chat-log migration to simulate the actual prod lineage (assist-ladder-execution.test.ts:107). Good defensive engineering given the db: migration ledger diverged from repo filenames on all 6 MCP-applied migrations (db push would re-apply) #708 ledger drift.
  • schema.sql mirrors the migration correctly (spot-checked function bodies, GRANT/REVOKE pattern).
  • Confirmed the PR's claim: no callers of get_challenge_assist_state/reset_challenge_assists/etc. outside assist-budget.ts, use-ai-partner.ts, the two route handlers, and the test files.

Server (assist-budget.ts, route.ts, reset/route.ts)

  • Fail-closed spend/reset wrappers are consistent: malformed RPC rows, thrown errors, and unexpected shapes all deny rather than pass through.
  • The billed/refund bookkeeping in route.ts correctly distinguishes "never billed" (network/DNS throw → refund) from "billed but useless" (empty output, non-JSON, malformed → no refund) — this closes the AIE-10/AIE-11 history referenced in the comments, and the tier-aware refund hands back exactly the tier that was spent.
  • Socratic-tier prompt suffix and 512-token cap are correctly scoped to hint/ask only, leaving propose's full diff contract untouched at every tier, matching the §4.2 ruling in the PR description.

Client (use-ai-partner.ts, assist-meter.tsx, ai-partner-pane.tsx, quick-actions.tsx)

  • Server is authoritative; the client-side counts mirror (advanceCounts) is cosmetic only — rehydration takes Math.max against the server counts rather than overwriting, so a fast first interaction during the async load can't be clobbered.
  • No padlock/paywall UI shapes; exhaustion degrades to the community-handoff block, matching the spec's "degrade, never block" framing. Copy is honest about the once+cooldown reset (verified by assist-copy.test.ts's literal wording scan across all 3 locales).
  • Analytics wiring (socratic_mode_entered session-deduped, assist_reset_used fired only on server-confirmed success) is correctly connected through challenge-interface.tsx:488.

Minor observation (non-blocking): refund_assist_ladder_turn (migration lines 142–151) unconditionally bumps updated_at on a successful refund. Since a refund only happens immediately after a spend that already touched updated_at, this just nudges the reset-cooldown anchor a few seconds later than the actual last spend — negligible in practice, but slightly inconsistent with the "denials never touch updated_at" invariant documented elsewhere in the same file. Not worth blocking on.

No functional bugs found. The real-SQL pglite tests (not just mocked RPCs) give solid confidence in the tier-boundary and cooldown logic specifically, which is where a subtle off-by-one would otherwise hide. As the PR body notes, the migration itself still needs to be MCP-applied by the gate before merge — nothing in this review changes that requirement.
· Branch

@thomgabriel
thomgabriel merged commit 46bff6a into main Jul 30, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AI assist ladder + wall boundaries: free → metered → Socratic, degrade-never-block (item 32)

1 participant