File size: 25,194 Bytes
f8ef20f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
# Dynamic SmartCrusher Preservation Plan

## Problem Statement

Current SmartCrusher uses **static "First 3 + Last 2" preservation** regardless of:
- Array size (100 items vs 10,000 items get same treatment)
- Data pattern (time series vs search results vs logs)
- Query context (user asking about "latest" vs "oldest")
- Position importance (first items might all be identical/wasteful)
- Learned retrieval patterns (which positions do users actually need?)

This is too simplistic for a production-grade compression system.

---

## Current Implementation Analysis

**Location:** `headroom/transforms/smart_crusher.py`

**Current Logic (lines 2273-2279, 2353-2359, 2561-2567):**
```python
# Always keep first 3
for i in range(min(3, n)):
    keep_indices.add(i)

# Always keep last 2
for i in range(max(0, n - 2), n):
    keep_indices.add(i)
```

**Problems:**
1. **Fixed slots waste budget** - If first 3 items are identical, we've wasted 3 slots
2. **No size adaptation** - 20-item array loses 25% to anchors; 1000-item array loses 0.5%
3. **Pattern-agnostic** - Search results don't need "last 2"; time series might need more recency
4. **No learning** - Doesn't adapt based on what users actually retrieve

---

## Proposed Solution: Adaptive Slot Allocation

### Idea 1: Size-Proportional Anchor Budget

Instead of fixed counts, allocate a **percentage budget** for position-based anchors:

```python
def calculate_anchor_budget(array_size: int, max_items: int) -> AnchorBudget:
    """Allocate slots proportionally, with floors and ceilings."""

    # Base: 20% of output budget for position anchors
    total_anchor_slots = max(3, min(10, int(max_items * 0.20)))

    # Distribution: 60% front, 40% back (front-weighted for context)
    front_slots = max(1, int(total_anchor_slots * 0.6))
    back_slots = max(1, total_anchor_slots - front_slots)

    return AnchorBudget(front=front_slots, back=back_slots)
```

**Example:**
| Array Size | Max Items | Anchor Budget | Front | Back |
|------------|-----------|---------------|-------|------|
| 50         | 10        | 3             | 2     | 1    |
| 200        | 15        | 3             | 2     | 1    |
| 1000       | 20        | 4             | 3     | 1    |
| 5000       | 30        | 6             | 4     | 2    |

---

### Idea 2: Pattern-Aware Anchor Weighting

Different data patterns need different position importance:

```python
class AnchorStrategy(Enum):
    FRONT_HEAVY = "front_heavy"     # Search results: top items matter most
    BACK_HEAVY = "back_heavy"       # Logs: recent items matter most
    BALANCED = "balanced"           # Time series: both ends matter
    MIDDLE_AWARE = "middle_aware"   # Database: order might be arbitrary

def get_anchor_strategy(pattern: DataPattern) -> AnchorStrategy:
    return {
        DataPattern.SEARCH_RESULTS: AnchorStrategy.FRONT_HEAVY,  # Top N by score
        DataPattern.LOGS: AnchorStrategy.BACK_HEAVY,             # Recency matters
        DataPattern.TIME_SERIES: AnchorStrategy.BALANCED,        # Both ends for trend
        DataPattern.GENERIC: AnchorStrategy.MIDDLE_AWARE,        # Don't assume order
    }.get(pattern, AnchorStrategy.BALANCED)
```

**FRONT_HEAVY (Search Results):**
- Front: 80% of anchor budget
- Back: 20% of anchor budget
- Rationale: Top search results are ranked by relevance

**BACK_HEAVY (Logs):**
- Front: 20% of anchor budget
- Back: 80% of anchor budget
- Rationale: Most recent logs are usually most relevant

**BALANCED (Time Series):**
- Front: 50% of anchor budget
- Back: 50% of anchor budget
- Rationale: Need both start and end for trend analysis

**MIDDLE_AWARE (Generic/Database):**
- Front: 30% of anchor budget
- Back: 30% of anchor budget
- Middle sample: 40% of anchor budget (stratified)
- Rationale: Order might be arbitrary; sample across positions

---

### Idea 3: Query-Aware Dynamic Weighting

Adjust anchor strategy based on user's query:

```python
def adjust_for_query(base_strategy: AnchorStrategy, query: str) -> AnchorWeights:
    """Shift anchor weights based on query intent."""

    weights = base_strategy.default_weights()

    # Recency signals
    recency_keywords = ["latest", "recent", "last", "newest", "current"]
    if any(kw in query.lower() for kw in recency_keywords):
        weights.back_weight *= 1.5
        weights.front_weight *= 0.7

    # Historical signals
    historical_keywords = ["first", "oldest", "earliest", "original", "initial"]
    if any(kw in query.lower() for kw in historical_keywords):
        weights.front_weight *= 1.5
        weights.back_weight *= 0.7

    # Range signals
    range_keywords = ["all", "every", "complete", "full"]
    if any(kw in query.lower() for kw in range_keywords):
        weights.middle_weight *= 1.3  # Better coverage

    return weights.normalize()
```

---

### Idea 4: Information-Density Anchor Selection

Don't blindly take first N - select **most informative** items from anchor regions:

```python
def select_informative_anchors(
    items: list[dict],
    region: str,  # "front", "back", "middle"
    slots: int,
    all_items_hash: set[str]
) -> list[int]:
    """Select most informative items from a region."""

    if region == "front":
        candidates = list(range(min(slots * 3, len(items))))  # Consider 3x candidates
    elif region == "back":
        start = max(0, len(items) - slots * 3)
        candidates = list(range(start, len(items)))
    else:  # middle
        step = len(items) // (slots * 3 + 1)
        candidates = [i * step for i in range(1, slots * 3 + 1)]

    # Score each candidate by information content
    scored = []
    for idx in candidates:
        item = items[idx]
        item_hash = hash_item(item)

        # Skip if we've seen identical item
        if item_hash in all_items_hash:
            continue

        score = calculate_information_score(item, items)
        scored.append((idx, score, item_hash))

    # Select top N by information score
    scored.sort(key=lambda x: x[1], reverse=True)
    selected = []
    for idx, _, item_hash in scored[:slots]:
        selected.append(idx)
        all_items_hash.add(item_hash)

    return sorted(selected)


def calculate_information_score(item: dict, all_items: list[dict]) -> float:
    """Score item by how much unique information it contributes."""

    score = 0.0

    # 1. Field uniqueness - rare field values score higher
    for field, value in item.items():
        field_values = [i.get(field) for i in all_items if field in i]
        value_frequency = field_values.count(value) / len(field_values)
        score += (1 - value_frequency)  # Rare values score higher

    # 2. Structural uniqueness - different fields than typical
    typical_fields = get_typical_fields(all_items)
    unique_fields = set(item.keys()) - typical_fields
    score += len(unique_fields) * 0.5

    # 3. Content length - longer items often more informative
    content_length = len(json.dumps(item))
    avg_length = sum(len(json.dumps(i)) for i in all_items) / len(all_items)
    if content_length > avg_length:
        score += 0.3

    return score
```

---

### Idea 5: TOIN-Learned Position Importance

Track which positions users actually retrieve and learn from it:

```python
@dataclass
class PositionRetrievalPattern:
    """Learned position importance from retrieval data."""
    tool_name: str
    total_compressions: int
    position_retrievals: dict[str, int]  # "front_10%", "middle", "back_10%"

    def get_position_weights(self) -> dict[str, float]:
        """Convert retrieval counts to weights."""
        total = sum(self.position_retrievals.values())
        if total == 0:
            return {"front": 0.5, "middle": 0.0, "back": 0.5}

        return {
            position: count / total
            for position, count in self.position_retrievals.items()
        }


class TOINPositionLearning:
    """Learn position importance from retrieval patterns."""

    def record_retrieval(
        self,
        tool_name: str,
        original_size: int,
        retrieved_indices: list[int]
    ):
        """Record which positions were retrieved."""
        for idx in retrieved_indices:
            position = self._classify_position(idx, original_size)
            self._increment_position_count(tool_name, position)

    def _classify_position(self, idx: int, size: int) -> str:
        """Classify index into position bucket."""
        relative_pos = idx / size
        if relative_pos < 0.1:
            return "front_10%"
        elif relative_pos < 0.3:
            return "front_30%"
        elif relative_pos > 0.9:
            return "back_10%"
        elif relative_pos > 0.7:
            return "back_30%"
        else:
            return "middle"

    def get_anchor_recommendation(self, tool_name: str) -> AnchorWeights:
        """Get learned anchor weights for a tool."""
        pattern = self._get_pattern(tool_name)
        if pattern.total_compressions < 10:
            return AnchorWeights.default()  # Not enough data

        weights = pattern.get_position_weights()
        return AnchorWeights(
            front=weights.get("front_10%", 0.3) + weights.get("front_30%", 0.1),
            middle=weights.get("middle", 0.2),
            back=weights.get("back_10%", 0.3) + weights.get("back_30%", 0.1),
        )
```

---

### Idea 6: Stratified Sampling for Middle Positions

For large arrays, sample strategically from middle:

```python
def stratified_middle_sample(
    items: list[dict],
    num_samples: int,
    analysis: ArrayAnalysis
) -> list[int]:
    """Sample middle positions using stratified approach."""

    n = len(items)
    front_boundary = int(n * 0.1)
    back_boundary = int(n * 0.9)
    middle_items = list(range(front_boundary, back_boundary))

    if not middle_items or num_samples <= 0:
        return []

    # Strategy 1: Cluster-based sampling
    if analysis.has_clusterable_field:
        clusters = cluster_by_field(items, analysis.cluster_field)
        return sample_from_clusters(clusters, num_samples, middle_items)

    # Strategy 2: Variance-based sampling (pick high-variance points)
    if analysis.numeric_fields:
        variance_scores = calculate_position_variance(items, analysis.numeric_fields)
        sorted_by_variance = sorted(
            middle_items,
            key=lambda i: variance_scores.get(i, 0),
            reverse=True
        )
        return sorted(sorted_by_variance[:num_samples])

    # Strategy 3: Even spacing (fallback)
    step = len(middle_items) // (num_samples + 1)
    return [middle_items[i * step] for i in range(1, num_samples + 1)]
```

---

## Testing Strategy

### Test Category 1: Adversarial Position Tests

Test cases where important data is NOT at expected positions:

```python
class TestAdversarialPositions:
    """Test scenarios that break 'first 3 + last 2' assumption."""

    def test_important_data_in_middle(self):
        """Critical error at position 50 of 100-item array."""
        items = [{"status": "ok", "value": i} for i in range(100)]
        items[50] = {"status": "error", "error_code": "CRITICAL", "value": 50}

        result = smart_crusher.crush(items, max_items=10)

        # Error item MUST be preserved regardless of position
        assert any(item.get("error_code") == "CRITICAL" for item in result)

    def test_spike_not_at_boundaries(self):
        """Numeric spike at position 75 of 100-item array."""
        items = [{"metric": 10.0 + random.random()} for _ in range(100)]
        items[75]["metric"] = 1000.0  # Spike

        result = smart_crusher.crush(items, max_items=10)

        # Spike MUST be preserved as anomaly
        assert any(item["metric"] > 500 for item in result)

    def test_first_items_identical(self):
        """First 10 items are identical - shouldn't waste slots."""
        items = [{"id": "same", "value": 0}] * 10 + [
            {"id": f"unique_{i}", "value": i} for i in range(90)
        ]

        result = smart_crusher.crush(items, max_items=10)

        # Should NOT have multiple identical items
        ids = [item["id"] for item in result]
        # At most 1-2 of the identical items, not 3
        assert ids.count("same") <= 2

    def test_last_items_identical(self):
        """Last 10 items are identical - shouldn't waste slots."""
        items = [{"id": f"unique_{i}", "value": i} for i in range(90)] + [
            {"id": "same", "value": 100}
        ] * 10

        result = smart_crusher.crush(items, max_items=10)

        ids = [item["id"] for item in result]
        assert ids.count("same") <= 2

    def test_relevant_item_in_middle(self):
        """Item matching user query is in middle of array."""
        items = [{"name": f"item_{i}", "status": "active"} for i in range(100)]
        items[42]["name"] = "target_item"
        items[42]["description"] = "This is what user asked about"

        result = smart_crusher.crush(
            items,
            max_items=10,
            query="find target_item"
        )

        # Query-matched item MUST be preserved
        assert any("target_item" in item.get("name", "") for item in result)
```

### Test Category 2: Size Adaptation Tests

```python
class TestSizeAdaptation:
    """Test that anchor allocation scales with array size."""

    @pytest.mark.parametrize("size,expected_min_anchors", [
        (20, 3),    # Small array: at least 3 anchors
        (100, 4),   # Medium array: at least 4 anchors
        (500, 5),   # Large array: at least 5 anchors
        (2000, 6),  # Very large: at least 6 anchors
    ])
    def test_anchor_count_scales(self, size, expected_min_anchors):
        """Anchor count should increase with array size."""
        items = [{"id": i, "value": i * 10} for i in range(size)]

        result = smart_crusher.crush(items, max_items=20)

        # Count items from first 10% and last 10%
        anchor_count = sum(
            1 for item in result
            if item["id"] < size * 0.1 or item["id"] > size * 0.9
        )

        assert anchor_count >= expected_min_anchors

    def test_small_array_high_preservation(self):
        """Small arrays should preserve higher percentage."""
        items = [{"id": i} for i in range(15)]

        result = smart_crusher.crush(items, max_items=20)

        # Should preserve most/all of small array
        assert len(result) >= 10  # At least 66%

    def test_large_array_efficient_sampling(self):
        """Large arrays should sample efficiently."""
        items = [{"id": i, "value": i} for i in range(5000)]

        result = smart_crusher.crush(items, max_items=20)

        # Should have good distribution across positions
        positions = [item["id"] for item in result]

        has_front = any(p < 500 for p in positions)
        has_middle = any(500 < p < 4500 for p in positions)
        has_back = any(p > 4500 for p in positions)

        assert has_front and has_back
        # Middle should be represented if array is large enough
        assert has_middle
```

### Test Category 3: Pattern-Specific Tests

```python
class TestPatternAwareAnchoring:
    """Test pattern-specific anchor strategies."""

    def test_search_results_front_heavy(self):
        """Search results should preserve more from front."""
        items = [
            {"title": f"Result {i}", "score": 1.0 - (i * 0.01)}
            for i in range(100)
        ]

        result = smart_crusher.crush(items, max_items=10)

        # More items should be from front (high scores)
        front_count = sum(1 for item in result if item["score"] > 0.9)
        back_count = sum(1 for item in result if item["score"] < 0.1)

        assert front_count > back_count

    def test_logs_back_heavy(self):
        """Logs should preserve more from back (recent)."""
        items = [
            {"timestamp": f"2024-01-{i:02d}", "level": "INFO", "message": f"Log {i}"}
            for i in range(1, 31)
        ]

        result = smart_crusher.crush(items, max_items=10)

        # More items should be from back (recent logs)
        timestamps = [item["timestamp"] for item in result]
        recent_count = sum(1 for ts in timestamps if int(ts[-2:]) > 20)
        old_count = sum(1 for ts in timestamps if int(ts[-2:]) < 10)

        assert recent_count >= old_count

    def test_time_series_balanced(self):
        """Time series should have balanced front/back."""
        items = [
            {"timestamp": f"2024-01-01T{i:02d}:00:00", "value": 100 + i}
            for i in range(24)
        ]

        result = smart_crusher.crush(items, max_items=8)

        hours = [int(item["timestamp"][11:13]) for item in result]
        front_count = sum(1 for h in hours if h < 8)
        back_count = sum(1 for h in hours if h > 16)

        # Should be roughly balanced
        assert abs(front_count - back_count) <= 2
```

### Test Category 4: Query-Aware Tests

```python
class TestQueryAwareAnchoring:
    """Test query-based anchor adjustment."""

    def test_latest_query_shifts_to_back(self):
        """'Latest' in query should preserve more recent items."""
        items = [{"id": i, "created": f"2024-01-{i:02d}"} for i in range(1, 31)]

        result = smart_crusher.crush(
            items,
            max_items=8,
            query="Show me the latest entries"
        )

        ids = [item["id"] for item in result]
        recent_count = sum(1 for id in ids if id > 20)

        assert recent_count >= 3  # At least 3 recent items

    def test_first_query_shifts_to_front(self):
        """'First' in query should preserve earlier items."""
        items = [{"id": i, "created": f"2024-01-{i:02d}"} for i in range(1, 31)]

        result = smart_crusher.crush(
            items,
            max_items=8,
            query="Show me the first entries"
        )

        ids = [item["id"] for item in result]
        early_count = sum(1 for id in ids if id < 10)

        assert early_count >= 3

    def test_specific_id_query_finds_item(self):
        """Query for specific ID should find it regardless of position."""
        items = [{"id": f"item_{i:04d}", "value": i} for i in range(1000)]

        result = smart_crusher.crush(
            items,
            max_items=10,
            query="Find item_0567"
        )

        assert any(item["id"] == "item_0567" for item in result)
```

### Test Category 5: Coverage Metrics Tests

```python
class TestCoverageMetrics:
    """Test that preserved items represent the full distribution."""

    def test_value_range_coverage(self):
        """Preserved items should cover the value range."""
        items = [{"value": i} for i in range(100)]

        result = smart_crusher.crush(items, max_items=10)

        values = [item["value"] for item in result]

        # Should cover most of the range
        assert min(values) < 10  # Has low values
        assert max(values) > 90  # Has high values

        # Should have some middle values too
        middle_count = sum(1 for v in values if 30 < v < 70)
        assert middle_count >= 1

    def test_category_coverage(self):
        """Preserved items should represent all categories."""
        items = [
            {"category": cat, "id": i}
            for i, cat in enumerate(["A"] * 30 + ["B"] * 30 + ["C"] * 40)
        ]

        result = smart_crusher.crush(items, max_items=10)

        categories = set(item["category"] for item in result)

        # Should have at least 2 of 3 categories
        assert len(categories) >= 2

    def test_temporal_coverage(self):
        """Preserved items should span the time range."""
        items = [
            {"timestamp": f"2024-{m:02d}-15", "event": f"event_{i}"}
            for i, m in enumerate(range(1, 13))
        ]

        result = smart_crusher.crush(items, max_items=5)

        months = [int(item["timestamp"][5:7]) for item in result]

        # Should span at least 6 months of the year
        assert max(months) - min(months) >= 6
```

### Test Category 6: Retrieval Simulation Tests

```python
class TestRetrievalSimulation:
    """Simulate user retrieval patterns to measure effectiveness."""

    def test_retrieval_hit_rate_random_queries(self):
        """Measure how often preserved items satisfy random queries."""
        items = [
            {"id": i, "name": f"Item {i}", "category": f"cat_{i % 5}"}
            for i in range(100)
        ]

        compressed = smart_crusher.crush(items, max_items=15)
        compressed_ids = {item["id"] for item in compressed}

        # Simulate 100 random "queries" (random item lookups)
        hits = 0
        for _ in range(100):
            target_id = random.randint(0, 99)
            if target_id in compressed_ids:
                hits += 1

        # Should hit at least 15% (we keep 15 of 100)
        assert hits >= 15

    def test_retrieval_hit_rate_weighted_queries(self):
        """Measure hits for queries weighted toward common patterns."""
        items = [{"id": i, "value": i * 10} for i in range(100)]

        compressed = smart_crusher.crush(items, max_items=15)
        compressed_ids = {item["id"] for item in compressed}

        # Weight queries toward front (30%), back (30%), anomalies (40%)
        hits = 0
        queries = (
            list(range(10)) * 3 +  # Front queries
            list(range(90, 100)) * 3 +  # Back queries
            [50] * 4  # Middle anomaly queries
        )

        for target_id in queries:
            if target_id in compressed_ids:
                hits += 1

        # Should hit more often than random due to anchor strategy
        assert hits >= 20  # At least 20% hit rate
```

---

## Implementation Phases

### Phase 1: Refactor Anchor Logic (Foundation)

1. Extract anchor selection into `AnchorSelector` class
2. Make slot counts configurable via `AnchorConfig`
3. Add size-proportional allocation
4. Maintain backward compatibility with current defaults

### Phase 2: Pattern-Aware Anchoring

1. Map `DataPattern` to `AnchorStrategy`
2. Implement front-heavy, back-heavy, balanced, middle-aware strategies
3. Add pattern-specific anchor weight configs

### Phase 3: Information-Density Selection

1. Add `calculate_information_score()` for items
2. Select from candidate region instead of fixed positions
3. Deduplicate identical items across regions

### Phase 4: Query-Aware Adjustment

1. Parse query for position intent keywords
2. Adjust anchor weights dynamically
3. Add query-position relevance scoring

### Phase 5: TOIN Position Learning

1. Track retrieval positions in TOIN
2. Learn per-tool position importance
3. Use learned weights to adjust anchor strategy

### Phase 6: Comprehensive Testing

1. Implement all adversarial tests
2. Add coverage metric tests
3. Add retrieval simulation tests
4. Performance benchmarks

---

## Configuration Schema

```python
@dataclass
class AnchorConfig:
    """Configuration for dynamic anchor allocation."""

    # Base anchor budget as percentage of max_items
    anchor_budget_pct: float = 0.20  # 20% of slots for position anchors

    # Minimum and maximum anchor slots
    min_anchor_slots: int = 3
    max_anchor_slots: int = 10

    # Default distribution (overridden by pattern)
    default_front_weight: float = 0.5
    default_back_weight: float = 0.5
    default_middle_weight: float = 0.0

    # Pattern-specific overrides
    search_front_weight: float = 0.8
    logs_back_weight: float = 0.8
    time_series_balance: float = 0.5

    # Query keyword detection
    recency_keywords: list[str] = field(default_factory=lambda: [
        "latest", "recent", "last", "newest", "current"
    ])
    historical_keywords: list[str] = field(default_factory=lambda: [
        "first", "oldest", "earliest", "original", "initial"
    ])

    # Information density selection
    use_information_density: bool = True
    candidate_multiplier: int = 3  # Consider 3x candidates per slot

    # TOIN learning
    use_learned_positions: bool = True
    min_samples_for_learning: int = 10
```

---

## Success Metrics

1. **Retrieval Coverage**: % of user retrievals that hit preserved items (target: >80%)
2. **Information Density**: Unique information per preserved slot (target: no duplicate items)
3. **Distribution Coverage**: Preserved items span full value/time/category ranges
4. **Adversarial Robustness**: All adversarial tests pass
5. **Backward Compatibility**: Existing tests still pass
6. **Performance**: <5ms additional latency for anchor selection

---

## Risks and Mitigations

| Risk | Mitigation |
|------|------------|
| Information density calculation is expensive | Cache scores, limit candidate pool |
| Query keyword detection is brittle | Use as soft signal, not hard rule |
| TOIN learning needs cold start | Fall back to pattern-based defaults |
| Breaking existing behavior | Feature flag, A/B testing |
| Middle sampling misses important items | Always include anomalies/errors regardless |

---

## Next Steps

1. Review and approve this plan
2. Write failing tests first (TDD approach)
3. Implement Phase 1 (refactor foundation)
4. Iterate through phases with test validation
5. Benchmark against current implementation
6. A/B test in production with telemetry