Skip to content

Recent changes

2026-04-27

  • [feat] Boost exact-match (tag/creator/lineage) personalization weights so they clearly drive the feed for engaged signed-in users. tagScore base 100→200 (top-end strong match: ~36 raw, ×1.5 ≈ 55; was ~18/×1.5≈27); creator coeff ×20→×30 with cap 15→25 (concentrated mindshare can reach +30 contribution); lineage 5→15 (sequel/fork is a strong intent signal, was a rounding error). Combined effect: a "matching card" with all three exact signals firing now lands around +60-90 instead of ~16-30, putting personalization in the same magnitude class as quality (~90 cap), vector (60 cap), and editorial (~30-50 typical).
  • [feat] Late tuning: dial quality cap from ~140 → ~90 + revert custom-UI to tiers. Top-end quality was dwarfing the bounded score components even after the recalibration. Each coefficient scaled ~0.65×: downloads ×5→×3, favorites ×6→×4, reviews ×6→×4, messages ×4→×3, rating cap 12→8, freshness max 25→18. Custom-UI reverted from continuous log curve back to tier system (200/500/1000 LOC → +20/+30/+40); the step pattern is more legible to creators than a smooth curve where one more line produces invisible delta. Combined effect on the screenshot world (downloads 1215, favs 190, reviews 17, rating 4.9, msgs 116K, age 9d): quality 140 → ~94, presentation 10 → 30 (tier 2 hit), total around the same but better-distributed across components.
  • [feat] Tighten vector similarity active range: cap moved from 1.0 → 0.85 (floor unchanged at 0.3). Embeddings of distinct worlds essentially never reach cos≈1.0 in this catalog; reserving budget for cos≥0.85 means "very strong" matches saturate fully and the meaningful range (0.5-0.8) gets the active part of the curve. Effect at coefficient 60: cos 0.5 +5 → +8, cos 0.7 +20 → +32, cos 0.8 +31 → +50, cos 0.85+ → +60 (was +37). Same squared shape, just compressed onto realistic cosine values.
  • [feat] Reco recalibration grounded in prod data (Apr 2026, n=152). Pre-recal data showed custom_ui was the dominant component for 70% of worlds (tier system acted as a flat ceiling almost everyone hit), messages had a 90K outlier inflating its top-end to +42, freshness fired for ~5% of catalog (linear cliff at 14d), and reviews/rating were carried with high coefficients despite P95=0 data. Changes:
    • Freshness: 15-pt linear cliff over 14d → 25 · 2^(−age/14) (true 14-day half-life, never zeroes). Brand-new now +25 (was +15); 14d still +12.5; 30d ≈ +6; 100d ≈ +0.1. Smoother decay keeps signal alive across the long tail without giving free points to anything.
    • Custom UI: 200/500/1000 LOC tiers (+20/+30/+40) → continuous min(30, 5·log1p(LOC/100)). P50 (~500 LOC) drops from +30 to +9; P95 (~8K LOC) drops from +40 to +22; max caps at +30. Removes the cliff/ceiling that made it dominant; rewards depth proportionally.
    • Quality coefficients: downloads ×4 → ×5; reviews ×8 → ×6 (sparse signal, don't over-amplify); messages 5·log(1+m/20)4·log(1+m/100) (tames the 90K-msg outlier from +42 to +27).
    • Rating: 2·ratingmin(12, 4·rating·√(reviews/10)). A 5.0 with 1 review now contributes ~6 (was 10); with 10 reviews caps at 12. Stops single-review worlds from inflating.
    • Vector cosine: linear 60·cos60·((cos−0.3)/0.7)² (clamped to [0,1]). Embeddings have a noise floor ~0.3; subtracting then squaring means unrelated worlds → 0, mid-similar (cos 0.5) → ~5, strong (cos 0.7) → ~20, very strong (cos 0.9) → ~44, identical → 60. Small bumps in the upper range produce big score deltas; small bumps in the noise floor produce nothing.
    • Editorial: unchanged per request.
  • [feat] Bump VECTOR_SCORE_WEIGHT 12 → 60 to match the post-L1 tag/creator magnitudes. After the L1 recalibration, tagScore caps near ~70 (×1.5 in exactTotal ≈ 105) but vector stayed at its old 12 cap, dropping vector from ~20% of strong tag overlap to ~8%. At 60, a typical strong vector match (cosine ≈ 0.6) yields ~36 — back in the same magnitude class as tag overlap so the two signals trade off again instead of vector being dwarfed.
  • [feat] L1-normalize all four recommendation weight maps (tagWeights, creatorWeights, relatedTagWeights, creatorTagAffinity) at profile build time so each map sums to 1. Prior raw weights grew unboundedly with signal count (heavy users had tagWeight totals in the hundreds), letting the tag/creator score components dwarf the bounded score components (editorial, presentation, freshness, vector) — the more a user did, the less leverage curation had on their feed. After L1 the formula operates on per-user mindshare ratios, not accumulated weight, so the same constants behave the same for new and heavy users alike. Recalibrated scoring constants accordingly: tagScore coefficient 15 → 100, creatorScore weight×1 → weight×20 (cap 15 unchanged), relatedComponent coefficient 5 → 30, affinityComponent weight×1 → weight×20 (cap 10 unchanged). Inspector renders weights as % mindshare with raw value in tooltip. Tests preserved by asserting on ratios (decay survives L1) where absolute values became trivially 1.0.
  • [feat] Presentation bonus in recommendations: tiered by lines of code in rootComponent.files (200/500/1000 LOC → +20/+30/+40) plus a flat +10 for any audioTracks. Calibrated against prod distribution (Apr 2026, n=152 published worlds): ~22% sub-tier (stub or v1→v2 migrated bubble-only), ~26% +20, ~29% +30, ~22% +40. Initial char-based threshold flagged 95% of worlds because the migration auto-generates a ~3.5K-char bubble component for every v1 world; LOC + tiering separates "creator typed something" from "creator built real UI." Max combined +50 — a category shift comparable to ~150 favorites. Computed in SQL on worlds.schema JSONB; no migration needed. Surfaced as a presentation block in ScoreBreakdown (now reports raw LOC alongside the bonus), visible in /admin/reco inspector.

2026-04-25

  • [feat] Soft/hard penalty tiering in recommendations: favorites and reviews become soft-penalties (kept in feed, score-deprioritized) with age-decay brackets (fresh 7d / mid 30d / old). Library entries remain hard-excluded.
  • [feat] Diversity penalties enabled in listwise rerank: CREATOR_PENALTY_WEIGHT set to 12 (was 0), new TAG_CLUSTER_PENALTY_WEIGHT of 7 using the candidate's first tag as the dominant cluster signal.
  • [feat] DB-backed editorial system: new editorial_boosts (worldId XOR languageGroupId, optional date window) and featured_worlds (slot-unique) tables, migration 0026_editorial_curation.sql seeded with the prior hardcoded values, 60s in-memory cache, admin CRUD at /api/admin/editorial/{boosts,featured}.
  • [feat] Public GET /api/worlds/featured endpoint replacing the hardcoded UUID list in featured-section.tsx.
  • [feat] Cold-start tag fan-out: 5 buckets × 8 candidates (角色卡 / 世界卡 / 模拟器 / 小说 / 游戏); cold.popularRecent reduced 48 → 16.
  • [feat] Hub-card StatusBadges (Favorited / Reviewed) overlaid on world cards in rec-tab, following-tab, and featured section. Subscribes to useFavoritesStore so badges flip instantly on toggle.
  • [refactor] Removed hardcoded EDITORIAL_INITIAL_SCORE_BY_GROUP / EDITORIAL_INITIAL_SCORE_BY_WORLD maps; profile now carries an editorialBoostFor closure over a cached snapshot.
  • [test] +14 tests in recommendations.test.ts and a new editorial.test.ts (5 tests) covering brackets, multi-signal max, candidate annotation, diversity penalties, and cold-start exports.
  • [docs] RECO_ALGO_BRANCH.md updated: status RESUMED, Phase 6 integration log, ship procedure adds migration 0026, rollback adds the editorial table drops.
  • [feat] New followed_recent candidate route — fetches latest published worlds from explicitly-followed creators (cold:0, warm:12, mature:12 candidates, sorted newest-first). Closes the "follow signal underused" gap: a brand-new release from a followed creator now enters the candidate pool by recall, not just popularity. Scoring (creatorWeights +8) is unchanged.
  • [fix] Server-side hub_serve event now skips emit when c.req.raw.signal.aborted is true. Stops React-strict-mode dev double-fetch (and any client-aborted/navigated-away request) from inflating the hub_serve count with events that can never funnel-close on a real impression/click.
  • [fix] hub_preview_open / hub_preview_close no longer double-fire in dev. Added a module-level 100ms dedupe keyed by ${event}:${worldId} so React strict-mode's mount → unmount → mount cycle doesn't double-emit. Production builds skip strict-mode entirely so this is a no-op there.