Skip to main content

gcf/
generic_delta.rs

1//! Generic-profile delta encoding (SPEC Section 10a).
2//!
3//! Full producer + consumer for keyed-row deltas over the generic profile,
4//! byte-for-byte interoperable with gcf-go, gcf-python, and gcf-typescript.
5//! Delta is opt-in and bilateral; the existing `encode_generic` path is unchanged.
6//!
7//! SHA-256 is implemented locally (no new dependency); the shared conformance
8//! fixtures verify it end to end.
9
10use crate::scalar::{
11    format_key, format_number, format_scalar, parse_quoted_string, parse_scalar, quote_string,
12    split_respecting_quotes, ScalarValue,
13};
14use serde_json::{Map, Value};
15use std::collections::HashMap;
16use std::fmt::Write;
17
18const NULL: Value = Value::Null;
19
20/// A keyed record set: the unit generic-profile delta operates on (Section 10a).
21/// Rows are order-agnostic (set semantics); `fields` carries the declared column
22/// order for the wire form; `key` names the identity column (the `@id` / `key=`);
23/// `name` is the tabular section name for a full payload.
24#[derive(Debug, Clone, PartialEq)]
25pub struct GenericSet {
26    pub name: String,
27    pub key: String,
28    pub fields: Vec<String>,
29    pub rows: Vec<Map<String, Value>>,
30}
31
32/// A diff between two `GenericSet`s (computed by `diff_generic_sets` or supplied
33/// directly and serialized by `encode_generic_delta`).
34#[derive(Debug, Clone, Default, PartialEq)]
35pub struct GenericDeltaPayload {
36    pub tool: String,
37    pub key: String,
38    pub fields: Vec<String>,
39    pub base_root: String,
40    pub new_root: String,
41    pub added: Vec<Map<String, Value>>,
42    pub changed: Vec<Map<String, Value>>,
43    pub removed: Vec<Value>,
44    pub delta_tokens: u64,
45    pub full_tokens: u64,
46}
47
48/// Canonicalize one value for the pack-root record (Section 10a.3). Purpose-built
49/// and deliberately decoupled from the wire cell encoder (`format_scalar`): it must
50/// be collision-free and record-safe, not round-trippable.
51///   - Typed literals stay bare so they never collide with the strings that spell
52///     them: null is `-` (never a string), booleans are `true`/`false`, numbers are
53///     canonical (Section 2.3.1).
54///   - Strings are ALWAYS quoted, so (a) they can't collide with a typed literal
55///     (`-`, `true`, `123` all become quoted), and (b) a tab or newline inside a
56///     value is escaped and cannot break the tab/newline-delimited record.
57pub fn canonical_cell(v: &Value) -> String {
58    match v {
59        Value::Null => "-".to_string(),
60        Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
61        Value::Number(n) => format_number(n),
62        Value::String(s) => quote_string(s),
63        other => quote_string(&other.to_string()),
64    }
65}
66
67/// Compute the canonical pack root for a keyed set using the gcf-pack-root-v1
68/// algorithm, generic profile (Section 10a.3). Two implementations given the same
69/// logical set MUST produce the same result. Fields and records sort by UTF-8 byte
70/// order (Rust `str` ordering is byte-wise), matching Go's `sort.Strings`.
71pub fn generic_pack_root(s: &GenericSet) -> String {
72    let mut sorted_fields = s.fields.clone();
73    sorted_fields.sort();
74
75    let mut records: Vec<String> = s
76        .rows
77        .iter()
78        .map(|row| {
79            let mut r = String::from("R");
80            for f in &sorted_fields {
81                r.push('\t');
82                r.push_str(f);
83                r.push('\t');
84                r.push_str(&canonical_cell(row.get(f).unwrap_or(&NULL)));
85            }
86            r.push('\n');
87            r
88        })
89        .collect();
90    records.sort();
91
92    format!("sha256:{}", sha256_hex(records.concat().as_bytes()))
93}
94
95/// Build an identity -> row map, rejecting duplicate identities (Section 10a.1).
96fn index_by_key(s: &GenericSet) -> Result<HashMap<String, &Map<String, Value>>, String> {
97    let mut m = HashMap::with_capacity(s.rows.len());
98    for row in &s.rows {
99        let id = canonical_cell(row.get(&s.key).unwrap_or(&NULL));
100        if m.contains_key(&id) {
101            return Err(format!(
102                "delta_invalid: duplicate identity {} for key \"{}\"",
103                id, s.key
104            ));
105        }
106        m.insert(id, row);
107    }
108    Ok(m)
109}
110
111fn rows_equal(a: &Map<String, Value>, b: &Map<String, Value>, fields: &[String]) -> bool {
112    fields.iter().all(|f| {
113        canonical_cell(a.get(f).unwrap_or(&NULL)) == canonical_cell(b.get(f).unwrap_or(&NULL))
114    })
115}
116
117fn key_of(row: &Map<String, Value>, key: &str) -> String {
118    canonical_cell(row.get(key).unwrap_or(&NULL))
119}
120
121/// Compute the delta from `base` to `next`. This is the blessed producer path: it
122/// is the single place that enforces the keyed-diff invariants (identity
123/// uniqueness, added-not-in-base, changed-must-exist, whole-row replacement,
124/// unchanged rows omitted). Added/changed/removed are sorted by identity for
125/// reproducible output (Section 10a.6). Schema change or a missing key returns an
126/// error: the caller must then send a full payload (Section 10a.7).
127pub fn diff_generic_sets(
128    base: &GenericSet,
129    next: &GenericSet,
130) -> Result<GenericDeltaPayload, String> {
131    if next.key.is_empty() {
132        return Err("delta_invalid: no identity key".to_string());
133    }
134    if next.key != base.key || base.fields != next.fields {
135        return Err("delta_invalid: schema change (send full)".to_string());
136    }
137    let base_idx = index_by_key(base)?;
138    let next_idx = index_by_key(next)?;
139
140    let mut added: Vec<Map<String, Value>> = Vec::new();
141    let mut changed: Vec<Map<String, Value>> = Vec::new();
142    let mut removed: Vec<Value> = Vec::new();
143
144    for (id, row) in &next_idx {
145        match base_idx.get(id) {
146            None => added.push((*row).clone()),
147            Some(brow) => {
148                if !rows_equal(brow, row, &next.fields) {
149                    changed.push((*row).clone());
150                }
151            }
152        }
153        // equal rows are omitted (silence = "keep it", Section 10a.5)
154    }
155    for (id, brow) in &base_idx {
156        if !next_idx.contains_key(id) {
157            removed.push(brow.get(&next.key).cloned().unwrap_or(Value::Null));
158        }
159    }
160
161    added.sort_by_key(|r| key_of(r, &next.key));
162    changed.sort_by_key(|r| key_of(r, &next.key));
163    removed.sort_by_key(canonical_cell);
164
165    Ok(GenericDeltaPayload {
166        tool: String::new(),
167        key: next.key.clone(),
168        fields: next.fields.clone(),
169        base_root: generic_pack_root(base),
170        new_root: generic_pack_root(next),
171        added,
172        changed,
173        removed,
174        delta_tokens: 0,
175        full_tokens: 0,
176    })
177}
178
179// --- producer-side wire encoding ---
180
181fn field_decl(fields: &[String], key: &str) -> String {
182    fields
183        .iter()
184        .map(|f| {
185            if f == key {
186                format!("@{}", format_key(f))
187            } else {
188                format_key(f)
189            }
190        })
191        .collect::<Vec<_>>()
192        .join(",")
193}
194
195fn encode_row(row: &Map<String, Value>, fields: &[String]) -> String {
196    fields
197        .iter()
198        .map(|f| format_scalar(row.get(f).unwrap_or(&NULL), '|'))
199        .collect::<Vec<_>>()
200        .join("|")
201}
202
203/// Emit a delta-participating full base payload: `key=` in the header, an
204/// `@`-prefixed identity field in the declaration, pipe-separated rows.
205pub fn encode_generic_full(s: &GenericSet, tool: &str) -> String {
206    let name = if s.name.is_empty() { "rows" } else { &s.name };
207    let mut b = String::from("GCF profile=generic");
208    if !tool.is_empty() {
209        write!(b, " tool={}", tool).unwrap();
210    }
211    writeln!(b, " pack_root={} key={}", generic_pack_root(s), s.key).unwrap();
212    writeln!(
213        b,
214        "## {} [{}]{{{}}}",
215        name,
216        s.rows.len(),
217        field_decl(&s.fields, &s.key)
218    )
219    .unwrap();
220    for row in &s.rows {
221        b.push_str(&encode_row(row, &s.fields));
222        b.push('\n');
223    }
224    b
225}
226
227/// Serialize a delta payload (Section 10a.2). Sections are emitted in the
228/// deterministic order added / changed / removed (Section 10a.6).
229pub fn encode_generic_delta(d: &GenericDeltaPayload) -> String {
230    let mut b = String::from("GCF profile=generic");
231    if !d.tool.is_empty() {
232        write!(b, " tool={}", d.tool).unwrap();
233    }
234    write!(
235        b,
236        " delta=true base_root={} new_root={} key={}",
237        d.base_root, d.new_root, d.key
238    )
239    .unwrap();
240    if d.full_tokens > 0 {
241        let savings = 100.0 * (1.0 - d.delta_tokens as f64 / d.full_tokens as f64);
242        write!(b, " savings={:.0}%", savings).unwrap();
243    }
244    b.push('\n');
245
246    if !d.added.is_empty() {
247        writeln!(
248            b,
249            "## added [{}]{{{}}}",
250            d.added.len(),
251            field_decl(&d.fields, &d.key)
252        )
253        .unwrap();
254        for row in &d.added {
255            b.push_str(&encode_row(row, &d.fields));
256            b.push('\n');
257        }
258    }
259    if !d.changed.is_empty() {
260        writeln!(
261            b,
262            "## changed [{}]{{{}}}",
263            d.changed.len(),
264            field_decl(&d.fields, &d.key)
265        )
266        .unwrap();
267        for row in &d.changed {
268            b.push_str(&encode_row(row, &d.fields));
269            b.push('\n');
270        }
271    }
272    if !d.removed.is_empty() {
273        writeln!(b, "## removed [{}]{{@{}}}", d.removed.len(), d.key).unwrap();
274        for idv in &d.removed {
275            b.push_str(&format_scalar(idv, '|'));
276            b.push('\n');
277        }
278    }
279    b
280}
281
282/// Apply a delta to a base set and verify the result hashes to `expected_new_root`
283/// (Section 10a.5). Atomic: the whole payload is validated before any state
284/// changes, and a mismatch leaves the base untouched.
285pub fn verify_generic_delta(
286    base: &GenericSet,
287    d: &GenericDeltaPayload,
288    expected_new_root: &str,
289) -> Result<GenericSet, String> {
290    if generic_pack_root(base) != d.base_root {
291        return Err("base_mismatch: base root does not equal delta base_root".to_string());
292    }
293    let base_idx = index_by_key(base)?;
294
295    // Validate the entire payload against the original base before mutating.
296    for idv in &d.removed {
297        if !base_idx.contains_key(&canonical_cell(idv)) {
298            return Err(format!(
299                "delta_invalid: removing identity {} not in base",
300                canonical_cell(idv)
301            ));
302        }
303    }
304    for row in &d.added {
305        if base_idx.contains_key(&key_of(row, &d.key)) {
306            return Err(format!(
307                "delta_invalid: adding identity {} that already exists",
308                key_of(row, &d.key)
309            ));
310        }
311    }
312    for row in &d.changed {
313        if !base_idx.contains_key(&key_of(row, &d.key)) {
314            return Err(format!(
315                "delta_invalid: changing identity {} not in base",
316                key_of(row, &d.key)
317            ));
318        }
319    }
320
321    // Apply to a working copy.
322    let mut work: HashMap<String, Map<String, Value>> = base_idx
323        .iter()
324        .map(|(k, v)| (k.clone(), (*v).clone()))
325        .collect();
326    for idv in &d.removed {
327        work.remove(&canonical_cell(idv));
328    }
329    for row in &d.added {
330        work.insert(key_of(row, &d.key), row.clone());
331    }
332    for row in &d.changed {
333        work.insert(key_of(row, &d.key), row.clone());
334    }
335
336    let result = GenericSet {
337        name: base.name.clone(),
338        key: base.key.clone(),
339        fields: base.fields.clone(),
340        rows: work.into_values().collect(),
341    };
342    let got = generic_pack_root(&result);
343    if got != expected_new_root {
344        return Err(format!(
345            "root_mismatch: computed {}, expected {}",
346            got, expected_new_root
347        ));
348    }
349    Ok(result)
350}
351
352// --- consumer-side wire parsing (Section 10a) ---
353
354fn scalar_to_value(sv: ScalarValue) -> Result<Value, String> {
355    match sv {
356        ScalarValue::Null => Ok(Value::Null),
357        ScalarValue::Bool(b) => Ok(Value::Bool(b)),
358        ScalarValue::Int(i) => Ok(Value::Number(i.into())),
359        ScalarValue::Float(f) => serde_json::Number::from_f64(f)
360            .map(Value::Number)
361            .ok_or_else(|| "delta_invalid: non-finite number".to_string()),
362        ScalarValue::Str(s) => Ok(Value::String(s)),
363        ScalarValue::Missing => {
364            Err("delta_invalid: missing (~) not allowed in delta row".to_string())
365        }
366        ScalarValue::Attachment => {
367            Err("delta_invalid: attachment (^) not allowed in delta row".to_string())
368        }
369    }
370}
371
372fn parse_header_fields(header: &str) -> HashMap<String, String> {
373    let mut m = HashMap::new();
374    for tok in header.split_whitespace() {
375        if let Some(i) = tok.find('=') {
376            if i > 0 {
377                m.insert(tok[..i].to_string(), tok[i + 1..].to_string());
378            }
379        }
380    }
381    m
382}
383
384fn parse_count(s: &str) -> Result<usize, String> {
385    if s == "0" {
386        return Ok(0);
387    }
388    if s.is_empty() || s.starts_with('0') {
389        return Err(format!("delta_invalid: invalid count {}", s));
390    }
391    s.parse::<usize>()
392        .map_err(|_| format!("delta_invalid: invalid count {}", s))
393}
394
395/// Find the byte index of the first `[` not inside a quoted string.
396fn find_bracket_start(s: &str) -> Option<usize> {
397    let mut in_quote = false;
398    let mut escaped = false;
399    for (i, c) in s.char_indices() {
400        if escaped {
401            escaped = false;
402            continue;
403        }
404        if c == '\\' && in_quote {
405            escaped = true;
406            continue;
407        }
408        if c == '"' {
409            in_quote = !in_quote;
410            continue;
411        }
412        if c == '[' && !in_quote {
413            return Some(i);
414        }
415    }
416    None
417}
418
419/// Parse a delta/full field declaration `{@id,total,...}`, returning the ordered
420/// fields and the key field (the one that was `@`-marked) (Section 10a.1).
421fn split_delta_field_decl(decl: &str) -> Result<(Vec<String>, String), String> {
422    if decl.len() < 2 || !decl.starts_with('{') || !decl.ends_with('}') {
423        return Err(format!(
424            "delta_invalid: invalid field declaration: {}",
425            decl
426        ));
427    }
428    let inner = &decl[1..decl.len() - 1];
429    if inner.is_empty() {
430        return Ok((Vec::new(), String::new()));
431    }
432    let mut fields = Vec::new();
433    let mut key_field = String::new();
434    for raw in split_respecting_quotes(inner, ',') {
435        let mut f = raw.trim().to_string();
436        let mut is_key = false;
437        if let Some(rest) = f.strip_prefix('@') {
438            f = rest.to_string();
439            is_key = true;
440        }
441        if f.len() >= 2 && f.starts_with('"') && f.ends_with('"') {
442            f = parse_quoted_string(&f)?;
443        }
444        if is_key {
445            key_field = f.clone();
446        }
447        fields.push(f);
448    }
449    Ok((fields, key_field))
450}
451
452/// Parse the content after `## ` of a delta/full section, e.g.
453/// `added [1]{@id,total,status,customer}` or `orders [3]{@id,...}` or `removed [1]{@id}`.
454fn parse_section_header(content: &str) -> Result<(String, usize, Vec<String>, String), String> {
455    let bi = find_bracket_start(content)
456        .ok_or_else(|| format!("delta_invalid: section header without count: {}", content))?;
457    let name = content[..bi].trim().to_string();
458    let rest = &content[bi..]; // "[N]{...}"
459    if !rest.starts_with('[') {
460        return Err(format!(
461            "delta_invalid: malformed section header: {}",
462            content
463        ));
464    }
465    let close = rest
466        .find(']')
467        .ok_or_else(|| format!("delta_invalid: unterminated count: {}", content))?;
468    let count = parse_count(&rest[1..close])?;
469    let (fields, key_field) = split_delta_field_decl(&rest[close + 1..])?;
470    Ok((name, count, fields, key_field))
471}
472
473fn parse_row(line: &str, fields: &[String]) -> Result<Map<String, Value>, String> {
474    let cells = split_respecting_quotes(line, '|');
475    if cells.len() != fields.len() {
476        return Err(format!(
477            "delta_invalid: row has {} cells, expected {}: {}",
478            cells.len(),
479            fields.len(),
480            line
481        ));
482    }
483    let mut row = Map::new();
484    for (i, f) in fields.iter().enumerate() {
485        row.insert(f.clone(), scalar_to_value(parse_scalar(&cells[i], true)?)?);
486    }
487    Ok(row)
488}
489
490/// Parse a delta-participating full base payload into a `GenericSet`, and return
491/// the declared `pack_root` (Section 10a).
492pub fn decode_generic_full(text: &str) -> Result<(GenericSet, String), String> {
493    let trimmed = text.trim_end_matches('\n');
494    let lines: Vec<&str> = trimmed.split('\n').collect();
495    let hdr = parse_header_fields(lines[0]);
496    if hdr.get("profile").map(String::as_str) != Some("generic") {
497        return Err("not a generic payload".to_string());
498    }
499    let mut set = GenericSet {
500        name: String::new(),
501        key: hdr.get("key").cloned().unwrap_or_default(),
502        fields: Vec::new(),
503        rows: Vec::new(),
504    };
505    let mut i = 1;
506    while i < lines.len() {
507        let line = lines[i];
508        if !line.starts_with("## ") {
509            // Only blank lines, comments, and the ##! summary trailer are valid
510            // outside a section; any other line is a surplus row past a declared
511            // section count (Section 13).
512            if line.is_empty() || line.starts_with("# ") || line.starts_with("##! ") {
513                i += 1;
514                continue;
515            }
516            return Err(format!(
517                "count_mismatch: unexpected content after declared section rows: {:?}",
518                line
519            ));
520        }
521        let (name, count, fields, key_field) = parse_section_header(&line[3..])?;
522        set.name = name;
523        set.fields = fields.clone();
524        if set.key.is_empty() {
525            set.key = key_field;
526        }
527        i += 1;
528        for j in 0..count {
529            if i >= lines.len() || lines[i].starts_with("## ") {
530                return Err(format!(
531                    "count_mismatch: declared {} rows, got {}",
532                    count, j
533                ));
534            }
535            set.rows.push(parse_row(lines[i], &fields)?);
536            i += 1;
537        }
538    }
539    Ok((set, hdr.get("pack_root").cloned().unwrap_or_default()))
540}
541
542/// Parse a delta payload into a `GenericDeltaPayload` (Section 10a.2). The result
543/// can be applied with `verify_generic_delta`.
544pub fn decode_generic_delta(text: &str) -> Result<GenericDeltaPayload, String> {
545    let trimmed = text.trim_end_matches('\n');
546    let lines: Vec<&str> = trimmed.split('\n').collect();
547    let hdr = parse_header_fields(lines[0]);
548    if hdr.get("profile").map(String::as_str) != Some("generic") {
549        return Err("not a generic payload".to_string());
550    }
551    if hdr.get("delta").map(String::as_str) != Some("true") {
552        return Err("not a delta payload".to_string());
553    }
554    let mut d = GenericDeltaPayload {
555        tool: hdr.get("tool").cloned().unwrap_or_default(),
556        key: hdr.get("key").cloned().unwrap_or_default(),
557        base_root: hdr.get("base_root").cloned().unwrap_or_default(),
558        new_root: hdr.get("new_root").cloned().unwrap_or_default(),
559        ..Default::default()
560    };
561    let mut fields_set = false;
562    let mut i = 1;
563    while i < lines.len() {
564        let line = lines[i];
565        if !line.starts_with("## ") {
566            // Only blank lines, comments, and the ##! summary trailer are valid
567            // outside a section; any other line is a surplus row past a declared
568            // section count (Section 13).
569            if line.is_empty() || line.starts_with("# ") || line.starts_with("##! ") {
570                i += 1;
571                continue;
572            }
573            return Err(format!(
574                "count_mismatch: unexpected content after declared section rows: {:?}",
575                line
576            ));
577        }
578        let (name, count, fields, key_field) = parse_section_header(&line[3..])?;
579        if d.key.is_empty() && !key_field.is_empty() {
580            d.key = key_field;
581        }
582        if !fields_set && (name == "added" || name == "changed") {
583            d.fields = fields.clone();
584            fields_set = true;
585        }
586        i += 1;
587        match name.as_str() {
588            "added" | "changed" => {
589                let mut rows = Vec::with_capacity(count);
590                for j in 0..count {
591                    if i >= lines.len() || lines[i].starts_with("## ") {
592                        return Err(format!(
593                            "count_mismatch: declared {} rows in ## {}, got {}",
594                            count, name, j
595                        ));
596                    }
597                    rows.push(parse_row(lines[i], &fields)?);
598                    i += 1;
599                }
600                if name == "added" {
601                    d.added = rows;
602                } else {
603                    d.changed = rows;
604                }
605            }
606            "removed" => {
607                for j in 0..count {
608                    if i >= lines.len() || lines[i].starts_with("## ") {
609                        return Err(format!(
610                            "count_mismatch: declared {} identities in ## removed, got {}",
611                            count, j
612                        ));
613                    }
614                    d.removed
615                        .push(scalar_to_value(parse_scalar(lines[i], true)?)?);
616                    i += 1;
617                }
618            }
619            other => {
620                return Err(format!("delta_invalid: unknown delta section {}", other));
621            }
622        }
623    }
624    Ok(d)
625}
626
627// --- producer-side re-anchor session (SPEC Section 10a.8) ---
628
629/// The working default cadence for `ReanchorPolicy::FixedN` (SPEC Section 10a.8).
630pub const DEFAULT_REANCHOR_N: usize = 15;
631
632/// Selects when a `GenericDeltaSession` re-anchors. Construct with
633/// `ReanchorPolicy::fixed_n` or `ReanchorPolicy::size_guard`.
634///
635/// This is producer-side policy only (Section 10a.8, non-normative): it never
636/// affects the wire syntax. Every payload the session emits is byte-identical to
637/// `encode_generic_full` / `encode_generic_delta`, and the decoder accepts them
638/// cadence-agnostically.
639#[derive(Debug, Clone, Copy, PartialEq, Eq)]
640pub enum ReanchorPolicy {
641    /// Re-anchor every N turns.
642    FixedN(usize),
643    /// Re-anchor once the cumulative delta since the last anchor reaches the
644    /// current full payload's size (size-adaptive).
645    SizeGuard,
646}
647
648impl ReanchorPolicy {
649    /// Re-anchor every `n` turns. `n == 0` falls back to `DEFAULT_REANCHOR_N`.
650    pub fn fixed_n(n: usize) -> Self {
651        ReanchorPolicy::FixedN(if n == 0 { DEFAULT_REANCHOR_N } else { n })
652    }
653
654    /// Re-anchor once the cumulative delta bytes since the last anchor reach the
655    /// current full payload's byte size: more anchors under heavy churn, rarely
656    /// under light churn, bounding delta spend between anchors to about one full
657    /// payload. Production-recommended.
658    pub fn size_guard() -> Self {
659        ReanchorPolicy::SizeGuard
660    }
661}
662
663/// A producer-side helper that manages the re-anchor cadence for a stream of
664/// generic-profile updates (SPEC Section 10a.8, non-normative producer policy).
665/// It is thin sugar over the primitives: each `next` emits either a compact delta
666/// or, on its chosen cadence, a full re-anchor, updating its held base. It
667/// introduces NO new wire syntax. Not safe for concurrent use.
668#[derive(Debug, Clone)]
669pub struct GenericDeltaSession {
670    base: GenericSet,
671    tool: String,
672    policy: ReanchorPolicy,
673    turn: usize,
674    cum: usize, // cumulative delta bytes since the last anchor
675}
676
677impl GenericDeltaSession {
678    /// Start a session anchored on `base`. Call `current_full` to get the initial
679    /// full payload to transmit, then `next` for each subsequent state.
680    pub fn new(base: GenericSet, tool: String, policy: ReanchorPolicy) -> Self {
681        let policy = match policy {
682            ReanchorPolicy::FixedN(0) => ReanchorPolicy::FixedN(DEFAULT_REANCHOR_N),
683            p => p,
684        };
685        GenericDeltaSession {
686            base,
687            tool,
688            policy,
689            turn: 0,
690            cum: 0,
691        }
692    }
693
694    /// Return the full payload for the current base (`encode_generic_full`). Send
695    /// this first to establish the base; it is also a valid manual re-anchor.
696    pub fn current_full(&self) -> String {
697        encode_generic_full(&self.base, &self.tool)
698    }
699
700    /// Return the number of `next` calls so far (the initial full is turn 0).
701    pub fn turn(&self) -> usize {
702        self.turn
703    }
704
705    /// Advance the session by one turn to `next`, returning the wire to transmit
706    /// and whether it is a full re-anchor (`true`) or a delta (`false`). A schema
707    /// change forces a full (Section 10a.7). The held base becomes `next` either
708    /// way. The wire is byte-identical to calling `encode_generic_full` /
709    /// `encode_generic_delta` directly.
710    pub fn next(&mut self, next: GenericSet) -> Result<(String, bool), String> {
711        self.turn += 1;
712
713        // Schema change (or a fresh key) cannot be expressed as a delta -> full.
714        if next.key != self.base.key || self.base.fields != next.fields {
715            return Ok((self.reanchor(next), true));
716        }
717
718        let d = diff_generic_sets(&self.base, &next)?;
719        let delta_wire = encode_generic_delta(&d);
720
721        let reanchor = match self.policy {
722            ReanchorPolicy::SizeGuard => {
723                self.cum + delta_wire.len() >= encode_generic_full(&next, &self.tool).len()
724            }
725            ReanchorPolicy::FixedN(n) => self.turn.is_multiple_of(n),
726        };
727
728        if reanchor {
729            return Ok((self.reanchor(next), true));
730        }
731        self.base = next;
732        self.cum += delta_wire.len();
733        Ok((delta_wire, false))
734    }
735
736    /// Emit a full payload for `next`, advance the base, and reset the
737    /// cumulative-delta counter.
738    fn reanchor(&mut self, next: GenericSet) -> String {
739        let wire = encode_generic_full(&next, &self.tool);
740        self.base = next;
741        self.cum = 0;
742        wire
743    }
744}
745
746// --- SHA-256 (local, no dependency) ---
747
748const SHA256_K: [u32; 64] = [
749    0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
750    0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
751    0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
752    0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
753    0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
754    0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
755    0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
756    0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
757];
758
759pub(crate) fn sha256_hex(data: &[u8]) -> String {
760    let mut h: [u32; 8] = [
761        0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
762        0x5be0cd19,
763    ];
764
765    let bit_len = (data.len() as u64).wrapping_mul(8);
766    let mut msg = data.to_vec();
767    msg.push(0x80);
768    while msg.len() % 64 != 56 {
769        msg.push(0);
770    }
771    msg.extend_from_slice(&bit_len.to_be_bytes());
772
773    for chunk in msg.chunks(64) {
774        let mut w = [0u32; 64];
775        for (i, word) in w.iter_mut().take(16).enumerate() {
776            *word = u32::from_be_bytes([
777                chunk[i * 4],
778                chunk[i * 4 + 1],
779                chunk[i * 4 + 2],
780                chunk[i * 4 + 3],
781            ]);
782        }
783        for i in 16..64 {
784            let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
785            let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
786            w[i] = w[i - 16]
787                .wrapping_add(s0)
788                .wrapping_add(w[i - 7])
789                .wrapping_add(s1);
790        }
791
792        let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h;
793        for i in 0..64 {
794            let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
795            let ch = (e & f) ^ ((!e) & g);
796            let t1 = hh
797                .wrapping_add(s1)
798                .wrapping_add(ch)
799                .wrapping_add(SHA256_K[i])
800                .wrapping_add(w[i]);
801            let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
802            let maj = (a & b) ^ (a & c) ^ (b & c);
803            let t2 = s0.wrapping_add(maj);
804            hh = g;
805            g = f;
806            f = e;
807            e = d.wrapping_add(t1);
808            d = c;
809            c = b;
810            b = a;
811            a = t1.wrapping_add(t2);
812        }
813        h[0] = h[0].wrapping_add(a);
814        h[1] = h[1].wrapping_add(b);
815        h[2] = h[2].wrapping_add(c);
816        h[3] = h[3].wrapping_add(d);
817        h[4] = h[4].wrapping_add(e);
818        h[5] = h[5].wrapping_add(f);
819        h[6] = h[6].wrapping_add(g);
820        h[7] = h[7].wrapping_add(hh);
821    }
822
823    let mut out = String::with_capacity(64);
824    for v in h {
825        write!(out, "{:08x}", v).unwrap();
826    }
827    out
828}
829
830#[cfg(test)]
831mod tests {
832    use super::*;
833    use serde_json::json;
834
835    #[test]
836    fn sha256_matches_known_vectors() {
837        assert_eq!(
838            sha256_hex(b""),
839            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
840        );
841        assert_eq!(
842            sha256_hex(b"abc"),
843            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
844        );
845    }
846
847    fn row(v: Value) -> Map<String, Value> {
848        v.as_object().unwrap().clone()
849    }
850
851    fn orders_base() -> GenericSet {
852        GenericSet {
853            name: "orders".into(),
854            key: "id".into(),
855            fields: vec![
856                "id".into(),
857                "total".into(),
858                "status".into(),
859                "customer".into(),
860            ],
861            rows: vec![
862                row(json!({"id": 1001, "total": 59.98, "status": "shipped", "customer": "Alice"})),
863                row(json!({"id": 1002, "total": 29.99, "status": "pending", "customer": "Bob"})),
864                row(json!({"id": 1003, "total": 129.50, "status": "shipped", "customer": "Carol"})),
865            ],
866        }
867    }
868
869    fn orders_next() -> GenericSet {
870        GenericSet {
871            name: "orders".into(),
872            key: "id".into(),
873            fields: vec![
874                "id".into(),
875                "total".into(),
876                "status".into(),
877                "customer".into(),
878            ],
879            rows: vec![
880                row(json!({"id": 1002, "total": 29.99, "status": "shipped", "customer": "Bob"})),
881                row(json!({"id": 1003, "total": 129.50, "status": "shipped", "customer": "Carol"})),
882                row(json!({"id": 1004, "total": 75.00, "status": "pending", "customer": "Dave"})),
883            ],
884        }
885    }
886
887    #[test]
888    fn roundtrip_by_root() {
889        let base = orders_base();
890        let next = orders_next();
891        let d = diff_generic_sets(&base, &next).unwrap();
892        assert_eq!((d.added.len(), d.changed.len(), d.removed.len()), (1, 1, 1));
893        assert_eq!(d.new_root, generic_pack_root(&next));
894        let result = verify_generic_delta(&base, &d, &generic_pack_root(&next)).unwrap();
895        assert_eq!(generic_pack_root(&result), generic_pack_root(&next));
896    }
897
898    #[test]
899    fn pack_root_row_order_invariant() {
900        let a = orders_base();
901        let mut b = orders_base();
902        b.rows.swap(0, 2);
903        assert_eq!(generic_pack_root(&a), generic_pack_root(&b));
904    }
905
906    #[test]
907    fn canonical_cell_no_collision() {
908        assert_eq!(canonical_cell(&Value::Null), "-");
909        assert_eq!(canonical_cell(&json!(true)), "true");
910        assert_eq!(canonical_cell(&json!("true")), "\"true\"");
911        assert_eq!(canonical_cell(&json!("-")), "\"-\"");
912        assert_eq!(canonical_cell(&json!(59.98)), "59.98");
913        assert_eq!(canonical_cell(&json!("59.98")), "\"59.98\"");
914        assert_eq!(canonical_cell(&json!("a\tb")), "\"a\\tb\"");
915    }
916
917    #[test]
918    fn invariants() {
919        let base = orders_base();
920        let base_root = generic_pack_root(&base);
921
922        let mut dup = orders_base();
923        dup.rows.push(row(
924            json!({"id": 1001, "total": 1.0, "status": "x", "customer": "y"}),
925        ));
926        assert!(diff_generic_sets(&dup, &orders_next())
927            .unwrap_err()
928            .contains("duplicate identity"));
929
930        let mut sc = orders_next();
931        sc.fields = vec!["id".into(), "total".into(), "status".into()];
932        assert!(diff_generic_sets(&base, &sc)
933            .unwrap_err()
934            .contains("schema change"));
935
936        let add_existing = GenericDeltaPayload {
937            key: "id".into(),
938            fields: base.fields.clone(),
939            base_root: base_root.clone(),
940            added: vec![row(
941                json!({"id": 1001, "total": 1.0, "status": "s", "customer": "c"}),
942            )],
943            ..Default::default()
944        };
945        assert!(verify_generic_delta(&base, &add_existing, "sha256:x")
946            .unwrap_err()
947            .contains("already exists"));
948
949        let change_missing = GenericDeltaPayload {
950            key: "id".into(),
951            fields: base.fields.clone(),
952            base_root: base_root.clone(),
953            changed: vec![row(
954                json!({"id": 9999, "total": 1.0, "status": "s", "customer": "c"}),
955            )],
956            ..Default::default()
957        };
958        assert!(verify_generic_delta(&base, &change_missing, "sha256:x")
959            .unwrap_err()
960            .contains("not in base"));
961
962        let remove_missing = GenericDeltaPayload {
963            key: "id".into(),
964            fields: base.fields.clone(),
965            base_root: base_root.clone(),
966            removed: vec![json!(9999)],
967            ..Default::default()
968        };
969        assert!(verify_generic_delta(&base, &remove_missing, "sha256:x")
970            .unwrap_err()
971            .contains("not in base"));
972
973        let wrong_base = GenericDeltaPayload {
974            key: "id".into(),
975            fields: base.fields.clone(),
976            base_root: "sha256:wrong".into(),
977            ..Default::default()
978        };
979        assert!(verify_generic_delta(&base, &wrong_base, &base_root)
980            .unwrap_err()
981            .contains("base_mismatch"));
982
983        let d = diff_generic_sets(&base, &orders_next()).unwrap();
984        assert!(verify_generic_delta(&base, &d, "sha256:deadbeef")
985            .unwrap_err()
986            .contains("root_mismatch"));
987    }
988
989    #[test]
990    fn full_wire_roundtrip() {
991        let base = orders_base();
992        let (got, pr) = decode_generic_full(&encode_generic_full(&base, "orders_query")).unwrap();
993        assert_eq!(generic_pack_root(&got), generic_pack_root(&base));
994        assert_eq!(pr, generic_pack_root(&base));
995    }
996
997    #[test]
998    fn end_to_end() {
999        let base = orders_base();
1000        let next = orders_next();
1001        let (held, _) = decode_generic_full(&encode_generic_full(&base, "orders_query")).unwrap();
1002        let d = diff_generic_sets(&base, &next).unwrap();
1003        let parsed = decode_generic_delta(&encode_generic_delta(&d)).unwrap();
1004        let result = verify_generic_delta(&held, &parsed, &generic_pack_root(&next)).unwrap();
1005        assert_eq!(generic_pack_root(&result), generic_pack_root(&next));
1006    }
1007
1008    #[test]
1009    fn nulls_and_string_keys() {
1010        let nulls = GenericSet {
1011            name: "items".into(),
1012            key: "id".into(),
1013            fields: vec![
1014                "id".into(),
1015                "total".into(),
1016                "status".into(),
1017                "customer".into(),
1018            ],
1019            rows: vec![
1020                row(json!({"id": 2001, "total": 10.0, "status": null, "customer": "Amy"})),
1021                row(json!({"id": 2002, "total": null, "status": "open", "customer": null})),
1022            ],
1023        };
1024        let (got, _) = decode_generic_full(&encode_generic_full(&nulls, "")).unwrap();
1025        assert_eq!(generic_pack_root(&got), generic_pack_root(&nulls));
1026
1027        let sku = GenericSet {
1028            name: "parts".into(),
1029            key: "sku".into(),
1030            fields: vec!["sku".into(), "name".into(), "qty".into()],
1031            rows: vec![
1032                row(json!({"sku": "1001", "name": "Widget", "qty": 5})),
1033                row(json!({"sku": "A-200", "name": "Gadget", "qty": 3})),
1034            ],
1035        };
1036        let (got2, _) = decode_generic_full(&encode_generic_full(&sku, "")).unwrap();
1037        assert_eq!(generic_pack_root(&got2), generic_pack_root(&sku));
1038    }
1039
1040    #[test]
1041    fn decode_malformed_fails_closed() {
1042        let cases = [
1043            "",
1044            "GCF profile=graph delta=true base_root=a new_root=b key=id\n",
1045            "GCF profile=generic pack_root=r key=id\n## t [1]{@id}\n1\n",
1046            "GCF profile=generic delta=true base_root=a new_root=b key=id\n## added [2]{@id,x}\n1|2\n",
1047            "GCF profile=generic delta=true base_root=a new_root=b key=id\n## added [1]{@id,x}\n1\n",
1048            "GCF profile=generic delta=true base_root=a new_root=b key=id\n## bogus [1]{@id}\n1\n",
1049            "GCF profile=generic delta=true base_root=a new_root=b key=id\n## added [01]{@id,x}\n1|2\n",
1050        ];
1051        for wire in cases {
1052            assert!(
1053                decode_generic_delta(wire).is_err(),
1054                "expected error for {:?}",
1055                wire
1056            );
1057        }
1058    }
1059}
1060
1061#[cfg(test)]
1062mod session_tests {
1063    use super::*;
1064    use serde_json::json;
1065
1066    fn row(v: Value) -> Map<String, Value> {
1067        v.as_object().unwrap().clone()
1068    }
1069
1070    fn sess_base() -> GenericSet {
1071        GenericSet {
1072            name: "orders".into(),
1073            key: "id".into(),
1074            fields: vec![
1075                "id".into(),
1076                "total".into(),
1077                "status".into(),
1078                "customer".into(),
1079            ],
1080            rows: vec![
1081                row(json!({"id": 1001, "total": 59.98, "status": "shipped", "customer": "Alice"})),
1082                row(json!({"id": 1002, "total": 29.99, "status": "pending", "customer": "Bob"})),
1083                row(json!({"id": 1003, "total": 129.50, "status": "shipped", "customer": "Carol"})),
1084            ],
1085        }
1086    }
1087
1088    fn mk(rows: Vec<Map<String, Value>>) -> GenericSet {
1089        GenericSet {
1090            name: "orders".into(),
1091            key: "id".into(),
1092            fields: vec![
1093                "id".into(),
1094                "total".into(),
1095                "status".into(),
1096                "customer".into(),
1097            ],
1098            rows,
1099        }
1100    }
1101
1102    // Small per-turn updates (same schema) for the FixedN scenario.
1103    fn sess_updates() -> Vec<GenericSet> {
1104        vec![
1105            mk(vec![
1106                row(json!({"id": 1001, "total": 59.98, "status": "shipped", "customer": "Alice"})),
1107                row(json!({"id": 1002, "total": 29.99, "status": "shipped", "customer": "Bob"})), // changed
1108                row(json!({"id": 1003, "total": 129.50, "status": "shipped", "customer": "Carol"})),
1109            ]),
1110            mk(vec![
1111                // add 1004
1112                row(json!({"id": 1001, "total": 59.98, "status": "shipped", "customer": "Alice"})),
1113                row(json!({"id": 1002, "total": 29.99, "status": "shipped", "customer": "Bob"})),
1114                row(json!({"id": 1003, "total": 129.50, "status": "shipped", "customer": "Carol"})),
1115                row(json!({"id": 1004, "total": 75.00, "status": "pending", "customer": "Dave"})),
1116            ]),
1117            mk(vec![
1118                // remove 1001
1119                row(json!({"id": 1002, "total": 29.99, "status": "shipped", "customer": "Bob"})),
1120                row(json!({"id": 1003, "total": 129.50, "status": "shipped", "customer": "Carol"})),
1121                row(json!({"id": 1004, "total": 75.00, "status": "pending", "customer": "Dave"})),
1122            ]),
1123            mk(vec![
1124                // change 1003
1125                row(json!({"id": 1002, "total": 29.99, "status": "shipped", "customer": "Bob"})),
1126                row(
1127                    json!({"id": 1003, "total": 140.00, "status": "delivered", "customer": "Carol"}),
1128                ),
1129                row(json!({"id": 1004, "total": 75.00, "status": "pending", "customer": "Dave"})),
1130            ]),
1131            mk(vec![
1132                // add 1005
1133                row(json!({"id": 1002, "total": 29.99, "status": "shipped", "customer": "Bob"})),
1134                row(
1135                    json!({"id": 1003, "total": 140.00, "status": "delivered", "customer": "Carol"}),
1136                ),
1137                row(json!({"id": 1004, "total": 75.00, "status": "pending", "customer": "Dave"})),
1138                row(json!({"id": 1005, "total": 12.00, "status": "pending", "customer": "Eve"})),
1139            ]),
1140        ]
1141    }
1142
1143    // Larger base + one-row updates so SizeGuard's cumulative delta reaches a full.
1144    fn size_guard_base() -> GenericSet {
1145        let names = [
1146            "Alice", "Bob", "Carol", "Dave", "Eve", "Frank", "Grace", "Heidi", "Ivan", "Judy",
1147            "Mallory", "Niaj", "Olivia", "Peggy", "Rupert", "Sybil", "Trent", "Uma", "Victor",
1148            "Walter",
1149        ];
1150        let rows = names
1151            .iter()
1152            .enumerate()
1153            .map(|(i, n)| {
1154                row(json!({"id": 2000 + i, "total": 10 + i, "status": "pending", "customer": n}))
1155            })
1156            .collect();
1157        GenericSet {
1158            name: "rows".into(),
1159            key: "id".into(),
1160            fields: vec![
1161                "id".into(),
1162                "total".into(),
1163                "status".into(),
1164                "customer".into(),
1165            ],
1166            rows,
1167        }
1168    }
1169
1170    fn size_guard_updates() -> Vec<GenericSet> {
1171        let base = size_guard_base();
1172        (0..6)
1173            .map(|turn| {
1174                let mut g = base.clone();
1175                // change one distinct row's status each turn
1176                g.rows[turn].insert("status".into(), json!("shipped"));
1177                g
1178            })
1179            .collect()
1180    }
1181
1182    #[test]
1183    fn fixed_n_pattern() {
1184        let mut s = GenericDeltaSession::new(
1185            sess_base(),
1186            "orders_query".into(),
1187            ReanchorPolicy::fixed_n(3),
1188        );
1189        let want_full = [false, false, true, false, false]; // re-anchor on turn 3
1190        for (i, up) in sess_updates().into_iter().enumerate() {
1191            let (_, is_full) = s.next(up).unwrap();
1192            assert_eq!(is_full, want_full[i], "turn {}", i + 1);
1193        }
1194    }
1195
1196    #[test]
1197    fn size_guard_triggers() {
1198        let mut s =
1199            GenericDeltaSession::new(size_guard_base(), "".into(), ReanchorPolicy::size_guard());
1200        let mut anchors = 0;
1201        for up in size_guard_updates() {
1202            let (_, is_full) = s.next(up).unwrap();
1203            if is_full {
1204                anchors += 1;
1205            }
1206        }
1207        assert!(
1208            anchors > 0,
1209            "SizeGuard never re-anchored across 6 turns; scenario should trigger at least one"
1210        );
1211    }
1212
1213    #[test]
1214    fn schema_change_reanchors() {
1215        let mut s = GenericDeltaSession::new(
1216            sess_base(),
1217            "orders_query".into(),
1218            ReanchorPolicy::fixed_n(15),
1219        );
1220        let changed = GenericSet {
1221            name: "orders".into(),
1222            key: "id".into(),
1223            fields: vec!["id".into(), "total".into(), "status".into()], // drop a column
1224            rows: vec![row(
1225                json!({"id": 1001, "total": 59.98, "status": "shipped"}),
1226            )],
1227        };
1228        let (_, is_full) = s.next(changed).unwrap();
1229        assert!(is_full, "schema change must force a full re-anchor");
1230    }
1231
1232    // With N=15 over 30 update turns, exactly two emissions are full re-anchors
1233    // (turns 15 and 30); the other 28 are deltas.
1234    #[test]
1235    fn fixed_n_15_over_30_turns() {
1236        let mut s = GenericDeltaSession::new(
1237            sess_base(),
1238            "orders_query".into(),
1239            ReanchorPolicy::fixed_n(15),
1240        );
1241        let _ = s.current_full(); // bootstrap full (turn 0), not counted below
1242
1243        let mut fulls = 0;
1244        let mut deltas = 0;
1245        let mut full_turns: Vec<usize> = Vec::new();
1246        let mut prev = sess_base();
1247        for turn in 1..=30usize {
1248            // mutate one row's total each turn so every turn is a real, same-schema delta
1249            let mut next = GenericSet {
1250                name: prev.name.clone(),
1251                key: prev.key.clone(),
1252                fields: prev.fields.clone(),
1253                rows: Vec::new(),
1254            };
1255            let n_rows = prev.rows.len();
1256            for (j, r) in prev.rows.iter().enumerate() {
1257                let mut nr = r.clone();
1258                if j == turn % n_rows {
1259                    nr.insert("total".into(), json!(turn as f64 + 0.5));
1260                }
1261                next.rows.push(nr);
1262            }
1263            let (_, is_full) = s.next(next.clone()).unwrap();
1264            if is_full {
1265                fulls += 1;
1266                full_turns.push(turn);
1267            } else {
1268                deltas += 1;
1269            }
1270            prev = next;
1271        }
1272        assert_eq!((fulls, deltas), (2, 28), "over 30 turns");
1273        assert_eq!(full_turns, vec![15, 30], "full re-anchor turns");
1274    }
1275
1276    // The load-bearing test: a consumer that applies each emission (full -> decode,
1277    // delta -> decode+verify) stays byte-for-byte in sync with the producer's state
1278    // at every turn, under both policies.
1279    #[test]
1280    fn consumer_stays_in_sync() {
1281        let cases: Vec<(&str, GenericSet, Vec<GenericSet>, String, ReanchorPolicy)> = vec![
1282            (
1283                "fixedN3",
1284                sess_base(),
1285                sess_updates(),
1286                "orders_query".into(),
1287                ReanchorPolicy::fixed_n(3),
1288            ),
1289            (
1290                "sizeGuard",
1291                size_guard_base(),
1292                size_guard_updates(),
1293                "".into(),
1294                ReanchorPolicy::size_guard(),
1295            ),
1296        ];
1297        for (name, base, ups, tool, policy) in cases {
1298            let mut s = GenericDeltaSession::new(base, tool, policy);
1299            let (mut held, _) = decode_generic_full(&s.current_full()).unwrap();
1300            for (i, up) in ups.into_iter().enumerate() {
1301                let (wire, is_full) = s.next(up.clone()).unwrap();
1302                if is_full {
1303                    held = decode_generic_full(&wire).unwrap().0;
1304                } else {
1305                    let d = decode_generic_delta(&wire).unwrap();
1306                    held = verify_generic_delta(&held, &d, &d.new_root).unwrap();
1307                }
1308                assert_eq!(
1309                    generic_pack_root(&held),
1310                    generic_pack_root(&up),
1311                    "{}: turn {} consumer root != producer root (is_full={})",
1312                    name,
1313                    i + 1,
1314                    is_full
1315                );
1316            }
1317        }
1318    }
1319}