Skip to contentRecent 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·rating → min(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·cos → 60·((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.