openlatch-client 0.3.3

OpenLatch runtime enforcement node — the capture-and-enforce adapter that evaluates every covered action against a coding agent's Autonomy Zone before it runs
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
//! Net-effect accounting for a would-have transform (D-02).
//!
//! The `tokens_net` formula is **derived once in the PRD** ("`tokens_net` — the
//! derivation") and is deliberately NOT restated here — restating it is what
//! produced the B-1 defect (a dropped `+0.1·S` term that moved break-even from
//! 11.5·S to 12.5·S). This module implements the derivation as **exact
//! fixed-point arithmetic** so the fire/skip test (`tokens_net > 0`, PRD: decided
//! on the UNROUNDED value) never depends on stored precision. Correctness is
//! pinned by the net-boundary tests at both TTLs, not by re-deriving the formula
//! in a comment.
//!
//! `W` (the write multiplier in force for the request) is **read from the
//! request's own `cache_control` TTL** — 1.25 at the 5-minute TTL, 2.0 at the
//! 1-hour TTL — never assumed. See [`write_multiplier_for`].
//!
//! ## Two net models, never interchangeable
//!
//! [`tokens_net_hundredths`] is the **removal** model (L-1 / L-2). L-0 removes
//! nothing, so that formula is `≤ 0` for every L-0 input and would classify
//! every reorder `skipped_net_negative` — a category error, not a tuning
//! problem (C-3). L-0's own model is derived in the PRD ("`tokens_net` — the
//! derivation" → L-0, D-28) and implemented here as
//! [`insert_breakpoints_net_hundredths`] and
//! [`reorder_blocks_net_hundredths`]. Both take a measured **horizon** `H`
//! rather than being per-turn, because what L-0 buys is future cache *reads*
//! in exchange for one cache *write* — a quantity that only exists over turns.

use serde_json::Value;

/// The write multiplier `W` in force for a request, read from its `cache_control`
/// TTL (never assumed — F-25 / PRD "read it from the request; never assume").
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WriteMultiplier {
    /// 5-minute ephemeral TTL → `W = 1.25`. The default when the request pins no
    /// explicit TTL (Anthropic's `cache_control` default is the 5-minute bucket).
    FiveMinute,
    /// 1-hour ephemeral TTL → `W = 2.0`.
    OneHour,
}

impl WriteMultiplier {
    /// `W × 100` as an integer (`125` or `200`). The scale factor for exact
    /// fixed-point net arithmetic — `0.1` and `(W − 0.1)` both become integers.
    pub fn hundredths(self) -> i128 {
        match self {
            WriteMultiplier::FiveMinute => 125,
            WriteMultiplier::OneHour => 200,
        }
    }

    /// `W` as the wire numeric (`1.25` or `2.0`) for
    /// `ai.openlatch.transform.write_multiplier`.
    pub fn value(self) -> f64 {
        match self {
            WriteMultiplier::FiveMinute => 1.25,
            WriteMultiplier::OneHour => 2.0,
        }
    }
}

/// `tokens_net` scaled to **hundredths** as an exact `i128`, so the fire/skip test
/// is `tokens_net_hundredths(..) > 0` on the unrounded value (PRD requirement).
///
/// Implements the PRD derivation `tokens_net = 0.1·T − (W − 0.1)·S`; multiplying
/// through by 100 keeps every coefficient integral:
/// `100·tokens_net = 10·T − (100·W − 10)·S`, and `100·W ∈ {125, 200}`.
///
/// `tokens_gross` is `|T|` (tokens removed); `retained_tail` is `|S|` (the shifted
/// tail that must be re-written). Widened to `i128` so a large `S` can legitimately
/// drive the net negative without overflow or an unsigned wrap.
pub fn tokens_net_hundredths(tokens_gross: u64, retained_tail: u64, w: WriteMultiplier) -> i128 {
    let ten_t = 10i128 * i128::from(tokens_gross);
    let coef = w.hundredths() - 10; // (100·W − 10): 115 at 5m, 190 at 1h
    ten_t - coef * i128::from(retained_tail)
}

/// Convert an hundredths-scaled net to the wire numeric (`hundredths / 100`).
/// Exact to two decimal places, so it stores losslessly into `numeric(18,4)`.
pub fn hundredths_to_numeric(hundredths: i128) -> f64 {
    hundredths as f64 / 100.0
}

/// Determine `W` from the request body's `cache_control` TTL.
///
/// Scans the parsed body for any `cache_control` breakpoint pinned to the 1-hour
/// TTL (`"ttl": "1h"`); a single 1-hour breakpoint puts the request on the 2.0
/// write rate. **Defaults to the 5-minute multiplier (1.25)** when no explicit
/// TTL is present — matching Anthropic's own `cache_control` default bucket. `W`
/// is therefore always read from the request, never assumed.
pub fn write_multiplier_for(body: &Value) -> WriteMultiplier {
    if has_one_hour_ttl(body) {
        WriteMultiplier::OneHour
    } else {
        WriteMultiplier::FiveMinute
    }
}

/// True when the request carries a `cache_control` breakpoint pinned to the 1-hour
/// ephemeral TTL **at a structural position Anthropic actually honors** — a `system`
/// content block, a `messages[].content[]` block, or a `tools[]` entry. A
/// `cache_control` object anywhere else (buried inside a `tool_use` block's `input`,
/// inside message text, or any other arbitrary nested object) is NOT a breakpoint and
/// is ignored. Inspecting only these positions is what keeps a stray `{"ttl":"1h"}` in
/// tool arguments from wrongly flipping `W` to 2.0.
fn has_one_hour_ttl(body: &Value) -> bool {
    // `system`: only an array of content blocks carries breakpoints; a plain string
    // `system` has no breakpoint position.
    if let Some(blocks) = body.get("system").and_then(Value::as_array) {
        if blocks.iter().any(block_has_one_hour_breakpoint) {
            return true;
        }
    }
    // `messages[].content[]`: each content block may carry a breakpoint. `content` may
    // be a plain string (no breakpoint) or an array of blocks. We inspect only each
    // block's OWN `cache_control` — never recursing into a `tool_use` block's `input`.
    if let Some(messages) = body.get("messages").and_then(Value::as_array) {
        for msg in messages {
            if let Some(blocks) = msg.get("content").and_then(Value::as_array) {
                if blocks.iter().any(block_has_one_hour_breakpoint) {
                    return true;
                }
            }
        }
    }
    // `tools[]`: a tool definition may carry a breakpoint.
    if let Some(tools) = body.get("tools").and_then(Value::as_array) {
        if tools.iter().any(block_has_one_hour_breakpoint) {
            return true;
        }
    }
    false
}

/// True when a block/entry's OWN `cache_control` field is a 1-hour ephemeral
/// breakpoint (`{"type":"ephemeral","ttl":"1h"}`). Reads only the top-level
/// `cache_control` of the passed value — it never recurses into nested payloads such
/// as a `tool_use` block's `input`, so a `cache_control`-shaped object living inside
/// tool arguments is correctly ignored. Requires BOTH `type == "ephemeral"` and
/// `ttl == "1h"`.
fn block_has_one_hour_breakpoint(block: &Value) -> bool {
    let Some(cc) = block.get("cache_control") else {
        return false;
    };
    cc.get("type").and_then(Value::as_str) == Some("ephemeral")
        && cc.get("ttl").and_then(Value::as_str) == Some("1h")
}

// ---------------------------------------------------------------------------
// L-0 net models (D-28). Derived in the PRD; implemented — not re-derived — here.
// ---------------------------------------------------------------------------

/// `net` for `insert_breakpoints`, scaled to **hundredths** as an exact `i128`.
///
/// PRD: `net = |P| · [ 0.9·H − (W − 0.1) ]`, where `|P|` is the currently
/// **uncached** prefix region a new breakpoint would cover and `H` is the
/// measured horizon. Multiplying through by 100 keeps every coefficient
/// integral: `100·net = |P| · [ 90·H − (100·W − 10) ]`, and `100·W ∈ {125, 200}`.
///
/// Break-even is `H = (W − 0.1)/0.9` — **1.28 turns at the 5-minute TTL, 2.11 at
/// the 1-hour TTL**, so the lever fires from the 2nd and 3rd turn respectively.
/// Those are the same two numbers Anthropic publishes for caching break-even,
/// which is the check that this is the derivation rather than merely arithmetic.
///
/// `H = 1` (a first turn) is always negative — correct, because a session with
/// one turn has nothing to amortise a write over.
pub fn insert_breakpoints_net_hundredths(
    prefix_tokens: u64,
    horizon: u32,
    w: WriteMultiplier,
) -> i128 {
    let coef = 90i128 * i128::from(horizon) - (w.hundredths() - 10);
    i128::from(prefix_tokens) * coef
}

/// `net` for `reorder_blocks` **landed on a churn turn**, scaled to hundredths.
///
/// PRD: `net = H · (W − 0.1) · |S|`, where `|S|` is the stable content the
/// volatile block was sitting in front of — the region that moves from the
/// write side to the read side — and `H` is the measured count of volatile
/// turns behind us.
///
/// The transition is **free** here, and that is a precondition rather than an
/// assumption: on a turn where the volatile block changed anyway, everything
/// from its position was already going to be re-written, so re-ordering inside
/// an already-invalid region costs nothing. The caller must not use this for a
/// reorder landed on a quiet turn — that one costs
/// `(W − 0.1)·(|S| + |V|)` up front and breaks even only at `H = 2 + |V|/|S|`.
/// The engine does not land reorders on quiet turns, so that branch is not
/// implemented rather than implemented and unused.
///
/// `|V|` does not appear: the saving is governed entirely by how much stable
/// content the volatile block was in front of.
pub fn reorder_blocks_net_hundredths(stable_tokens: u64, horizon: u32, w: WriteMultiplier) -> i128 {
    let coef = w.hundredths() - 10; // (100·W − 10): 115 at 5m, 190 at 1h
    i128::from(horizon) * coef * i128::from(stable_tokens)
}

/// The **minimum horizon** at which `insert_breakpoints` is net-positive for `w`.
///
/// Derived from the same inequality rather than tabulated, so it cannot drift
/// away from [`insert_breakpoints_net_hundredths`]: the smallest integer `H`
/// with `90·H > 100·W − 10`.
pub fn insert_break_even_horizon(w: WriteMultiplier) -> u32 {
    let threshold = w.hundredths() - 10; // 115 at 5m, 190 at 1h
                                         // smallest H with 90·H > threshold  ⇒  H = floor(threshold / 90) + 1
    u32::try_from(threshold / 90 + 1).unwrap_or(u32::MAX)
}

// ---------------------------------------------------------------------------
// Breakpoint positions — the same three Anthropic honours, as positions not a bool
// ---------------------------------------------------------------------------

/// Which of the three honoured layers a `cache_control` breakpoint sits in.
///
/// Mirrors the layer set [`has_one_hour_ttl`] already walks. Kept distinct from
/// the wire `ChurnLayer` so this module stays free of generated types; the
/// acting engine maps between them.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BreakpointLayer {
    /// A `tools[]` entry.
    Tools,
    /// A `system[]` content block.
    System,
    /// A `messages[i].content[j]` block.
    Messages,
}

/// One `cache_control` breakpoint, located.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BreakpointPosition {
    /// Which layer it sits in.
    pub layer: BreakpointLayer,
    /// Index within that layer — the `tools[]`/`system[]` index, or the
    /// `messages[]` index for a message-content breakpoint.
    pub index: usize,
}

/// Every `cache_control` breakpoint in the body, at a position Anthropic
/// actually honours.
///
/// The sibling [`has_one_hour_ttl`] wanted, and the reason a bool was not
/// enough: acting needs to know **where** the breakpoints are (to respect the
/// four-breakpoint ceiling, to find whether a volatile block sits before one,
/// and to avoid inserting a second marker into a layer that already has one),
/// not merely whether any exists.
///
/// Walks exactly the three honoured positions, and — like
/// [`block_has_one_hour_breakpoint`] — reads only each block's **own**
/// `cache_control`. A `cache_control`-shaped object buried inside a `tool_use`
/// block's `input` is not a breakpoint and is not reported. Order is
/// deterministic: `tools`, then `system`, then `messages`, ascending index.
pub fn breakpoint_positions(body: &Value) -> Vec<BreakpointPosition> {
    let mut out = Vec::new();
    if let Some(tools) = body.get("tools").and_then(Value::as_array) {
        for (i, tool) in tools.iter().enumerate() {
            if block_has_breakpoint(tool) {
                out.push(BreakpointPosition {
                    layer: BreakpointLayer::Tools,
                    index: i,
                });
            }
        }
    }
    if let Some(blocks) = body.get("system").and_then(Value::as_array) {
        for (i, block) in blocks.iter().enumerate() {
            if block_has_breakpoint(block) {
                out.push(BreakpointPosition {
                    layer: BreakpointLayer::System,
                    index: i,
                });
            }
        }
    }
    if let Some(messages) = body.get("messages").and_then(Value::as_array) {
        for (i, msg) in messages.iter().enumerate() {
            if msg
                .get("content")
                .and_then(Value::as_array)
                .is_some_and(|blocks| blocks.iter().any(block_has_breakpoint))
            {
                out.push(BreakpointPosition {
                    layer: BreakpointLayer::Messages,
                    index: i,
                });
            }
        }
    }
    out
}

/// True when a block/entry's OWN `cache_control` field is any ephemeral
/// breakpoint, whatever its TTL.
///
/// The TTL-agnostic sibling of [`block_has_one_hour_breakpoint`]: that one
/// answers "does this put the request on the 2.0 write rate", this one answers
/// "is this position already a cache checkpoint". Same non-recursing read, for
/// the same reason.
pub fn block_has_breakpoint(block: &Value) -> bool {
    block
        .get("cache_control")
        .and_then(|cc| cc.get("type"))
        .and_then(Value::as_str)
        == Some("ephemeral")
}

/// Anthropic's hard ceiling on `cache_control` breakpoints in one request.
///
/// At the ceiling `insert_breakpoints` declines. It never displaces an existing
/// breakpoint to make room: that would be a **removal** inside a lever whose
/// entire safety argument is that it removes nothing, and it would silently
/// uncache whatever the displaced marker was covering (D-19).
pub const MAX_BREAKPOINTS: usize = 4;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn net_break_even_5m_skips_below_and_at_fires_above() {
        // 5-minute TTL (W = 1.25): break-even is T = 11.5·S (PRD). With S = 2,
        // 11.5·S = 23, so T = 23 lands exactly on break-even (net = 0 → skip),
        // T = 22 is below (net < 0 → skip), T = 24 is above (net > 0 → fire).
        let w = WriteMultiplier::FiveMinute;
        assert_eq!(
            tokens_net_hundredths(23, 2, w),
            0,
            "T = 11.5·S is break-even"
        );
        assert!(tokens_net_hundredths(22, 2, w) < 0, "just below break-even");
        assert!(tokens_net_hundredths(24, 2, w) > 0, "just above break-even");
    }

    #[test]
    fn net_break_even_1h_skips_below_and_at_fires_above() {
        // 1-hour TTL (W = 2.0): break-even is T = 19·S (PRD). With S = 2,
        // 19·S = 38, so T = 38 is break-even (net = 0 → skip), 37 below, 39 above.
        let w = WriteMultiplier::OneHour;
        assert_eq!(tokens_net_hundredths(38, 2, w), 0, "T = 19·S is break-even");
        assert!(tokens_net_hundredths(37, 2, w) < 0, "just below break-even");
        assert!(tokens_net_hundredths(39, 2, w) > 0, "just above break-even");
    }

    #[test]
    fn net_hundredths_are_exact_to_two_places() {
        // 0.1·10 − 1.15·1 = 1.0 − 1.15 = −0.15 → −15 hundredths, exactly.
        assert_eq!(
            tokens_net_hundredths(10, 1, WriteMultiplier::FiveMinute),
            -15
        );
        assert!((hundredths_to_numeric(-15) - (-0.15)).abs() < 1e-12);
    }

    #[test]
    fn w_defaults_to_five_minute_when_no_ttl() {
        // No cache_control at all → 5-minute default.
        let body = serde_json::json!({"model":"claude-opus-4-8","messages":[]});
        assert_eq!(write_multiplier_for(&body), WriteMultiplier::FiveMinute);

        // A cache_control breakpoint with no explicit ttl → still the 5m default.
        let ephemeral = serde_json::json!({
            "system":[{"type":"text","text":"x","cache_control":{"type":"ephemeral"}}],
            "messages":[]
        });
        assert_eq!(
            write_multiplier_for(&ephemeral),
            WriteMultiplier::FiveMinute
        );
    }

    #[test]
    fn w_reads_one_hour_ttl_from_the_request() {
        // An ephemeral 1-hour breakpoint on a `system` content block → the 2.0 write
        // rate. (A `system` string carries no breakpoint; a block does.)
        let body = serde_json::json!({
            "system":[{"type":"text","text":"x","cache_control":{"type":"ephemeral","ttl":"1h"}}],
            "messages":[{"role":"user","content":"hi"}]
        });
        assert_eq!(write_multiplier_for(&body), WriteMultiplier::OneHour);
        assert_eq!(write_multiplier_for(&body).value(), 2.0);
    }

    #[test]
    fn w_ignores_ttl_1h_inside_message_text_and_tool_use_input() {
        // A `cache_control` with a 1h ttl buried inside message TEXT content, or inside
        // a `tool_use` block's `input` object, is NOT an Anthropic-honored breakpoint —
        // it must not flip W. Both cases must resolve to the 5-minute default (1.25).
        let text_ttl = serde_json::json!({
            "messages":[
                {"role":"user","content":[
                    {"type":"text","text":"cache this {\"cache_control\":{\"type\":\"ephemeral\",\"ttl\":\"1h\"}}"}
                ]}
            ]
        });
        assert_eq!(write_multiplier_for(&text_ttl), WriteMultiplier::FiveMinute);
        assert_eq!(write_multiplier_for(&text_ttl).value(), 1.25);

        let tool_input_ttl = serde_json::json!({
            "messages":[
                {"role":"assistant","content":[
                    {"type":"tool_use","id":"t1","name":"lookup","input":{
                        "cache_control":{"type":"ephemeral","ttl":"1h"}
                    }}
                ]}
            ]
        });
        assert_eq!(
            write_multiplier_for(&tool_input_ttl),
            WriteMultiplier::FiveMinute
        );
        assert_eq!(write_multiplier_for(&tool_input_ttl).value(), 1.25);
    }

    #[test]
    fn w_reads_one_hour_ttl_from_a_message_content_block() {
        // A real ephemeral 1h breakpoint carried on a `messages[].content[]` block →
        // the 2.0 write rate.
        let body = serde_json::json!({
            "messages":[
                {"role":"user","content":[
                    {"type":"text","text":"hi","cache_control":{"type":"ephemeral","ttl":"1h"}}
                ]}
            ]
        });
        assert_eq!(write_multiplier_for(&body), WriteMultiplier::OneHour);
        assert_eq!(write_multiplier_for(&body).value(), 2.0);
    }

    // --- L-0 net models (D-28) -------------------------------------------------

    #[test]
    fn insert_breakpoints_break_even_is_turn_2_at_5m_and_turn_3_at_1h() {
        // The PRD's headline numbers, and Anthropic's own published break-even:
        // 1.25 + 0.1 = 1.35 < 2 uncached (so 2 requests pay off at 5m);
        // 2.0 + 0.2 = 2.2 < 3 uncached  (so 3 requests pay off at 1h).
        let p = 1_000u64;

        let w = WriteMultiplier::FiveMinute;
        assert!(
            insert_breakpoints_net_hundredths(p, 1, w) < 0,
            "turn 1 is always a loss"
        );
        assert!(
            insert_breakpoints_net_hundredths(p, 2, w) > 0,
            "5m pays off on turn 2"
        );
        assert_eq!(insert_break_even_horizon(w), 2);

        let w = WriteMultiplier::OneHour;
        assert!(insert_breakpoints_net_hundredths(p, 1, w) < 0);
        assert!(
            insert_breakpoints_net_hundredths(p, 2, w) < 0,
            "the doubled 1h write is NOT repaid by turn 2"
        );
        assert!(
            insert_breakpoints_net_hundredths(p, 3, w) > 0,
            "1h pays off on turn 3"
        );
        assert_eq!(insert_break_even_horizon(w), 3);
    }

    #[test]
    fn insert_breakpoints_turn_one_loss_is_exactly_w_minus_one() {
        // net(H=1) = |P|·[0.9 − (W − 0.1)] = |P|·(1 − W): −0.25·|P| at 5m, −1.0·|P| at 1h.
        assert_eq!(
            insert_breakpoints_net_hundredths(100, 1, WriteMultiplier::FiveMinute),
            -2_500, // −0.25 × 100 tokens, in hundredths
        );
        assert_eq!(
            insert_breakpoints_net_hundredths(100, 1, WriteMultiplier::OneHour),
            -10_000, // −1.00 × 100 tokens
        );
    }

    #[test]
    fn insert_breakpoints_steady_saving_is_ninety_percent_of_the_region() {
        // Each turn beyond break-even adds 0.9·|P| — 90% of that region's input
        // cost, which is the whole reason the lever is worth a write.
        let w = WriteMultiplier::FiveMinute;
        let step = insert_breakpoints_net_hundredths(200, 6, w)
            - insert_breakpoints_net_hundredths(200, 5, w);
        assert_eq!(step, 90 * 200);
    }

    #[test]
    fn reorder_net_is_positive_for_any_horizon_and_ignores_the_moved_block() {
        // Landed on a churn turn the transition is free, so the net is positive
        // from H = 1 — and |V| does not appear in the formula at all.
        for h in 1..=5u32 {
            for w in [WriteMultiplier::FiveMinute, WriteMultiplier::OneHour] {
                assert!(reorder_blocks_net_hundredths(500, h, w) > 0);
            }
        }
        // 1h saves more per volatile turn than 5m: (2.0 − 0.1) vs (1.25 − 0.1).
        assert_eq!(
            reorder_blocks_net_hundredths(100, 1, WriteMultiplier::FiveMinute),
            11_500, // 1.15 × 100, in hundredths
        );
        assert_eq!(
            reorder_blocks_net_hundredths(100, 1, WriteMultiplier::OneHour),
            19_000, // 1.90 × 100
        );
    }

    #[test]
    fn reorder_with_no_stable_content_behind_it_saves_nothing() {
        // |S| = 0 is the "the move crosses no breakpoint" case: exactly zero,
        // never a small positive the engine could talk itself into firing on.
        for h in 1..=5u32 {
            assert_eq!(
                reorder_blocks_net_hundredths(0, h, WriteMultiplier::OneHour),
                0
            );
        }
    }

    #[test]
    fn the_removal_formula_is_a_category_error_for_l0() {
        // The guard C-3 describes, asserted rather than asserted-in-a-comment:
        // a reorder removes nothing (T = 0), so the removal net is ≤ 0 for every
        // input — every L-0 decision would read `skipped_net_negative`.
        for s in [0u64, 1, 100, 10_000] {
            for w in [WriteMultiplier::FiveMinute, WriteMultiplier::OneHour] {
                assert!(tokens_net_hundredths(0, s, w) <= 0);
            }
        }
        // …while L-0's own model says the same input is worth acting on.
        assert!(reorder_blocks_net_hundredths(10_000, 2, WriteMultiplier::FiveMinute) > 0);
    }

    // --- breakpoint positions ---------------------------------------------------

    #[test]
    fn positions_walk_the_three_honoured_layers_in_order() {
        let body = serde_json::json!({
            "tools": [
                {"name": "a"},
                {"name": "b", "cache_control": {"type": "ephemeral"}}
            ],
            "system": [
                {"type": "text", "text": "s0", "cache_control": {"type": "ephemeral", "ttl": "1h"}},
                {"type": "text", "text": "s1"}
            ],
            "messages": [
                {"role": "user", "content": [
                    {"type": "text", "text": "hi", "cache_control": {"type": "ephemeral"}}
                ]}
            ]
        });
        assert_eq!(
            breakpoint_positions(&body),
            vec![
                BreakpointPosition {
                    layer: BreakpointLayer::Tools,
                    index: 1
                },
                BreakpointPosition {
                    layer: BreakpointLayer::System,
                    index: 0
                },
                BreakpointPosition {
                    layer: BreakpointLayer::Messages,
                    index: 0
                },
            ]
        );
    }

    #[test]
    fn positions_ignore_a_cache_control_shaped_object_inside_tool_input() {
        // Same non-recursing rule as `block_has_one_hour_breakpoint`: a
        // breakpoint-shaped object in tool arguments is data, not a checkpoint.
        let body = serde_json::json!({
            "messages": [
                {"role": "assistant", "content": [
                    {"type": "tool_use", "id": "t1", "name": "lookup", "input": {
                        "cache_control": {"type": "ephemeral"}
                    }}
                ]}
            ]
        });
        assert!(breakpoint_positions(&body).is_empty());
    }

    #[test]
    fn positions_are_empty_for_a_string_system_and_string_content() {
        // A string-valued `system` (or message `content`) has no block position,
        // so it carries no breakpoint and offers none to insert into.
        let body = serde_json::json!({
            "system": "a plain string prompt",
            "messages": [{"role": "user", "content": "hi"}]
        });
        assert!(breakpoint_positions(&body).is_empty());
    }

    #[test]
    fn block_has_breakpoint_is_ttl_agnostic_unlike_its_one_hour_sibling() {
        let five_min = serde_json::json!({"cache_control": {"type": "ephemeral"}});
        let one_hour = serde_json::json!({"cache_control": {"type": "ephemeral", "ttl": "1h"}});
        assert!(block_has_breakpoint(&five_min));
        assert!(block_has_breakpoint(&one_hour));
        assert!(!block_has_one_hour_breakpoint(&five_min));
        assert!(block_has_one_hour_breakpoint(&one_hour));
    }
}