feat(ai): assist ladder — free tier, Socratic mode, community handoff, self-reset (#864) - #887
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
1383773 to
ef1a3be
Compare
…sist_ladder (prod-drift guard #708), prod-lineage pglite guard, refund tier-guard, honest reset_available_at
|
@claude review |
|
Claude finished @thomgabriel's task in 2m 48s —— View job Review
This is a thorough, well-executed implementation. Summary of what I verified: DB layer (
Server (
Client (
Minor observation (non-blocking): 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. |
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).supabase/migrations/20260730120000_assist_ladder.sqlis NOT applied to any database. Per process, the gate applies it via the Supabase MCP before merge (and extendsdocs/DB-MIGRATION-LEDGER.mdwith the MCP-stamped version). It contains twoDROP FUNCTION+ re-CREATEs (reset_challenge_assists,get_challenge_assist_state— return types changed), three newchallenge_assistscolumns, 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.sqlmirrors everything.The ladder (per (user, lesson), catalog-bounded, no calendar refills)
tutorNoteshintsMAX_PAID_ASSISTS4→10)proposekeeps its full diff contract at every tier incl. Socratic (§4.2 ruling);reviewuntouched/community/help), "lighter tutor" copy family — degrade, never block; no padlock, nothing paywall-shapedTier resolution is atomic and server-side in the new
spend_assist_ladder_turnRPC (row-locked; denials don't touchupdated_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_assistsis now guarded inside the SECURITY DEFINER RPC (rule R-6), in the same migration that exposes the route:reset_used_atflag;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, preserveschat_log+ the never-decrementedbilled_assistsaudit. UI affordance appears only at exhaustion and states the once+cooldown honestly (ready / "available {date}" / already-used states).Pieces
supabase/migrations/20260730120000_assist_ladder.sql(+schema.sqlmirror, +lib/supabase/types.tsRPC types). Legacy single-meter RPCs left in place.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).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).budgetExhausted/meter.labelkeys removed.socratic_mode_entered(session-deduped) +assist_reset_used(server-confirmed only), inventory inlib/analytics/README.md.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 thenalready_usedforever; 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 theaiPartnersubtree ×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
DROPped because their return types changed — flag if any other caller ofget_challenge_assist_stateexists outsideassist-budget.ts(grep found none).#865(attempt-gate nudge) touchesai-partner-pane.tsx/events.tson 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.