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
//! Cross-provider ranking policy (`SPEC.md` §6.6, F10).
//!
//! `score` is **provider-local and ordinal**: it orders one provider's frames
//! against one query, and nothing in the protocol makes two providers' numbers
//! commensurable. A host still has to put the frames in *some* order — a prompt
//! is a sequence, and a budget is spent front-first — so every host is making a
//! cross-provider ranking decision whether or not it admits to one. F10's rule
//! is that the decision is the host's policy, named as such.
//!
//! [`RankingStrategy`] is where that policy lives. [`super::compose_for_prompt`]
//! keeps ranking by raw `score` ([`ScoreDescending`]) so an existing host's
//! output does not move; [`super::compose_for_prompt_with`] takes any strategy,
//! and two that need no configuration ship here:
//!
//! - [`RoundRobinByRank`] — every provider's best frame, then every provider's
//! second, and so on. Uses only within-provider rank, where the ordering is
//! meaningful.
//! - [`PerProviderQuota`] — the same idea in blocks of `k`: each provider's top
//! `k`, then each provider's next `k`.
//!
//! # Why raw score starves a provider
//!
//! Consider a semantic-search provider that reports cosine similarity in the
//! `0.8`–`0.95` band and a lexical provider that reports a normalized BM25 rank
//! topping out near `0.4`. Both are honest about their own frames. Rank the
//! union by raw `score` under a budget that fits four frames and the lexical
//! provider contributes nothing — not because its evidence is worse, but
//! because its retriever's number is smaller. The prompt's evidence set was
//! decided by an implementation detail of someone else's scorer.
//! `starvation_*` in this module's tests is that scenario, run.
//!
//! # Determinism
//!
//! Ranking must be a pure function of the input *set*: two runs over the same
//! frames must produce the same order, or a host's prompt stops being
//! reproducible and its provider prompt cache stops hitting (`super`'s module
//! docs). Every strategy here derives its provider ordering from a
//! [`BTreeMap`] and breaks every remaining tie on the canonical
//! [`FrameId`], so no ordering ever depends on
//! hash iteration or arrival order.
use Ordering;
use BTreeMap;
use NonZeroUsize;
use ;
/// A host's policy for ordering frames drawn from more than one provider —
/// the choice `SPEC.md` §6.6 (F10) says a host owns and must name.
///
/// A strategy reports a **permutation of the input indices, best first**. It
/// ranks; it never filters. Dropping evidence is the budget packer's job in
/// [`super::compose_for_prompt_with`], which excludes a frame with a recorded
/// [`ExclusionReason`](super::ExclusionReason) so the audit still explains
/// every drop. A strategy that quietly returned fewer indices would put frames
/// into neither the prompt nor the audit.
///
/// # Contract
///
/// - `order(frames)` returns each index in `0..frames.len()` exactly once.
/// - The result depends only on `frames` as a set — not on their arrival
/// order, and not on anything that varies between two runs over the same
/// input. A `HashMap` iteration is the usual way to get this wrong.
///
/// [`rank_with`] holds the first half rather than trusting it: an index out of
/// range or repeated is skipped, and any frame a strategy failed to place is
/// appended in canonical order, so a third-party strategy cannot make the
/// reference host lose evidence. It repairs rather than panicking — a library
/// that aborted a host's turn over a ranking bug would be a worse failure than
/// the one it caught. [`is_ranking_permutation`] is the assertion to put in a
/// strategy's own tests.
/// Apply a [`RankingStrategy`], returning the frames best-first.
///
/// The strategy's permutation is checked, not trusted: an out-of-range or
/// repeated index is skipped and anything the strategy left unplaced is
/// appended in canonical order (`score` descending, `FrameId` ascending). The
/// output is therefore always a permutation of the input, whoever wrote the
/// strategy — which is what keeps the composition audit a total partition of
/// the evidence the host offered.
Sized>
/// Whether `order` is exactly the indices `0..n`, each once — the
/// [`RankingStrategy`] contract, as an assertion a strategy's own tests can
/// make.
///
/// [`rank_with`] repairs a violation rather than calling this, because a host
/// mid-turn needs its evidence more than it needs a panic. That leaves a
/// strategy author with nothing to fail on, which is what this is for.
/// The canonical within-provider ordering: `score` descending, canonical
/// [`FrameId`] ascending as the tiebreak.
///
/// Comparing `score` here is comparing two frames **from one provider**, which
/// is the only comparison F10 says means anything. Every strategy in this
/// module uses it for exactly that, and never to rank one provider's frame
/// against another's.
/// One frame's position in the lane structure every interleaving strategy
/// ranks over: which provider it came from, and how good it is *within that
/// provider*.
/// Each frame's [`Lane`].
///
/// Providers are ordinalized through a [`BTreeMap`], so the traversal is by
/// provider id and never by hash order — the determinism this module's docs
/// promise. Ranks come from [`by_score_desc`] applied inside one provider,
/// which is the only place F10 says a score comparison means anything.
///
/// The provider ordinal is **arbitrary but stable**, and deliberately so: once
/// two frames sit in the same tier, the protocol offers nothing that ranks one
/// provider above another, and reaching for `score` there would be exactly the
/// cross-provider comparison F10 says is not a measurement. An alphabetical
/// order admits it is a coin toss; a score comparison would dress one up as a
/// judgement.
/// Sort `0..frames.len()` by a per-frame key, canonical `FrameId` ascending as
/// the standing final tiebreak so the order is total and reproducible.
///
/// The keys are computed once rather than inside the comparator, so a strategy
/// pays for one `identity()` per frame instead of one per comparison.
/// Rank the whole mixed set by raw `score`, descending — the reference host's
/// documented default (`SPEC.md` §6.6), and what
/// [`super::order_by_value`] has always done.
///
/// It is a real policy with a real cost: the provider that scores most
/// generously wins the top of the prompt and the front of the budget, whatever
/// its evidence is worth. It stays the default because it is the only strategy
/// here that changes nothing for a host that never asked for a ranking policy,
/// and because with a single provider it is simply the right answer — see
/// [ADR 0015](https://github.com/macanderson/context-graph-protocol/blob/main/docs/adr/0015-cross-provider-ranking-strategies.md).
;
/// Interleave providers by **within-provider rank**: every provider's best
/// frame first, then every provider's second-best, and so on.
///
/// The only score comparisons are within one provider, where F10 says the
/// ordering is meaningful. Across providers the order is by provider id, which
/// is arbitrary and stable rather than a claim that one source outranks
/// another.
///
/// With one provider the ranks are `0, 1, 2, …` in score order, so this is
/// identical to [`ScoreDescending`] — the cross-provider question does not
/// arise, and no strategy here invents one.
;
/// Give every provider its top `k` before any provider gets its `k + 1`th:
/// each provider's best `k` frames, then each provider's next `k`, and so on.
///
/// The difference from [`RoundRobinByRank`] is contiguity. A round robin deals
/// one frame per provider per turn; a quota deals `k` at a time, so a
/// provider's block of evidence stays together in the prompt. `k = 1` is a
/// round robin.
///
/// The quota **tiers, it does not truncate.** A provider's `k + 1`th frame is
/// ranked lower, never dropped: dropping is the budget packer's decision, and
/// it records a reason for the audit. A strategy that discarded frames would
/// leave them out of both the prompt and the record of why.
///
/// With one provider every frame sits in its own tier position in score order,
/// so this too degenerates to [`ScoreDescending`].