tapes-client 0.2.0

One client for the whole tapes read surface: the sealed contract and a deployment's discovered cassettes, driven through a single pluggable transport.
Documentation
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
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
//! The schema-coverage gate.
//!
//! # What this gate is for
//!
//! [`crate::core::coverage`] catches an operation the contract grew and the
//! client never exposed. This one catches the quieter half of the same failure:
//! an operation whose *shape* grew — a field added to `SessionItem`, a schema
//! added beside it — while the models kept decoding happily, dropping the new
//! data on the floor. Nothing fails at runtime when that happens. The response
//! still parses; the field is simply never seen again.
//!
//! So the models are held to the vendored document mechanically:
//!
//! 1. **Every schema is accounted for.** Modelled, or allow-listed with the
//!    reason it is not — the same partition, and the same failure, as the
//!    operation gate.
//! 2. **Every property survives a round trip.** A document synthesised from the
//!    schema is decoded into the model and re-serialised; anything the model
//!    does not carry comes back missing, and is reported by path.
//! 3. **The decoding rules hold.** A schema's optional properties really are
//!    optional (the whole document decodes from `{}`), a required one really is
//!    required, and a composite property really does tolerate `null` — see
//!    [`super`] for why each of those matters.
//!
//! The synthesised document is the trick that makes this work without a
//! hand-written description of each model. A hand-written one would be a second
//! copy of the contract, kept by hand, which is the thing being prevented.
//! Serde is the description: what the model can carry is exactly what survives
//! decoding and re-encoding.
//!
//! # Why the tables live here and not with the consumer
//!
//! Deliberately the opposite of [`crate::core::coverage`], and for the same
//! reason. Coverage of *operations* is a statement about one client's surface,
//! so sharing the tables would make the gate report on a union and protect
//! nobody. Coverage of *schemas* is a statement about these models, which ship
//! in this crate — so the tables ship with them, and a consumer gets the gate
//! by depending on the crate rather than by maintaining a copy of it.

use std::collections::BTreeSet;
use std::fmt;

use serde_json::{Map, Value, json};

use super::ContractModel;
use super::params::{ContractEnum, ContractParams};
use crate::core::contract::{TAPES_API_YAML, core};
use crate::error::{Result, error};
use snafu::OptionExt;

/// A coverage table: schema name paired with prose for the reviewer.
pub type Table<'a> = &'a [(&'a str, &'a str)];

/// Schemas this crate deliberately does not model, and why.
///
/// The cassette surface models the discovery document itself — partially and on
/// purpose, since a deployment's configuration is not part of the generated
/// command surface. Modelling it a second time here is exactly the duplication
/// this crate exists to end, so these are allow-listed rather than copied.
pub const UNMODELLED: Table<'static> = &[
    (
        "Discovery",
        "modelled by the cassette surface, which reads only the fields it acts on",
    ),
    (
        "DiscoveryEntry",
        "part of the discovery document; see Discovery",
    ),
    (
        "DiscoveryDepends",
        "part of the discovery document; see Discovery",
    ),
    (
        "DiscoverySetting",
        "part of the discovery document; see Discovery",
    ),
    ("Rejection", "part of the discovery document; see Discovery"),
];

/// One modelled schema, and the checks its model can be put through.
#[derive(Clone, Copy)]
pub struct Entry {
    schema: &'static str,
    run: fn(&Value, &Map<String, Value>) -> Vec<String>,
}

impl Entry {
    /// Register one model against the schema it claims.
    #[must_use]
    pub fn of<M: ContractModel>() -> Self {
        Self {
            schema: M::SCHEMA,
            run: audit::<M>,
        }
    }

    /// The schema this entry covers.
    #[must_use]
    pub fn schema(&self) -> &'static str {
        self.schema
    }
}

impl fmt::Debug for Entry {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Entry")
            .field("schema", &self.schema)
            .finish()
    }
}

/// Every schema this crate models, in one table.
///
/// The registry is a function rather than a `const` so a model is registered by
/// naming its type — `Entry::of::<SessionItem>()` — which cannot disagree with
/// the type's own [`ContractModel::SCHEMA`] the way a repeated string could.
#[must_use]
pub fn registry() -> Vec<Entry> {
    use super::{admin, protocol, raw_turn, session, skill, span, trace};
    vec![
        Entry::of::<session::SessionItem>(),
        Entry::of::<session::SessionRollup>(),
        Entry::of::<session::SessionUsage>(),
        Entry::of::<session::ModelUsage>(),
        Entry::of::<session::TreeTask>(),
        Entry::of::<session::SessionListResponse>(),
        Entry::of::<session::SessionDetailResponse>(),
        Entry::of::<session::SessionTracesResponse>(),
        Entry::of::<session::SessionUpdateRequest>(),
        Entry::of::<trace::TraceItem>(),
        Entry::of::<trace::TraceUsage>(),
        Entry::of::<trace::MainUsage>(),
        Entry::of::<trace::TraceDetail>(),
        Entry::of::<trace::TraceListResponse>(),
        Entry::of::<span::SpanItem>(),
        Entry::of::<span::SpanLinkItem>(),
        Entry::of::<span::SpanSearchOutput>(),
        Entry::of::<span::SpanSearchResult>(),
        Entry::of::<raw_turn::RawTurnHeaderItem>(),
        Entry::of::<raw_turn::RawTurnListResponse>(),
        Entry::of::<raw_turn::RawTurnAttribution>(),
        Entry::of::<raw_turn::RawTurnAttributionRepairRequest>(),
        Entry::of::<raw_turn::RawTurnAttributionRepairResult>(),
        Entry::of::<raw_turn::RepairPendingSession>(),
        Entry::of::<skill::SkillResponse>(),
        Entry::of::<skill::SkillsListResponse>(),
        Entry::of::<skill::SkillCounts>(),
        Entry::of::<skill::SkillVersionResponse>(),
        Entry::of::<skill::SkillVersionsResponse>(),
        Entry::of::<skill::SessionSkillsResponse>(),
        Entry::of::<skill::CreateSkillRequest>(),
        Entry::of::<skill::UpdateSkillRequest>(),
        Entry::of::<skill::PublishSkillRequest>(),
        Entry::of::<skill::GenerateSkillRequest>(),
        Entry::of::<admin::SeedResult>(),
        Entry::of::<admin::SeedDemoRequest>(),
        Entry::of::<admin::DeriveRunResponse>(),
        Entry::of::<admin::RederiveReport>(),
        Entry::of::<admin::ReconcileStats>(),
        Entry::of::<admin::StatsResponse>(),
        Entry::of::<protocol::ErrorResponse>(),
        Entry::of::<protocol::McpRequest>(),
        Entry::of::<protocol::McpResponse>(),
        Entry::of::<protocol::McpError>(),
    ]
}

/// What a coverage run found wrong.
///
/// Every category is reported at once, because a gate that surfaces one problem
/// per run turns a contract bump into a sequence of runs.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct SchemaReport {
    /// Schemas in the contract that are neither modelled nor allow-listed.
    pub unmodelled: Vec<String>,
    /// Schemas named by a table that the contract does not have.
    pub stale: Vec<String>,
    /// Schemas both modelled and allow-listed.
    pub contradictory: Vec<String>,
    /// Ways a model disagreed with the schema it claims.
    pub disagreements: Vec<String>,
}

impl SchemaReport {
    /// Whether the models and the contract agree.
    #[must_use]
    pub fn is_clean(&self) -> bool {
        self.unmodelled.is_empty()
            && self.stale.is_empty()
            && self.contradictory.is_empty()
            && self.disagreements.is_empty()
    }
}

impl fmt::Display for SchemaReport {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if !self.unmodelled.is_empty() {
            write!(
                f,
                "schemas in the vendored tapes-api contract that this crate neither models nor \
                 allow-lists: {:?} — add a model (and register it) or allow-list it with the \
                 reason it stays unmodelled. ",
                self.unmodelled,
            )?;
        }
        if !self.stale.is_empty() {
            write!(
                f,
                "schemas named by a coverage table that the vendored contract does not have: \
                 {:?} — the contract dropped or renamed them, and the models must move in the \
                 same change. ",
                self.stale,
            )?;
        }
        if !self.contradictory.is_empty() {
            write!(
                f,
                "schemas both modelled and allow-listed: {:?}. ",
                self.contradictory
            )?;
        }
        for disagreement in &self.disagreements {
            write!(f, "{disagreement} ")?;
        }
        Ok(())
    }
}

/// Compare this crate's models against the vendored contract's schemas.
///
/// Returns the report whether or not it is clean; [`check`] is the assertion
/// form.
///
/// # Errors
///
/// Fails only when the vendored contract cannot be read at all, which this
/// crate's contract tests catch long before.
pub fn report(modelled: &[Entry], unmodelled: Table<'_>) -> Result<SchemaReport> {
    let schemas = schemas()?;
    let known: BTreeSet<&str> = schemas.keys().map(String::as_str).collect();
    let modelled_ids: BTreeSet<&str> = modelled.iter().map(|entry| entry.schema).collect();
    let unmodelled_ids: BTreeSet<&str> = unmodelled.iter().map(|(id, _)| *id).collect();

    let owned =
        |ids: BTreeSet<&str>| -> Vec<String> { ids.into_iter().map(ToOwned::to_owned).collect() };

    let mut disagreements = Vec::new();
    for entry in modelled {
        let Some(schema) = schemas.get(entry.schema) else {
            continue; // reported as stale below
        };
        disagreements.extend((entry.run)(schema, &schemas));
    }

    Ok(SchemaReport {
        unmodelled: owned(
            known
                .iter()
                .filter(|id| !modelled_ids.contains(*id) && !unmodelled_ids.contains(*id))
                .copied()
                .collect(),
        ),
        stale: owned(
            modelled_ids
                .union(&unmodelled_ids)
                .filter(|id| !known.contains(*id))
                .copied()
                .collect(),
        ),
        contradictory: owned(
            modelled_ids
                .intersection(&unmodelled_ids)
                .copied()
                .collect(),
        ),
        disagreements,
    })
}

/// The assertion form of [`report`], over this crate's own tables.
///
/// # Errors
///
/// The rendered report, when the models and the contract disagree.
pub fn check() -> std::result::Result<(), String> {
    let report = report(&registry(), UNMODELLED).map_err(|error| error.to_string())?;
    if report.is_clean() {
        return Ok(());
    }
    Err(report.to_string())
}

/// Hold one operation's parameter type to the parameters the contract declares.
///
/// `params` must be fully populated: the check is two-directional, so a value
/// left unset reads as a parameter the type cannot express.
///
/// # Errors
///
/// A message naming every parameter the type sends that the contract does not
/// declare, and every non-path parameter the contract declares that the type
/// cannot send.
pub fn check_params<P: ContractParams>(params: &P) -> std::result::Result<(), String> {
    let surface = core().map_err(|error| error.to_string())?;
    let method = surface.method(P::OPERATION).map_err(|e| e.to_string())?;
    let declared: BTreeSet<&str> = method
        .params
        .iter()
        .filter(|param| param.location != crate::cassettes::spec::Location::Path)
        .map(|param| param.wire.as_str())
        .collect();
    let sent: BTreeSet<&str> = params.values().into_iter().map(|(wire, _)| wire).collect();

    let undeclared: Vec<&str> = sent.difference(&declared).copied().collect();
    let unsendable: Vec<&str> = declared.difference(&sent).copied().collect();
    if undeclared.is_empty() && unsendable.is_empty() {
        return Ok(());
    }
    Err(format!(
        "{} parameters disagree with the contract: sends {undeclared:?} which the contract does \
         not declare; cannot send {unsendable:?} which it does.",
        P::OPERATION,
    ))
}

/// One Rust enum's claim on a contract-declared value set.
#[derive(Debug, Clone, Copy)]
pub struct ClaimedEnum {
    declared_by: &'static [(&'static str, &'static str)],
    values: &'static [&'static str],
}

impl ClaimedEnum {
    /// Register one parameter enum.
    #[must_use]
    pub fn of<E: ContractEnum>() -> Self {
        Self {
            declared_by: E::DECLARED_BY,
            values: E::VALUES,
        }
    }
}

/// Hold the parameter enums to the value sets the contract closes.
///
/// Two-directional, like the schema gate: a value the contract added and the
/// Rust enum lacks is unreachable from a typed call site, and a
/// contract-declared set that no enum claims is a parameter still spelled by
/// hand.
///
/// # Errors
///
/// A message naming every disagreement.
pub fn check_enums(claimed: &[ClaimedEnum]) -> std::result::Result<(), String> {
    let document = document().map_err(|error| error.to_string())?;
    let mut problems = Vec::new();
    let mut covered: BTreeSet<(&str, &str)> = BTreeSet::new();

    for claim in claimed {
        for (operation, parameter) in claim.declared_by {
            covered.insert((operation, parameter));
            problems.extend(compare_enum(&document, claim, operation, parameter));
        }
    }

    for (operation, parameter, _) in every_declared_enum(&document) {
        if !covered.contains(&(operation.as_str(), parameter.as_str())) {
            problems.push(format!(
                "{operation}'s {parameter} closes a value set that no typed enum claims."
            ));
        }
    }

    if problems.is_empty() {
        return Ok(());
    }
    Err(problems.join(" "))
}

/// The vendored document, parsed.
fn document() -> Result<Value> {
    serde_yaml::from_str(TAPES_API_YAML)
        .ok()
        .context(error::VendoredContractSnafu {
            surface: "tapes-api",
        })
}

/// The vendored document's `components.schemas`.
fn schemas() -> Result<Map<String, Value>> {
    let document = document()?;
    document
        .get("components")
        .and_then(|components| components.get("schemas"))
        .and_then(Value::as_object)
        .cloned()
        .context(error::VendoredContractSnafu {
            surface: "tapes-api",
        })
}

/// One claim against one declaration, as a problem or nothing.
fn compare_enum(
    document: &Value,
    claim: &ClaimedEnum,
    operation: &str,
    parameter: &str,
) -> Option<String> {
    let Some(values) = declared_enum(document, operation, parameter) else {
        return Some(format!(
            "{operation}'s {parameter} is claimed as a closed set, but the contract declares no \
             enum for it."
        ));
    };
    let ours: BTreeSet<&str> = claim.values.iter().copied().collect();
    let theirs: BTreeSet<&str> = values.iter().map(String::as_str).collect();
    if ours == theirs {
        return None;
    }
    Some(format!(
        "{operation}'s {parameter} accepts {theirs:?} but the typed enum offers {ours:?}."
    ))
}

/// The `enum` a document declares for one operation's parameter, if any.
fn declared_enum(document: &Value, operation: &str, parameter: &str) -> Option<Vec<String>> {
    every_declared_enum(document)
        .into_iter()
        .find(|(op, name, _)| op == operation && name == parameter)
        .map(|(_, _, values)| values)
}

/// Every `(operation, parameter, values)` the document closes with an `enum`.
fn every_declared_enum(document: &Value) -> Vec<(String, String, Vec<String>)> {
    let mut found = Vec::new();
    for (operation, _, params) in operations(document) {
        for param in params {
            let Some(name) = param.get("name").and_then(Value::as_str) else {
                continue;
            };
            let Some(values) = param
                .get("schema")
                .and_then(|schema| schema.get("enum"))
                .and_then(Value::as_array)
            else {
                continue;
            };
            found.push((
                operation.clone(),
                name.to_owned(),
                values
                    .iter()
                    .filter_map(Value::as_str)
                    .map(ToOwned::to_owned)
                    .collect(),
            ));
        }
    }
    found
}

/// `(operationId, path, parameters)` for every operation in the document.
fn operations(document: &Value) -> Vec<(String, String, Vec<Value>)> {
    let mut found = Vec::new();
    let Some(paths) = document.get("paths").and_then(Value::as_object) else {
        return found;
    };
    for (path, item) in paths {
        let Some(item) = item.as_object() else {
            continue;
        };
        for operation in item.values() {
            let Some(id) = operation_id(operation) else {
                continue;
            };
            let params = operation
                .get("parameters")
                .and_then(Value::as_array)
                .cloned()
                .unwrap_or_default();
            found.push((id, path.clone(), params));
        }
    }
    found
}

fn operation_id(operation: &Value) -> Option<String> {
    operation
        .get("operationId")
        .and_then(Value::as_str)
        .map(ToOwned::to_owned)
}

/// Put one model through every check the schema supports.
fn audit<M: ContractModel>(schema: &Value, schemas: &Map<String, Value>) -> Vec<String> {
    let name = M::SCHEMA;
    let mut problems = Vec::new();
    let populated = sample(schema, schemas, 0);

    // 1. Everything the schema declares survives decode + re-encode.
    match serde_json::from_value::<M>(populated.clone()) {
        Err(error) => problems.push(format!(
            "{name} does not decode a document built from its own schema: {error}.",
        )),
        Ok(model) => match serde_json::to_value(&model) {
            Err(error) => problems.push(format!("{name} does not re-encode: {error}.")),
            Ok(encoded) => survived(name, &populated, &encoded, &mut problems),
        },
    }

    // 2. Optional properties really are optional; required ones really are
    //    required. The contract declares required-ness per schema, so this
    //    reads it rather than assuming today's answer (which is "none").
    let required: Vec<&str> = schema
        .get("required")
        .and_then(Value::as_array)
        .map(|names| names.iter().filter_map(Value::as_str).collect())
        .unwrap_or_default();
    let mut minimal = Map::new();
    for property in &required {
        if let Some(value) = populated.get(*property) {
            minimal.insert((*property).to_owned(), value.clone());
        }
    }
    if serde_json::from_value::<M>(Value::Object(minimal)).is_err() {
        problems.push(format!(
            "{name} does not decode a document carrying only the properties the contract \
             requires; an optional property is modelled as mandatory.",
        ));
    }
    for property in &required {
        let mut without = populated.as_object().cloned().unwrap_or_default();
        without.remove(*property);
        if serde_json::from_value::<M>(Value::Object(without)).is_ok() {
            problems.push(format!(
                "{name}.{property} is required by the contract but decodes when absent.",
            ));
        }
    }

    // 3. A composite property tolerates an explicit null.
    for (property, declared) in properties(schema) {
        if !is_composite(declared) {
            continue;
        }
        let mut nulled = populated.as_object().cloned().unwrap_or_default();
        nulled.insert(property.clone(), Value::Null);
        if serde_json::from_value::<M>(Value::Object(nulled)).is_err() {
            problems.push(format!(
                "{name}.{property} does not tolerate a null; a nil map, slice, or struct pointer \
                 the server did not omit would blank the whole response.",
            ));
        }
    }

    problems
}

/// Report every value that did not survive the round trip, by path.
fn survived(path: &str, sent: &Value, back: &Value, problems: &mut Vec<String>) {
    match (sent, back) {
        (Value::Object(sent), Value::Object(back)) => {
            for (key, value) in sent {
                match back.get(key) {
                    None => problems.push(format!(
                        "{path}.{key} is in the contract but not carried by the model.",
                    )),
                    Some(got) => survived(&format!("{path}.{key}"), value, got, problems),
                }
            }
        }
        (Value::Array(sent), Value::Array(back)) => {
            for (index, value) in sent.iter().enumerate() {
                match back.get(index) {
                    None => problems.push(format!("{path}[{index}] was dropped by the model.")),
                    Some(got) => survived(&format!("{path}[{index}]"), value, got, problems),
                }
            }
        }
        (sent, back) if sent != back => {
            problems.push(format!("{path} decoded as {back} rather than {sent}."));
        }
        _ => {}
    }
}

/// A schema's declared properties, resolving one level of `$ref`.
fn properties(schema: &Value) -> Vec<(String, &Value)> {
    schema
        .get("properties")
        .and_then(Value::as_object)
        .map(|props| props.iter().map(|(k, v)| (k.clone(), v)).collect())
        .unwrap_or_default()
}

/// Whether a property is one of the positions a `null` can legitimately arrive
/// in: an array, a map, an object, or another schema.
fn is_composite(schema: &Value) -> bool {
    if schema.get("$ref").is_some() {
        return true;
    }
    match schema.get("type").and_then(Value::as_str) {
        Some("array" | "object") => true,
        Some(_) => false,
        // An untyped schema accepts anything, `null` included.
        None => true,
    }
}

fn resolve<'a>(schema: &Value, schemas: &'a Map<String, Value>) -> Option<&'a Value> {
    let name = schema.get("$ref")?.as_str()?.rsplit('/').next()?;
    schemas.get(name)
}

/// Build a document that exercises every property a schema declares.
///
/// Values are chosen to be exactly representable after a JSON round trip, so a
/// faithful model returns them unchanged and the comparison stays a statement
/// about the model rather than about float formatting.
fn sample(schema: &Value, schemas: &Map<String, Value>, depth: usize) -> Value {
    // The document nests about ten deep at its worst (a listing, of sessions,
    // of rollups, of per-model spend). The cap is well past that and exists
    // only so a schema that ever references itself terminates — loudly, as a
    // decode failure, rather than by recursing until the stack ends.
    if depth > 24 {
        return Value::Null;
    }
    if let Some(target) = resolve(schema, schemas) {
        return sample(target, schemas, depth + 1);
    }
    match schema.get("type").and_then(Value::as_str) {
        Some("string") => match schema.get("format").and_then(Value::as_str) {
            Some("date-time") => json!("2020-01-02T03:04:05Z"),
            _ => json!("sample"),
        },
        Some("boolean") => json!(true),
        Some("integer") => json!(1),
        Some("number") => json!(1.5),
        Some("array") => {
            let items = schema.get("items").cloned().unwrap_or_else(|| json!({}));
            json!([sample(&items, schemas, depth + 1)])
        }
        Some("object") | None => {
            if let Some(props) = schema.get("properties").and_then(Value::as_object) {
                let mut object = Map::new();
                for (name, declared) in props {
                    object.insert(name.clone(), sample(declared, schemas, depth + 1));
                }
                return Value::Object(object);
            }
            match schema.get("additionalProperties") {
                Some(additional) if additional.as_object().is_some_and(Map::is_empty) => {
                    json!({"key": "sample"})
                }
                Some(additional) => json!({"key": sample(additional, schemas, depth + 1)}),
                None if schema.get("type").is_none() => json!("sample"),
                None => json!({}),
            }
        }
        Some(_) => json!("sample"),
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
    use super::*;
    use crate::core::models::params::{
        ExportDetail, ExportSessionParams, ExportSessionsParams, PayloadDetail, SearchSpansParams,
        SessionListParams, SessionTracesParams, SkillScope, SkillSort, SkillsListParams,
        SortDirection, StatsParams, TraceListParams, TraceParams,
    };
    use serde::{Deserialize, Serialize};

    #[test]
    fn the_models_cover_the_vendored_contracts_schemas() {
        // The gate itself. A contract bump that adds a schema, adds a field to
        // one, or changes a field's type fails here — at build time, where
        // somebody can decide about it — rather than by quietly dropping data.
        assert_eq!(check(), Ok(()));
    }

    #[test]
    fn a_schema_in_neither_table_is_reported_as_unmodelled() {
        let report = report(&[Entry::of::<super::super::SessionItem>()], &[]).unwrap();
        assert!(!report.is_clean());
        assert!(
            report.unmodelled.contains(&"SpanItem".to_owned()),
            "got: {report:?}",
        );
    }

    #[test]
    fn a_table_entry_the_contract_does_not_have_is_reported_as_stale() {
        let report = report(&[], &[("LaunchCodes", "nowhere")]).unwrap();
        assert_eq!(report.stale, vec!["LaunchCodes".to_owned()]);
    }

    #[test]
    fn a_model_that_drops_a_contract_field_is_reported_by_path() {
        // The perturbation this gate exists to catch, pinned as a test rather
        // than as a claim: a model missing one property of its schema names
        // that property in the failure.
        #[derive(Debug, Default, Serialize, Deserialize)]
        #[serde(default)]
        struct HalfASession {
            id: String,
        }
        impl ContractModel for HalfASession {
            const SCHEMA: &'static str = "SessionItem";
        }

        let report = report(&[Entry::of::<HalfASession>()], UNMODELLED).unwrap();
        assert!(
            report
                .disagreements
                .iter()
                .any(|problem| problem.contains("SessionItem.display_title")
                    && problem.contains("not carried by the model")),
            "got: {report:?}",
        );
    }

    #[test]
    fn an_omittable_field_still_has_to_carry_its_property() {
        // The partial-update bodies omit an unset field from the wire, which is
        // exactly what a dropped property looks like to the round trip. So the
        // gate has to keep telling the two apart, and this is where that is
        // pinned: the model below carries one of `updateSkillRequest`'s six
        // properties as an omittable `Option` and simply lacks the rest. The
        // one it models is populated by the sample and survives; the five it
        // does not are reported by name, exactly as a model with five plain
        // missing fields would be.
        #[derive(Debug, Default, Serialize, Deserialize)]
        #[serde(default)]
        struct HalfAnUpdate {
            #[serde(skip_serializing_if = "Option::is_none")]
            name: Option<String>,
        }
        impl ContractModel for HalfAnUpdate {
            const SCHEMA: &'static str = "updateSkillRequest";
        }

        let report = report(&[Entry::of::<HalfAnUpdate>()], UNMODELLED).unwrap();
        for dropped in ["content", "description", "tags", "type", "visibility"] {
            assert!(
                report.disagreements.iter().any(|problem| {
                    problem.contains(&format!("updateSkillRequest.{dropped}"))
                        && problem.contains("not carried by the model")
                }),
                "{dropped} was dropped by the model and should have been reported; got: {report:?}",
            );
        }
        assert!(
            !report
                .disagreements
                .iter()
                .any(|problem| problem.contains("updateSkillRequest.name")),
            "an Option field the sample populates is carried, not missing; got: {report:?}",
        );
    }

    #[test]
    fn a_model_that_mistypes_a_field_is_reported_as_a_decode_failure() {
        #[derive(Debug, Default, Serialize, Deserialize)]
        #[serde(default)]
        struct MistypedUsage {
            input_tokens: String,
        }
        impl ContractModel for MistypedUsage {
            const SCHEMA: &'static str = "SessionUsage";
        }

        let report = report(&[Entry::of::<MistypedUsage>()], UNMODELLED).unwrap();
        assert!(
            report
                .disagreements
                .iter()
                .any(|problem| problem.contains("does not decode a document built from its own")),
            "got: {report:?}",
        );
    }

    #[test]
    fn a_composite_that_refuses_a_null_is_reported() {
        // The rule that keeps one nil projection from costing a caller the
        // whole document.
        #[derive(Debug, Default, Serialize, Deserialize)]
        #[serde(default)]
        struct StrictItems {
            items: Vec<Value>,
        }
        impl ContractModel for StrictItems {
            const SCHEMA: &'static str = "RawTurnListResponse";
        }

        let report = report(&[Entry::of::<StrictItems>()], UNMODELLED).unwrap();
        assert!(
            report
                .disagreements
                .iter()
                .any(|problem| problem.contains("RawTurnListResponse.items")
                    && problem.contains("does not tolerate a null")),
            "got: {report:?}",
        );
    }

    #[test]
    fn a_required_property_modelled_as_optional_is_reported() {
        // The contract requires nothing today. The rule is read from the
        // document rather than assumed, so this exercises the branch that will
        // matter the first time a schema does mark one.
        let schema = json!({
            "type": "object",
            "required": ["id"],
            "properties": {"id": {"type": "string"}},
        });
        let schemas = Map::new();

        #[derive(Debug, Default, Serialize, Deserialize)]
        #[serde(default)]
        struct Lenient {
            id: String,
        }
        impl ContractModel for Lenient {
            const SCHEMA: &'static str = "Synthetic";
        }

        let problems = audit::<Lenient>(&schema, &schemas);
        assert!(
            problems
                .iter()
                .any(|problem| problem.contains("required by the contract but decodes when absent")),
            "got: {problems:?}",
        );
    }

    #[test]
    fn every_typed_parameter_set_matches_the_contracts_declaration() {
        // Two-directional, and the struct literals are exhaustive: a parameter
        // added to the contract fails the check, and a field added to one of
        // these structs fails the compile until it is decided about here.
        check_params(&SessionListParams {
            limit: Some(1),
            cursor: Some("c".to_owned()),
            sort: Some("last_active".to_owned()),
            direction: Some(SortDirection::Desc),
            since: Some("2020-01-01T00:00:00Z".to_owned()),
            until: Some("2020-01-02T00:00:00Z".to_owned()),
            harness_id: Some("claude".to_owned()),
            harness_session_id: Some("hs-1".to_owned()),
            auth_subject: Some("user".to_owned()),
        })
        .unwrap();
        check_params(&SessionTracesParams {
            payload: Some(PayloadDetail::Full),
        })
        .unwrap();
        check_params(&TraceParams {
            payload: Some(PayloadDetail::Preview),
        })
        .unwrap();
        check_params(&TraceListParams {
            session_id: "s-1".to_owned(),
        })
        .unwrap();
        check_params(&SearchSpansParams {
            query: "gum glow charm".to_owned(),
            top_k: Some(5),
        })
        .unwrap();
        check_params(&ExportSessionParams {
            detail: Some(ExportDetail::Spans),
        })
        .unwrap();
        check_params(&ExportSessionsParams {
            since: Some("2020-01-01T00:00:00Z".to_owned()),
            until: Some("2020-01-02T00:00:00Z".to_owned()),
            detail: Some(ExportDetail::Traces),
        })
        .unwrap();
        check_params(&SkillsListParams {
            limit: Some(1),
            cursor: Some("c".to_owned()),
            q: Some("rust".to_owned()),
            scope: Some(SkillScope::Mine),
            sort: Some(SkillSort::Downloads),
        })
        .unwrap();
        check_params(&StatsParams {
            since: Some("2020-01-01T00:00:00Z".to_owned()),
            until: Some("2020-01-02T00:00:00Z".to_owned()),
            auth_subject: Some("user".to_owned()),
        })
        .unwrap();
    }

    #[test]
    fn a_parameter_the_contract_does_not_declare_is_reported() {
        struct Typo;
        impl ContractParams for Typo {
            const OPERATION: &'static str = "getSessionTraces";
            fn values(&self) -> Vec<(&'static str, String)> {
                vec![("payolad", "full".to_owned())]
            }
        }
        let err = check_params(&Typo).unwrap_err();
        assert!(err.contains("payolad"), "got: {err}");
    }

    #[test]
    fn every_closed_value_set_in_the_contract_has_a_typed_enum() {
        assert_eq!(
            check_enums(&[
                ClaimedEnum::of::<PayloadDetail>(),
                ClaimedEnum::of::<ExportDetail>(),
                ClaimedEnum::of::<SortDirection>(),
                ClaimedEnum::of::<SkillScope>(),
                ClaimedEnum::of::<SkillSort>(),
            ]),
            Ok(())
        );
    }

    #[test]
    fn a_value_set_no_typed_enum_claims_is_reported() {
        let err = check_enums(&[]).unwrap_err();
        assert!(err.contains("no typed enum claims"), "got: {err}");
    }
}