• T34: priorityInbox (M.8) — paid_boost clips get inserted immediately
    after whatever's currently playing (danceSchedule[0]), pushing every
    later entry's timing back, rather than waiting for the fill-loop's
    normal "top up to 30s ahead" trigger. That distinction mattered:
    draining priorityInbox only from inside the fill loop (first attempt)
    technically satisfied "never loses to a vote-ranked pick" but could
    leave a ready paid_boost clip waiting a long time if the schedule
    already had ~30s queued — dance-mode3.test.js's race test caught this
    (timed out) before the eager-insert fix. ensureDanceSchedule's existing
    "generating -> done" reconciliation loop diverts a job into
    priorityInbox instead of danceQueue's "ready" state when
    job.priority === "paid_boost", spliced out of danceQueue after the
    loop (not mid-iteration).
    
    T35: POST /api/dance/upload-character (需登录+emailVerified, same rule
    as T27). Face photo -> T2 upload+EXIF strip -> T32 moderateFaceBlacklist
    -> T25-style describeCharacterImage (new pluggable fn, shares
    VISION_API_KEY) -> characters.create(source:"user_paid", isDefault:
    false, totalSegments/remainingSegments computed from pricing_config's
    new spend_character_per_segment×segmentsPerUnit) -> creditLedger.spend
    (relatedAssetId=character.id, rolled back on failure) ->
    submitGeneration(..., priority:"paid_boost") -> lands in priorityInbox
    (cache hit) or danceQueue pending T34's reconciliation (fresh
    generation). Garment is optional (need求文档 3.3 第 4 点): given
    garmentId is validated active-or-400, otherwise a random active
    garment is picked — a fully empty wardrobe is a 400 before any spend.
    
    dance-mode3.test.js covers both: auth/balance/no-active-garments gates,
    the happy path (cost scales with requested segments, character record
    shape, clip carries priority/requestedByUserId), and T34's actual
    guarantee — a paid_boost character is scheduled at or ahead of an
    already-ready free pick for the same garment, distinguished by
    characterId since both target the same garment on purpose.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse File »
  • credit_ledger (dev-plan-4.md T23; schema M.2) joins users/sessions in
    the same SQLite db.js — architecture.md 1.4 named this as the other
    half of why SQLite got picked in T18 ("扣积分 + 创建生成任务 + 扣减片段
    配额" needs real atomicity, T18 only used the credential-storage half).
    spend()/refund()/topup() each read-modify-write users.creditBalance
    inside a transaction; spend() refuses to take a balance negative
    (INSUFFICIENT_BALANCE) instead of allowing debt, and topup() dedupes
    by paymentRef so a Stripe webhook's guaranteed at-least-once delivery
    can't double-credit (T24 will rely on this).
    
    pricing_config (M.12) stays a JSON file via a new pricing.js — it's
    operator-edited config, not on the atomic spend path, so SQLite would
    be over-engineering here; keyed by `key` rather than an id, hence its
    own tiny module instead of store.js's createStore. Seeded with
    placeholder credit amounts (dev-plan-4.md §1.2 item 3 — real numbers
    are still TBD).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse File »