| 1 | # Retrieve-Judge-Retry for X Search |
| 2 | |
| 3 | **Date:** 2026-08-14 |
| 4 | **Status:** Completed |
| 5 | |
| 6 | ## Problem Statement |
| 7 | |
| 8 | X search results become off-topic when multi-word search queries are phrase-quoted. The Rome failure (2026-08-14) demonstrated this: |
| 9 | |
| 10 | 1. Planner generated `search_query: "Rome Italy"` (phrase-quoted) |
| 11 | 2. Phrase-quoting returned thin hits with engagement bait (pretty-cities, geopolitics accounts) |
| 12 | 3. `entity_extract` ranked off-topic handles (PrettyCitiesX, visegrad24) by frequency |
| 13 | 4. `pipeline.py` promoted those handles to the FROM lane |
| 14 | 5. FROM lane filled the 40-slot X budget with off-topic timelines |
| 15 | |
| 16 | ## Solution |
| 17 | |
| 18 | Implement retrieve-judge-retry for X search: |
| 19 | |
| 20 | 1. **Query Compilation (R2):** X now uses `raw_topic` like Reddit/YouTube, not the planner's `search_query` |
| 21 | 2. **Fanout Queries (R3):** Multi-word topics use unquoted AND as first variant; phrase-quote only for proper names |
| 22 | 3. **Corpus Judging (R7):** New `x_judge.py` module evaluates corpus on-topic ratio after retrieval |
| 23 | 4. **Retry (R1):** If off-topic flood detected (ratio < 0.4), retry ONCE with simplified keyword query inside the X stream (not `_retry_thin_sources`) |
| 24 | 5. **Split FROM Promotion (R4):** |
| 25 | - Explicit handles (--x-handle): always FROM, no AND topic |
| 26 | - Extracted handles: FROM only if ≥2 on-topic hits AND ≥50% ratio, and they DO AND the topic |
| 27 | 6. **First-Party Exemption (R5):** Floor immunity stays conservative (explicit handles only) |
| 28 | 7. **Status Reporting (R6):** Off-topic floods emit artifact warning, not `record_failure(PARTIAL)` |
| 29 | |
| 30 | ## Implementation Details |
| 31 | |
| 32 | ### New Module: `x_judge.py` |
| 33 | |
| 34 | - `judge_x_corpus(items, topic, ranking_query)`: Returns on_topic_ratio, is_off_topic_flood, on_topic_items, handle_stats |
| 35 | - `promotable_handles(items, topic, extracted_handles, explicit_handles)`: Returns (explicit_promotable, extracted_promotable) |
| 36 | - `should_retry_x_search(items, topic, depth)`: Returns True if retry warranted |
| 37 | - `prune_off_topic_items(items, topic)`: Returns only on-topic items |
| 38 | |
| 39 | ### Key Changes |
| 40 | |
| 41 | - `grok_x._fanout_queries()`: No phrase-quote for place/disambiguation strings |
| 42 | - `grok_x._is_proper_name()`: Detects title-cased proper names for phrase-quoting |
| 43 | - `grok_x.search_handles()`: Added `and_topic` parameter (default False) |
| 44 | - `pipeline._fetch_x_backend()`: Accepts query directly, not subquery |
| 45 | - `pipeline._retrieve_stream_impl()`: X source uses raw_topic, judges corpus, retries if needed |
| 46 | - `pipeline._run_supplemental_searches()`: Uses `promotable_handles` for split FROM logic |
| 47 | |
| 48 | ### Tests |
| 49 | |
| 50 | - `tests/test_x_judge.py`: New test file for x_judge module |
| 51 | - `tests/test_grok_x.py`: Added fanout and and_topic tests |
| 52 | - `tests/test_pipeline_v3.py`: Updated fixtures to have promotable content |
| 53 | |
| 54 | ## Success Criteria |
| 55 | |
| 56 | - [ ] `_fanout_queries("Rome Italy")` has no `"Rome Italy"` variant |
| 57 | - [ ] `search_name("Peter Steinberger")` still phrase-quotes |
| 58 | - [ ] `search_handles(["steipete"], "topic")` does not AND topic by default |
| 59 | - [ ] `search_handles(["visegrad24"], "Rome", and_topic=True)` does AND Rome |
| 60 | - [ ] Explicit --x-handle always gets FROM lane |
| 61 | - [ ] Off-topic handles (visegrad24) not promoted to FROM lane |
| 62 | - [ ] On-topic handles (mamboitaliano__) promoted to FROM lane |
| 63 | - [ ] X source status is artifact warning, not PARTIAL failure |
| 64 | - [ ] All tests pass |
| 65 | |
| 66 | ## Out of Scope |
| 67 | |
| 68 | - No live Grok calls in tests |
| 69 | - No auth/doctor touch |
| 70 | - No collision lexicon (AS Roma / Odunze still appear; judge + ranking_query drop them) |
| 71 | - bird_x quote-preserving `build_topic_query` (follow-up if needed) |
| 72 |