Commit Graph

9 Commits

Author SHA1 Message Date
Alexander Rozhenko
beeace231d fix #3: guard breakdown() so a delisted mid-batch card cannot 500 the deck
A section can outlive an ad: when a card's listing left the active pool after compose and has no swipe history, it is absent from the engine's by_id map. breakdown() indexed the missing row without a guard, so one such card 500'd GET /api/section (and /api/listing/{id}) - live incident 2026-11-15: section 57 + delisted listing 298. Degrade to no-signal lines instead. Regression test added; before/after verified on a copy of the live DB (section 500 -> 200; forge rebuild 200).
2026-11-15 11:06:51 -04:00
Torres
2160ba6ce8 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.
2026-11-13 13:53:33 -04:00
Torres
a1455aade4 feat(v1.1): items 7-8 — fresh counter pill + loud 'No more new bikes!' empty state
Item 7: /api/stats gains fresh (actives with no swipe row — the deck's
new-pool predicate verbatim); the header pill reads 'N live · M fresh'
(new badge unchanged); old APIs without fresh render no segment.

Item 8: sections.pool_counts() answers can-the-forge-serve-anything from
the composer's own candidate SQLs (stock answer, LAST_TWO not applied)
and rides every section_state body. The FE adds #empty2 ('No more new
bikes!' + 08:30 line + undo note): shown whenever the pile/buffer drain
and pool is 0/0/0 (report masked), on a zero-item dry forge too; undo
hides it; legacy .empty untouched. UpdateUI repaints the caption per
hydrate; the stats key-set selfcheck gains fresh.
2026-11-13 13:42:08 -04:00
Torres
98d345ceee fix(v1.1): item 5 — section counter keys off in-section events (2/2 -> 3/2 bug)
A card is decided only when a swipe event carries THIS section's id;
the lifetime swipes.action a second look wears into its section no longer
paints it as pre-decided. progress/undecided, report counts (latest
in-section event per listing) and maybe_exhaust all use the new
section_decided_ids() set; read path only, no writes. Every section_state
item carries decided_this_section; the FE counts server truth unless a
local intent is in flight, and sectDecided is clamped to sectTotal
everywhere (commit + paintSect) so the pill can never read past N/N.

Semantics note: completing a section now means striking EVERY item in it
(returns re-struck on their second look) — the exhaust selfcheck was
updated to strike all items, matching the deck's real flow. Undo-on-a-
return residual (event stays after the DELETE) is the accepted Q5 clamp-
only behavior, covered by a dedicated selfcheck case.
2026-11-13 13:38:18 -04:00
Torres
38271d0d88 b1: selfcheck — 6 new B1 checks (guarded migration, imgs import + idempotence, 640-row copy byte-stable re-import, /img?i= chain, swipe photo_idx persistence, payload n_photos/last_photo_idx + rotation math); hardened two P3 checks against live-data drift on migrated copies (event seeding + profile sources); payload contract now includes B1 keys 2026-11-11 00:10:56 -04:00
Torres
04463cadcd P3: personalization core — event log + taste engine + section engine + agent pass + API + frontend
- db.py: guarded P3 tables (swipe_events/sections/section_items/taste_profile/
  agent_runs), one-time event seeding from swipes, append_event round math
- taste.py: deterministic taste engine (action x round x recency weights,
  n/(n+3) shrink, ramp w=min(.85,N/60), percentile norm, agent adjustments
  +/-10, points + source-labeled breakdown + tank bands)
- sections.py: compose/read/exhaust/forge — 2 wildcards + <=5 returns
  (<=2 nopes, surprising only) + remainder new, <=4/family, >=3 price
  bands w/ repair, returns maybe>like>nope, last-two-section exclusion,
  ondemand/strict modes
- agentpass.py: mock (deterministic) / live (OpenRouter, one retry) /
  fallback; strict payload schema; --print-summary/--dry-run
- api.py: GET /api/section, POST /api/section/forge, GET /api/trends,
  POST /api/agent/run, POST /api/prefs; P3 payload fields (points, tank,
  slot, round_next, points_breakdown); events stamped with section_id
- frontend: section buffer + progress pill, report screen + forge + mode
  toggle, WILDCARD/SECOND LOOK bands, tank gauges (list + dossier), dossier
  Why-this-rank, real Trends tab, /api/deck fallback for pre-P3 API, sw v2
- selfcheck: 9 new P3 checks (25/25 green)
2026-11-10 23:18:26 -04:00
Torres
d7fb694626 importer: dedupe refinement — min(ns,nb) completeness pairing (stretch I)
- cross-province groups now pair ns<->nb by completeness (img > km > older)
  up to min per region instead of collapsing the whole group to one row;
  surplus siblings (multi-unit dealer ads) stay active — no false merges
- verified on live DB: dup 80 -> 77, active 560 -> 563, total 640; the 3
  freed rows are the BMW G 310 R dealer siblings (450/451/452); deck top
  order unchanged (Indian Scout 91 x3 then Vulcan 900 73); re-import
  idempotent (77 stable)
- selfcheck +2: multi-unit pairing semantics, reversibility (flip to
  active + re-import re-derives the pairing) — 16/16 green
2026-11-10 21:25:02 -04:00
Torres
778d1bfd4b selfcheck: extend with P2 API checks on a temp DB copy (14 total)
- 8 P1 spine checks unchanged; 6 new API checks via fastapi.testclient
  against create_app() on a temp COPY of the live DB (live file never
  opened by tests): root/static, stats-live==active+health, deck page 1
  monotonic + undecided-only + full cursor walk zero-overlap vs SQL truth,
  swipe like->list->edit->delete lifecycle (+invalid payload rejection),
  /img/{id} cached-200/hotlink-302/none-404, badge roundtrip
- bare-python run keeps the 8 P1 checks and notes the skipped P2 set
- known framework quirk documented: this fastapi+httpx TestClient pair
  mangles RedirectResponse to a bare 404; real uvicorn serves the true 302
  (proof run verifies over the wire)
2026-11-10 21:22:26 -04:00
Torres
19e46cd11b app: add proof deck CLI, dependency-free selfcheck, gitignore 2026-11-10 18:57:17 -04:00