openlatch-client 0.6.21

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
//! The zone evaluator's performance fixtures — and the numbers `ci/check-bench-gate.py`
//! turns into a red build.
//!
//! # Why this file exists
//!
//! The PRD quotes one envelope for the engine: **p50 66 µs / p99 183 µs at 2,000
//! atoms, ASCII**. Two documented ways to lose it, both of which leave every
//! conformance row green because neither changes a single verdict:
//!
//! 1. **`(?-u)` flips off on the `regex_lite` set.** A Unicode `\b` regresses the
//!    p99 fifteenfold. `tier1::compile_regex_set` pins `.unicode(false)` and says
//!    so; nothing but [`regex_unicode_boundary`] below notices when it stops.
//! 2. **The `keyword` fold moves, or loses its ASCII fast path.**
//!    `tier1::fold_keyword` carries a measured table — +0.5% on lowercase ASCII,
//!    +1.3% on mixed case, **+14.5% on an all-non-ASCII surface** — and until this
//!    file existed that table lived in a doc comment. A doc comment does not fail
//!    a build. The three `keyword_*` fixtures are that table, made executable.
//!
//! Correctness is not what moves here. **Cost is**, which is why the corpus cannot
//! be the gate for either one.
//!
//! # The four groups, and which two are gated
//!
//! | group | layer | gated |
//! | --- | --- | --- |
//! | `zone_eval_load` | `bundle::load` — one automaton and one `RegexSet` built | no, amortised |
//! | `zone_eval_scan` | `ScanTable::scan` — the automata pass, once per event | **yes** |
//! | `zone_eval_evaluate` | `evaluate()` end to end, 2,000 mixed atoms | **yes** |
//! | `zone_eval_evaluate_wide` | `evaluate()` over a 40-value surface | **yes, own ceiling** |
//!
//! Bundle load is paid once per bundle refresh — a job stage. Per-event evaluation
//! is paid on every tool call the developer makes, all day. Conflating them
//! produces a number that cannot tell an author which of the two they made worse.
//! The whole tier-1 design — one aho-corasick automaton and one `RegexSet`
//! compiled at load, each run ONCE per event, with the predicate trees reading
//! booleans out of the shared result — exists precisely to move work across that
//! line.
//!
//! `zone_eval_evaluate_wide` costs ~7 ms — ~40x the per-event budget — for a reason
//! that has nothing to do with the automata and everything to do with
//! `field_values` running once per leaf. It is held to its own, looser ceiling in a
//! second `check-bench-gate.py` invocation rather than left ungated, because an
//! ungated group is how a real regression gets swallowed. Read its doc comment
//! before changing which groups the gate holds to which budget.
//!
//! # Why 2,000 atoms and 40 values
//!
//! 2,000 is the scale the 183 µs figure is quoted at; 40 values is the scan surface
//! width `fold_keyword`'s table was measured against. A bench at 20 atoms measures
//! the harness.
//!
//! Every fixture asserts it is **non-vacuous** before it is timed — a bundle that
//! silently skipped its artifacts, or an atom that matches nothing, would benchmark
//! an empty automaton and report a beautiful p99. That is the same failure the gate
//! script guards against one level up.

use std::hint::black_box;

use criterion::{criterion_group, criterion_main, Criterion};
use openlatch_client::zone_eval::{self, Bundle, Event};
use serde_json::{json, Value};

/// Injected, never read from a clock — the engine takes `now_ms` as a parameter
/// and a bench that passed it a real timestamp would drift its own fixtures.
const NOW_MS: i64 = 1_756_742_400_000;

/// The scale the 183 µs p99 is quoted at.
const ATOM_COUNT: usize = 2_000;

/// The scan surface width `fold_keyword`'s measured table was taken at.
const SURFACE_WIDTH: usize = 40;

// ── Fixture construction ─────────────────────────────────────────────

/// One Tier 1 artifact carrying one predicate tree.
fn t1_artifact(index: usize, node: Value) -> Value {
    json!({
        "artifact_id": format!("artifact-{index:04}"),
        "atom_id": format!("atom-{index:04}"),
        "kind": "t1_predicate_tree",
        "on_inconclusive": "ask",
        "tier": 1,
        "body": {
            "node": node,
            "verdict": "block",
            "reason": "bench fixture fired",
        },
    })
}

fn leaf(pred: &str, field: &str, value: Value) -> Value {
    json!({"op": "leaf", "leaf": {"pred": pred, "field": field, "value": value}})
}

fn regex_leaf(field: &str, pattern: &str) -> Value {
    json!({"op": "leaf", "leaf": {"pred": "regex_lite", "field": field, "pattern": pattern}})
}

/// The bundle envelope. `enforcement_enabled` is true because a monitor-composed
/// bundle never joins, and a join that never runs is not the path being measured.
fn bundle_doc(artifacts: Vec<Value>) -> Value {
    json!({
        "schema_version": 2,
        "organization_id": "org-bench",
        "revision": 1,
        "built_at": "2026-09-01T00:00:00Z",
        "enforcement_enabled": true,
        "signature": null,
        "artifacts": artifacts,
    })
}

/// Load a fixture and refuse to hand back one that would measure nothing.
///
/// A bundle whose artifacts all landed in `skipped[]` loads fine, scans nothing
/// and benches at a few nanoseconds. Asserting the count here is what keeps a
/// typo in a fixture from reading as a performance win.
fn load(doc: Value) -> Bundle {
    let bundle = zone_eval::bundle::load(doc).expect("the bench fixture bundle loads");
    assert!(
        bundle.skipped.is_empty(),
        "fixture skipped {} artifact(s): {:?}",
        bundle.skipped.len(),
        bundle.skipped,
    );
    assert_eq!(
        bundle.artifacts.len(),
        ATOM_COUNT,
        "fixture must carry the full {ATOM_COUNT}-atom scale",
    );
    bundle
}

// ── The three scan surfaces from `fold_keyword`'s measured table ──────

/// Lowercase ASCII — the common case. `fold_keyword` borrows and never allocates.
///
/// Pure ASCII down to the punctuation, and that is not pedantry: the first draft
/// of this fixture separated its clauses with an em dash, which is a three-byte
/// character, which sent every value down the `caseless` arm. The "cheap" row
/// then measured the expensive one and the three fixtures collapsed into one.
/// [`assert_surface_shape`] is what now catches that.
fn surface_lowercase_ascii() -> Vec<String> {
    (0..SURFACE_WIDTH)
        .map(|i| format!("src/core/policy/module_{i:02}/handler.rs - a short line of prose"))
        .collect()
}

/// Mixed-case ASCII. Every value carries an uppercase byte, so every value takes
/// the `to_ascii_lowercase` arm: one allocation, one byte mask per character.
fn surface_mixed_case_ascii() -> Vec<String> {
    (0..SURFACE_WIDTH)
        .map(|i| format!("Src/Core/Policy/Module_{i:02}/Handler.rs - A Short Line Of Prose"))
        .collect()
}

/// Which arm of `fold_keyword` a surface is supposed to exercise.
#[derive(Clone, Copy)]
enum FoldArm {
    /// No uppercase ASCII, no high byte: borrowed, zero allocation.
    Borrowed,
    /// ASCII with an uppercase byte: `to_ascii_lowercase`, one byte mask per char.
    AsciiLower,
    /// Non-ASCII: `caseless`, one folding-table walk per character.
    Caseless,
}

/// Prove a surface takes the arm its name claims, before it is timed.
///
/// These three fixtures are only meaningful as a set — the point is the *delta*
/// between them — and a single stray non-ASCII character in the "cheap" surface
/// silently makes two of them the same measurement. That is not hypothetical; it
/// is what the em dash in the first draft did. The assertion mirrors
/// `tier1::fold_keyword`'s own branch conditions, so it fails if either moves.
fn assert_surface_shape(name: &str, values: &[String], arm: FoldArm) {
    for value in values {
        let has_upper = value.bytes().any(|b| b.is_ascii_uppercase());
        let ascii = value.is_ascii();
        let ok = match arm {
            FoldArm::Borrowed => ascii && !has_upper,
            FoldArm::AsciiLower => ascii && has_upper,
            FoldArm::Caseless => !ascii,
        };
        assert!(
            ok,
            "surface '{name}' takes the wrong fold_keyword arm on {value:?}"
        );
    }
}

/// The three rows of `fold_keyword`'s table, each checked against its arm.
fn folding_surfaces() -> Vec<(&'static str, Vec<String>)> {
    let surfaces = vec![
        ("keyword_lowercase_ascii", surface_lowercase_ascii()),
        ("keyword_mixed_case_ascii", surface_mixed_case_ascii()),
        ("keyword_non_ascii", surface_non_ascii()),
    ];
    let arms = [FoldArm::Borrowed, FoldArm::AsciiLower, FoldArm::Caseless];
    for ((name, values), arm) in surfaces.iter().zip(arms) {
        assert_surface_shape(name, values, arm);
    }
    surfaces
}

/// Every value non-ASCII — the worst case by construction, and the row that costs
/// 14.5% of the p99 budget. `caseless` walks a folding table per character where
/// `to_ascii_lowercase` is a byte mask. The three folding classes are all here on
/// purpose: `ß`→`ss` (length-changing), `fi`→`fi` (decomposing), and `ΣΊΣΥΦΟΣ`,
/// whose final sigma folds to `σ` where `to_lowercase` would contextually give `ς`.
fn surface_non_ascii() -> Vec<String> {
    (0..SURFACE_WIDTH)
        .map(|i| format!("le café {i:02} est fermé — ΣΊΣΥΦΟΣ pousse la pierre, Straße finale"))
        .collect()
}

/// An event whose scan surface is exactly these values.
///
/// `Write` rather than `Bash` on purpose: a `Bash` event routes through the
/// tree-sitter shell walk, whose cost is identical across the three surfaces and
/// would only dilute the delta these fixtures exist to show.
fn surface_event(values: &[String]) -> Event {
    Event {
        event_type: "pre_tool_use".to_string(),
        session_id: "session-bench".to_string(),
        tool_name: "Write".to_string(),
        tool_input: json!({"values": values}),
        ..Event::default()
    }
}

// ── The bundles ──────────────────────────────────────────────────────

/// 2,000 `keyword` atoms — the shape `fold_keyword`'s table was measured against.
///
/// Every needle misses. That is the honest common case (most events match no rule)
/// and it is also the expensive one: a hit short-circuits nothing here, because the
/// automaton runs to the end of the value either way.
fn keyword_bundle() -> Bundle {
    let artifacts = (0..ATOM_COUNT)
        .map(|i| {
            t1_artifact(
                i,
                leaf(
                    "keyword",
                    "input.strings",
                    json!([format!("needle_{i:04}_absent")]),
                ),
            )
        })
        .collect();
    load(bundle_doc(artifacts))
}

/// 2,000 `regex_lite` atoms, every pattern carrying a `\b`.
///
/// **This fixture's whole job**: `tier1::compile_regex_set` pins `.unicode(false)`,
/// so these word boundaries are ASCII. Flip that and the same patterns become
/// Unicode-aware and the p99 regresses fifteenfold — a change no conformance row
/// can see, because the verdicts do not move.
fn regex_bundle() -> Bundle {
    let artifacts = (0..ATOM_COUNT)
        .map(|i| {
            t1_artifact(
                i,
                regex_leaf("input.strings", &format!(r"\bneedle_{i:04}\b")),
            )
        })
        .collect();
    load(bundle_doc(artifacts))
}

/// 2,000 atoms across the whole pattern vocabulary — the headline fixture, and the
/// one the 183 µs p99 is quoted for.
///
/// Atom 0 is a deliberate **hit** on `tool.name`, so the decide-and-join half of
/// the event path is measured and not just the scan.
fn mixed_node(i: usize) -> Value {
    match i % 5 {
        0 if i == 0 => leaf("keyword", "tool.name", json!(["bash"])),
        0 => leaf("keyword", "input.strings", json!([format!("kw_{i:04}")])),
        1 => leaf("prefix", "input.strings", json!([format!("pre_{i:04}/")])),
        2 => regex_leaf("input.strings", &format!(r"\brx_{i:04}\b")),
        3 => leaf("glob", "path.value", json!([format!("/opt/g_{i:04}/**")])),
        _ => leaf("equals", "tool.name", json!(format!("Tool{i:04}"))),
    }
}

fn mixed_bundle() -> Bundle {
    let artifacts = (0..ATOM_COUNT)
        .map(|i| t1_artifact(i, mixed_node(i)))
        .collect();
    load(bundle_doc(artifacts))
}

/// A realistic shell event: the classifier parses it, the path resolver places its
/// argument, and every one of the 2,000 trees then reads the shared scan result.
fn shell_event() -> Event {
    Event {
        event_type: "pre_tool_use".to_string(),
        session_id: "session-bench".to_string(),
        tool_name: "Bash".to_string(),
        tool_input: json!({
            "command": "rm -rf /data/warehouse/staging && aws s3 cp report.csv s3://acme-exports/",
        }),
        ..Event::default()
    }
}

/// Prove a fixture decides something before it is timed.
fn assert_decides(bundle: &Bundle, event: &Event) {
    let (decision, _) = zone_eval::evaluate(bundle, event, None, NOW_MS);
    assert!(
        decision.artifact_id.is_some(),
        "fixture matched no artifact — it would benchmark an empty path",
    );
}

// ── Group 1: bundle load, amortised across a job stage ────────────────

fn bench_load(c: &mut Criterion) {
    let keyword = bundle_doc(
        (0..ATOM_COUNT)
            .map(|i| {
                t1_artifact(
                    i,
                    leaf(
                        "keyword",
                        "input.strings",
                        json!([format!("needle_{i:04}_absent")]),
                    ),
                )
            })
            .collect(),
    );
    let regex = bundle_doc(
        (0..ATOM_COUNT)
            .map(|i| {
                t1_artifact(
                    i,
                    regex_leaf("input.strings", &format!(r"\bneedle_{i:04}\b")),
                )
            })
            .collect(),
    );

    let mixed = bundle_doc(
        (0..ATOM_COUNT)
            .map(|i| t1_artifact(i, mixed_node(i)))
            .collect(),
    );

    let mut group = c.benchmark_group("zone_eval_load");
    // Load is measured in milliseconds, not microseconds. The default 100 samples
    // would spend minutes of CI wall clock re-proving a number nothing gates.
    group.sample_size(10);
    group.bench_function("keyword_2000_atoms", |b| {
        b.iter(|| zone_eval::bundle::load(black_box(keyword.clone())).expect("loads"))
    });
    group.bench_function("regex_2000_atoms", |b| {
        b.iter(|| zone_eval::bundle::load(black_box(regex.clone())).expect("loads"))
    });
    // The realistic shape, and the one the platform's replay budget should be
    // reasoned about with: 400 of the 2,000 atoms carry a `regex_lite` pattern
    // rather than all of them.
    group.bench_function("mixed_2000_atoms", |b| {
        b.iter(|| zone_eval::bundle::load(black_box(mixed.clone())).expect("loads"))
    });
    group.finish();
}

// ── Group 2: the shared scan, once per event ──────────────────────────

/// `ScanTable::scan` in isolation — the automata pass and nothing else.
///
/// This is the layer `fold_keyword`'s table measured, so these four numbers are
/// directly comparable to the ones in that doc comment. The `zone_eval_evaluate`
/// group below carries the same surfaces through the whole event path.
fn bench_scan(c: &mut Criterion) {
    let keyword = keyword_bundle();
    let regex = regex_bundle();

    let surfaces = folding_surfaces();

    let mut group = c.benchmark_group("zone_eval_scan");
    for (name, values) in &surfaces {
        let event = surface_event(values);
        let facts = zone_eval::facts::resolve(&keyword, NOW_MS);
        let class = zone_eval::effect::classify(&event, &facts, keyword.catalogue.as_ref());
        let ctx = zone_eval::EvalContext::new(&event, &class, &facts, NOW_MS);
        group.bench_function(*name, |b| b.iter(|| keyword.scan.scan(black_box(&ctx))));
    }

    // The Unicode-`\b` fixture, at the layer where the 15× would land.
    let values = surface_lowercase_ascii();
    let event = surface_event(&values);
    let facts = zone_eval::facts::resolve(&regex, NOW_MS);
    let class = zone_eval::effect::classify(&event, &facts, regex.catalogue.as_ref());
    let ctx = zone_eval::EvalContext::new(&event, &class, &facts, NOW_MS);
    group.bench_function("regex_unicode_boundary", |b| {
        b.iter(|| regex.scan.scan(black_box(&ctx)))
    });
    group.finish();
}

// ── Group 3: the whole per-event path — what the p99 budget is about ──

/// The headline fixture, and the only gated one at this layer.
///
/// 2,000 atoms across the whole pattern vocabulary — keyword, prefix, `regex_lite`
/// with a `\b`, glob and equals — one shell event, one match, one join. It carries
/// 400 `\b` patterns, so a `(?-u)` flip lands here end-to-end as well as in
/// `zone_eval_scan`.
///
/// # How it relates to the PRD's "2,000 atoms at p50 66 µs / p99 183 µs"
///
/// It measures more than that figure does, and the split was probed directly:
///
/// | layer | measured |
/// | --- | --- |
/// | 2,000 predicate trees + the join, no shell, no strings | **77 µs** |
/// | the tree-sitter shell walk on one two-command pipeline | **106 µs** |
/// | this fixture, end to end | **319 µs** |
///
/// The evaluator core — the thing "2,000 atoms" names — lands at 77 µs, between
/// the quoted p50 and p99. The rest is the shell parse and the per-leaf
/// `field_values` pass described on [`bench_evaluate_wide`], both real per-event
/// costs and neither of them what that figure was describing. **The frozen NFR is
/// the one this gate enforces: p99 < 1 ms in-daemon** (PRD D17, and its own
/// instrumentation row). 66/183 µs is a rationale figure in the R4 cell whose
/// provenance is not in this repository — the spike worktree carries no benchmark
/// at all: no `criterion`, no `[[bench]]`, no `benches/`.
fn bench_evaluate(c: &mut Criterion) {
    let mixed = mixed_bundle();
    let shell = shell_event();
    assert_decides(&mixed, &shell);

    let mut group = c.benchmark_group("zone_eval_evaluate");
    group.bench_function("mixed_2000_atoms", |b| {
        b.iter(|| zone_eval::evaluate(black_box(&mixed), black_box(&shell), None, NOW_MS))
    });
    group.finish();
}

// ── Group 4: the wide surface — measured, reported, NOT gated ─────────

/// The same 2,000-atom keyword bundle as `zone_eval_scan`, carried through the
/// whole event path against a 40-value surface.
///
/// # What actually dominates this group — measured, not assumed
///
/// The name says "folding surfaces", and that is **not** what it measures. Three
/// probes at 2,000 atoms settle it (p50, this machine):
///
/// | `tool_input` shape | total bytes | measured |
/// | --- | --- | --- |
/// | 1 value | 60 B | 270 µs |
/// | 1 value | 2,400 B | 430 µs |
/// | **40 values** | **2,400 B** | **6,420 µs** |
///
/// Same bytes, 15x the cost, for nothing but splitting them across 40 values. The
/// driver is the value **count**, not the byte total: `tier1::leaf_attribute` calls
/// `field_values(field, ctx)` **once per leaf**, and `input.strings` walks
/// `tool_input` and clones every string it finds. 2,000 leaves x 40 values =
/// 80,000 `String` allocations to answer 2,000 booleans the shared scan already
/// computed, at a marginal **~79 ns per (leaf x value)**. The scan table amortises
/// the *automaton* pass across atoms; it does not amortise the *field* pass.
///
/// That is why the fold delta is unmeasurable here: +21 µs between the two
/// surfaces is **+515%** at `zone_eval_scan` and **+0.3%** here, inside a spread
/// that was 6.58-6.87 ms and 7.07-7.63 ms on one run. A fixture that cannot
/// resolve the regression it exists for is not a fixture, and that is the argument
/// for gating `fold_keyword` at `zone_eval_scan`.
///
/// # This shape is reachable, so it is gated — at its own budget
///
/// A 40-string `tool_input` is an ordinary payload, not a stress shape: a
/// `MultiEdit` carries two strings per edit, a `TodoWrite` two or three per item,
/// an MCP tool a string per element of any list it takes. The conformance corpus's
/// widest row is 3 strings, but that corpus is hand-authored spec fixtures and is
/// not a traffic sample.
///
/// So this group is **not** an ungated info line — that shape of escape hatch is
/// how a real regression gets swallowed. The CI job holds it to a **separate,
/// looser ceiling** in a second `check-bench-gate.py` invocation: a ratchet that
/// cannot get worse silently, on a cost this slice found but does not own. It
/// moves under the 1 ms budget with the rest the day the per-leaf `field_values`
/// call is hoisted out of the loop.
///
/// `keyword_mixed_case_ascii` is deliberately absent: at this layer it is a third
/// copy of the same ~7 ms, and the number it would add is the one above.
fn bench_evaluate_wide(c: &mut Criterion) {
    let keyword = keyword_bundle();
    let surfaces = folding_surfaces();

    let mut group = c.benchmark_group("zone_eval_evaluate_wide");
    // Milliseconds per iteration; the default 100 samples buys precision this
    // group does not gate on and costs CI minutes.
    group.sample_size(10);
    for (name, values) in surfaces
        .iter()
        .filter(|(name, _)| *name != "keyword_mixed_case_ascii")
    {
        let event = surface_event(values);
        group.bench_function(*name, |b| {
            b.iter(|| zone_eval::evaluate(black_box(&keyword), black_box(&event), None, NOW_MS))
        });
    }
    group.finish();
}

criterion_group!(
    benches,
    bench_load,
    bench_scan,
    bench_evaluate,
    bench_evaluate_wide
);
criterion_main!(benches);