feat(v1.1): item 9 — batch formula v2 compose rewrite (AMENDS the frozen '<=5 returns' rule)
Allocator now: reserved returns (>=2 whenever eligible stock exists, role order, surprising-gate on reserved nope slots, cap-4 family) -> wildcards (2) -> fresh (remainder, capped at 23; wilds draw from the fresh pool without consuming fresh slots) -> fill returns to size (role order, <=2 re-nopes total, two-pass family cap: 4 preferred, 8 rescue for fill slots only). MAX_RETURNS dies; FRESH_CAP=23, MIN_RETURNS=2, RETURN_FILL_FAMILY_CAP=8 land. Rich day = 21 fresh + 2 second looks + 2 wildcards (the sign-off shape); dry day fills to 25 with returns; 0/0/0 composes an empty section; shortfalls are never padded. Docs: module docstring, RANKING.md section recipe and DATA-MODEL returns eligibility rewritten to the v1.1 contract (+ rubric_debug pref noted). Selfcheck: A1-A8 compose matrix on deterministic staged pools (21/2/2 rich; 5 fresh + 20 SL fill incl. wild-from-fresh derivation; 21/2/2 sign-off; 12-return dry with <=2 nopes + LAST_TWO re-compose; 40 fresh -> 23+2 then the 15 leftover with zero overlap; 0/0/0 empty; family-cap 8 fill rescue; rested/gate-blocked re-nope rules). Pool-honest note: the spec's A2 '5 fresh + 18 returns + 2 wild' assumed 7 fresh listings — wildcards are by definition never-swiped, so a 5-fresh day reads 5 fresh total (3 new + 2 wild) + 20 second looks.
This commit is contained in:
@@ -162,9 +162,9 @@ Semantics:
|
||||
- Round = prior events for that listing + 1, assigned at write time. Rounds cap at 2 for engine influence (any later events weigh as round 2; v1 returns never produce rounds >2 by design).
|
||||
- **Migration seeding:** when `swipe_events` is empty and `swipes` has rows → insert one round-1 event per swipe row (created_at copied). Only-when-empty, hence idempotent.
|
||||
- **Sections:** at most ONE `open` section at a time. The deck = that section's `section_items` in `position` order, filtered to items undecided at read. Exhausted = no item undecided → `status='report'`. Forge = close current + open new. First-ever read composes a cold section (rubric-weighted, w=0).
|
||||
- **Returns eligibility (v1 contract):** a verdict exists (like/maybe/nope in `swipes`); listing `active`; NOT in either of the previous two sections; no round≥2 `nope` ever (rested); not already returned once (round cap). Selection prefers maybe > like > nope; nope candidates must be "surprising" (points ≥55 or net-positive family). Max 5 returns per section, ≤2 of them nopes.
|
||||
- **Returns eligibility (v1.1 contract, amends the frozen ≤5 rule):** a verdict exists (like/maybe/nope in `swipes`); listing `active`; NOT in either of the previous two sections; no round≥2 `nope` ever (rested); not already returned once (round cap). Selection prefers maybe > like > nope; nope candidates must be "surprising" (points ≥55 or net-positive family) for the RESERVED slots only. Every batch carries ≥2 returns whenever eligible stock exists and fills the remainder up to 25 when fresh < 23; ≤2 nope-verdict returns per batch; fill returns relax the per-family cap from 4 to 8 (fill slots only).
|
||||
- **`points` is computed, not stored** (deterministic from events + profile; a cache is allowed only if it never changes semantics).
|
||||
- **prefs:** `forge_mode` ('ondemand' default | 'strict'). Existing keys unchanged.
|
||||
- **prefs:** `forge_mode` ('ondemand' default | 'strict') · `rubric_debug` ('off' default | 'on' — dossier "why" breakdown visibility; v1.1). Existing keys unchanged.
|
||||
- **Legacy deck semantics:** `/api/deck` (cursor paging; 7-day maybe re-admit) stays as a read-only fallback for debugging; sections are the production flow.
|
||||
|
||||
## B1 additions — frozen 2026-11-10 (photo experience v2)
|
||||
|
||||
@@ -26,7 +26,7 @@ Deterministic, explainable, pure-Python. Learning reads `swipe_events` only.
|
||||
|
||||
**Score.** `taste_raw` = Σ shrunk weights over the listing's values; `taste_norm` = percentile rank among active listings (0–100). Blend: `w = min(0.85, N/60)` where N = total events; `points = clip( round( (1−w)·rubric + w·taste_norm + agent_adj ), 0, 100 )`. `agent_adj` = Σ profile adjustments (clamped ±10 per listing). Breakdown for the dossier: rubric share · top 3 ± taste features · agent lines — all source-labeled.
|
||||
|
||||
**Section recipe.** 25 cards = 2 wildcards + ≤5 returns + remainder new (points DESC). Constraints: ≤4 per family per section; ≥3 price bands when the pool allows; shortfalls fill from new.
|
||||
**Section recipe.** Every batch targets 25 cards = fresh first (up to 23 slots) + ≥2 second looks whenever eligible stock exists + the 2 wildcard slots (wildcards draw from the fresh pool). When fresh < the remainder, second looks fill up to 25 (family cap relaxes to 8 for fill returns only); leftover fresh rolls to the next batch; fresh/return shortfalls are never padded — a drained pool yields a short (or empty) section. Constraints: ≤4 per family per section for fresh + reserved returns; ≥3 price bands when the pool allows.
|
||||
|
||||
**Wildcards.** Candidates: active, unseen, not in the last two sections. Exclusions: families with ≥3 cumulative nopes and net-negative weight. Prefer: different family AND different price band than the section's top new items. Score = `0.5·points + 0.5·novelty` (novelty = distance from the liked-profile centroid). Picks: 2 per section.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user