use std::cmp::Ordering;
use std::collections::BTreeMap;
use serde_json::{Map, Value};
use crate::retrieval_store::hex_sha256;
use crate::token_estimator::TokenEstimator;
pub const TRANSFORM_ID: &str = "json_prune";
pub const TRANSFORM_VERSION: &str = "1.0.0";
const MIN_ARRAY_LEN: usize = 2;
const SELF_FIELD: &str = "$self";
const DISCRETE_OUTLIER_SCORE: f64 = 1_000.0;
const MARGIN_FLOOR: i64 = 32;
const MARGIN_PER_ITEM: f64 = 0.5;
#[derive(Debug, thiserror::Error)]
pub enum JsonPruneError {
#[error("invalid json: {0}")]
Invalid(#[from] serde_json::Error),
}
#[derive(Debug, Clone)]
pub struct LossyOptions {
pub preserve_paths: Vec<String>,
pub ratio: f64,
pub namespace: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DroppedItem {
pub hash: String,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PruneReport {
pub eligible_arrays: usize,
pub total_candidates: usize,
pub preserved_candidates: usize,
pub kept_candidates: usize,
pub dropped_candidates: usize,
}
#[derive(Debug, Clone)]
pub struct PruneOutcome {
pub json: Value,
pub dropped: Vec<DroppedItem>,
pub report: PruneReport,
}
pub fn prune(
input: &[u8],
options: &LossyOptions,
estimator: &dyn TokenEstimator,
) -> Result<Option<PruneOutcome>, JsonPruneError> {
if input.is_empty() || options.ratio >= 1.0 {
return Ok(None);
}
let value: Value = serde_json::from_slice(input)?;
let mut arrays = Vec::new();
collect_eligible_arrays(&value, String::new(), &mut arrays);
if arrays.is_empty() {
return Ok(None);
}
let preserved_array: Vec<bool> = arrays
.iter()
.map(|a| is_preserved(&a.path, &options.preserve_paths))
.collect();
let field_stats: Vec<BTreeMap<String, FieldStats>> = arrays
.iter()
.map(|a| numeric_field_stats(&a.items))
.collect();
let markers: Vec<Vec<Value>> = arrays
.iter()
.map(|a| {
a.items
.iter()
.map(|item| {
let bytes = serde_json::to_vec(item).unwrap_or_default();
marker_json(&hex_sha256(&bytes), &options.namespace)
})
.collect()
})
.collect();
let marker_bytes_cache: Vec<Vec<Vec<u8>>> = markers
.iter()
.map(|per_array| {
per_array
.iter()
.map(|m| serde_json::to_vec(m).unwrap_or_default())
.collect()
})
.collect();
let total_candidates: usize = arrays.iter().map(|a| a.items.len()).sum();
let preserved_candidates: usize = arrays
.iter()
.zip(&preserved_array)
.filter(|&(_, &p)| p)
.map(|(a, _)| a.items.len())
.sum();
let mut candidates = Vec::new();
for (array_idx, array) in arrays.iter().enumerate() {
if preserved_array[array_idx] {
continue;
}
let stats = &field_stats[array_idx];
let n = array.items.len();
for (item_idx, item) in array.items.iter().enumerate() {
candidates.push(Candidate {
array_idx,
item_idx,
bytes: serde_json::to_vec(item).unwrap_or_default(),
rank: RankKey {
failure_signal: has_structural_failure_signal(item),
outlier_score: numeric_outlier_score(item, stats),
edge_bonus: item_idx == 0 || item_idx + 1 == n,
},
fingerprint: structural_fingerprint(item),
});
}
}
if candidates.is_empty() {
return Ok(None);
}
let marker_tokens_cache: Vec<Vec<i64>> = marker_bytes_cache
.iter()
.map(|per_array| {
per_array
.iter()
.map(|m| estimator.count_bytes(m) as i64)
.collect()
})
.collect();
let real_tokens_cache: Vec<i64> = candidates
.iter()
.map(|c| estimator.count_bytes(&c.bytes) as i64)
.collect();
let mut kept_mask: Vec<Vec<bool>> = arrays.iter().map(|a| vec![false; a.items.len()]).collect();
let mut droppable: Vec<usize> = Vec::new();
for (idx, c) in candidates.iter().enumerate() {
let marker_tok = marker_tokens_cache[c.array_idx][c.item_idx];
if real_tokens_cache[idx] > marker_tok {
droppable.push(idx);
} else {
kept_mask[c.array_idx][c.item_idx] = true;
}
}
let walk_order = diversity_walk_order(&candidates, &droppable);
let mut pool_tokens: i64 = droppable
.iter()
.map(|&idx| marker_tokens_cache[candidates[idx].array_idx][candidates[idx].item_idx])
.sum();
let mut pool_bytes: i64 = droppable
.iter()
.map(|&idx| {
marker_bytes_cache[candidates[idx].array_idx][candidates[idx].item_idx].len() as i64
})
.sum();
let real_total_tokens: i64 = droppable.iter().map(|&idx| real_tokens_cache[idx]).sum();
let budget_tokens = pool_tokens
+ ((real_total_tokens - pool_tokens) as f64 * options.ratio.clamp(0.0, 1.0)).round() as i64;
let mut accepted = 0usize;
const MAX_EXACT_TIER_CALLS_PER_ARRAY: usize = 16;
let mut exact_calls_used = vec![0usize; arrays.len()];
for &idx in &walk_order {
let c = &candidates[idx];
let marker_len = marker_bytes_cache[c.array_idx][c.item_idx].len() as i64;
let marker_tok = marker_tokens_cache[c.array_idx][c.item_idx];
let real_len = c.bytes.len() as i64;
let real_tok = real_tokens_cache[idx];
let trial_bytes = pool_bytes - marker_len + real_len;
let trial_tokens = pool_tokens - marker_tok + real_tok;
let margin = MARGIN_FLOOR + (MARGIN_PER_ITEM * accepted as f64) as i64;
let mut exact_delta: Option<i64> = None;
let fits = trial_bytes <= budget_tokens
|| trial_tokens + margin <= budget_tokens
|| (exact_calls_used[c.array_idx] < MAX_EXACT_TIER_CALLS_PER_ARRAY && {
exact_calls_used[c.array_idx] += 1;
kept_mask[c.array_idx][c.item_idx] = true;
let with = estimator.count_bytes(&assemble(&arrays[c.array_idx], &kept_mask[c.array_idx], &markers[c.array_idx]));
kept_mask[c.array_idx][c.item_idx] = false;
let without = estimator.count_bytes(&assemble(&arrays[c.array_idx], &kept_mask[c.array_idx], &markers[c.array_idx]));
let delta = with as i64 - without as i64;
exact_delta = Some(delta);
pool_tokens + delta <= budget_tokens
});
if fits {
kept_mask[c.array_idx][c.item_idx] = true;
pool_bytes = trial_bytes;
pool_tokens = match exact_delta {
Some(delta) => pool_tokens + delta,
None => trial_tokens,
};
accepted += 1;
}
}
let mut cursor = 0usize;
let pruned = rewrite_tree(&value, &mut cursor, &preserved_array, &kept_mask, &markers);
let mut dropped = Vec::new();
for (array_idx, mask) in kept_mask.iter().enumerate() {
if preserved_array[array_idx] {
continue;
}
for (item_idx, &kept) in mask.iter().enumerate() {
if !kept {
dropped.push(DroppedItem {
hash: hex_sha256(
&serde_json::to_vec(&arrays[array_idx].items[item_idx]).unwrap_or_default(),
),
bytes: serde_json::to_vec(&arrays[array_idx].items[item_idx])
.unwrap_or_default(),
});
}
}
}
if dropped.is_empty() {
return Ok(None);
}
let dropped_candidates = dropped.len();
Ok(Some(PruneOutcome {
json: pruned,
dropped,
report: PruneReport {
eligible_arrays: arrays.len(),
total_candidates,
preserved_candidates,
kept_candidates: total_candidates - preserved_candidates - dropped_candidates,
dropped_candidates,
},
}))
}
struct RankKey {
failure_signal: bool,
outlier_score: f64,
edge_bonus: bool,
}
impl RankKey {
fn cmp(&self, other: &RankKey) -> Ordering {
self.failure_signal
.cmp(&other.failure_signal)
.then_with(|| {
self.outlier_score
.partial_cmp(&other.outlier_score)
.unwrap_or(Ordering::Equal)
})
.then_with(|| self.edge_bonus.cmp(&other.edge_bonus))
}
}
struct Candidate {
array_idx: usize,
item_idx: usize,
bytes: Vec<u8>,
rank: RankKey,
fingerprint: String,
}
fn diversity_walk_order(candidates: &[Candidate], eligible: &[usize]) -> Vec<usize> {
let mut groups: BTreeMap<String, Vec<usize>> = BTreeMap::new();
for &idx in eligible {
let c = &candidates[idx];
groups.entry(c.fingerprint.clone()).or_default().push(idx);
}
for members in groups.values_mut() {
members.sort_by(|&a, &b| candidates[b].rank.cmp(&candidates[a].rank));
}
let mut group_order: Vec<String> = groups.keys().cloned().collect();
group_order.sort_by(|a, b| {
let ra = &candidates[groups[a][0]].rank;
let rb = &candidates[groups[b][0]].rank;
rb.cmp(ra)
});
let mut cursors: BTreeMap<String, usize> =
group_order.iter().map(|k| (k.clone(), 0usize)).collect();
let mut order = Vec::with_capacity(eligible.len());
let mut active: Vec<String> = group_order;
while !active.is_empty() {
for key in &active {
let members = &groups[key];
let cursor = cursors.get_mut(key).expect("seeded above");
order.push(members[*cursor]);
*cursor += 1;
}
active.retain(|key| cursors[key] < groups[key].len());
}
order
}
fn assemble(array: &EligibleArray, kept_mask: &[bool], markers: &[Value]) -> Vec<u8> {
let items: Vec<Value> = array
.items
.iter()
.zip(kept_mask)
.zip(markers)
.map(|((item, &kept), marker)| if kept { item.clone() } else { marker.clone() })
.collect();
serde_json::to_vec(&Value::Array(items)).unwrap_or_default()
}
fn marker_json(hash: &str, namespace: &str) -> Value {
let mut inner = Map::new();
inner.insert("hash".to_string(), Value::String(hash.to_string()));
inner.insert("alg".to_string(), Value::String("sha256".to_string()));
inner.insert(
"namespace".to_string(),
Value::String(namespace.to_string()),
);
let mut outer = Map::new();
outer.insert("$tf_ref".to_string(), Value::Object(inner));
Value::Object(outer)
}
struct EligibleArray {
path: String,
items: Vec<Value>,
}
fn collect_eligible_arrays(value: &Value, path: String, out: &mut Vec<EligibleArray>) {
match value {
Value::Array(items) if items.len() >= MIN_ARRAY_LEN => {
out.push(EligibleArray {
path,
items: items.clone(),
});
}
Value::Array(items) => {
for item in items {
collect_eligible_arrays(item, path.clone(), out);
}
}
Value::Object(map) => {
for (k, v) in map {
let child_path = if path.is_empty() {
k.clone()
} else {
format!("{path}.{k}")
};
collect_eligible_arrays(v, child_path, out);
}
}
_ => {}
}
}
fn rewrite_tree(
value: &Value,
cursor: &mut usize,
preserved_array: &[bool],
kept_mask: &[Vec<bool>],
markers: &[Vec<Value>],
) -> Value {
match value {
Value::Array(items) if items.len() >= MIN_ARRAY_LEN => {
let idx = *cursor;
*cursor += 1;
if preserved_array[idx] {
return value.clone();
}
let rebuilt: Vec<Value> = items
.iter()
.enumerate()
.map(|(i, item)| {
if kept_mask[idx][i] {
item.clone()
} else {
markers[idx][i].clone()
}
})
.collect();
Value::Array(rebuilt)
}
Value::Array(items) => Value::Array(
items
.iter()
.map(|item| rewrite_tree(item, cursor, preserved_array, kept_mask, markers))
.collect(),
),
Value::Object(map) => {
let mut out = Map::new();
for (k, v) in map {
out.insert(
k.clone(),
rewrite_tree(v, cursor, preserved_array, kept_mask, markers),
);
}
Value::Object(out)
}
_ => value.clone(),
}
}
pub fn revert_markers(json: &Value, restore: &std::collections::HashMap<String, Value>) -> Value {
if let Value::Object(map) = json
&& map.len() == 1
&& let Some(Value::Object(inner)) = map.get("$tf_ref")
&& let Some(Value::String(hash)) = inner.get("hash")
&& let Some(original) = restore.get(hash)
{
return original.clone();
}
match json {
Value::Array(items) => {
Value::Array(items.iter().map(|v| revert_markers(v, restore)).collect())
}
Value::Object(map) => {
let mut out = Map::new();
for (k, v) in map {
out.insert(k.clone(), revert_markers(v, restore));
}
Value::Object(out)
}
_ => json.clone(),
}
}
fn is_preserved(array_path: &str, preserve_paths: &[String]) -> bool {
let prefix = if array_path.is_empty() {
String::new()
} else {
format!("{array_path}.")
};
preserve_paths
.iter()
.any(|p| p == array_path || (p.len() > prefix.len() && p.starts_with(&prefix)))
}
#[derive(Debug, Clone, Copy)]
struct FieldStats {
median: f64,
mad: f64,
}
fn numeric_field_stats(items: &[Value]) -> BTreeMap<String, FieldStats> {
let mut values: BTreeMap<String, Vec<f64>> = BTreeMap::new();
for item in items {
match item {
Value::Number(n) => {
if let Some(f) = n.as_f64() {
values.entry(SELF_FIELD.to_string()).or_default().push(f);
}
}
Value::Object(map) => {
for (k, v) in map {
if let Value::Number(n) = v
&& let Some(f) = n.as_f64()
{
values.entry(k.clone()).or_default().push(f);
}
}
}
_ => {}
}
}
values
.into_iter()
.filter(|(_, v)| v.len() >= MIN_ARRAY_LEN)
.map(|(k, mut v)| {
let median = exact_median(&mut v);
let mut deviations: Vec<f64> = v.iter().map(|x| (x - median).abs()).collect();
let mad = exact_median(&mut deviations);
(k, FieldStats { median, mad })
})
.collect()
}
fn exact_median(values: &mut [f64]) -> f64 {
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let n = values.len();
if n == 0 {
return 0.0;
}
if n % 2 == 1 {
values[n / 2]
} else {
(values[n / 2 - 1] + values[n / 2]) / 2.0
}
}
fn modified_z_score(value: f64, stats: &FieldStats) -> f64 {
if stats.mad > 0.0 {
(value - stats.median).abs() / (1.4826 * stats.mad)
} else if value == stats.median {
0.0
} else {
DISCRETE_OUTLIER_SCORE
}
}
fn numeric_outlier_score(item: &Value, stats: &BTreeMap<String, FieldStats>) -> f64 {
match item {
Value::Number(n) => n
.as_f64()
.and_then(|f| stats.get(SELF_FIELD).map(|s| modified_z_score(f, s)))
.unwrap_or(0.0),
Value::Object(map) => map
.iter()
.filter_map(|(k, v)| {
let Value::Number(n) = v else { return None };
let f = n.as_f64()?;
let s = stats.get(k)?;
Some(modified_z_score(f, s))
})
.fold(0.0, f64::max),
_ => 0.0,
}
}
const STATUS_FALSE_KEYS: &[&str] = &["success", "ok", "healthy", "passed", "valid"];
const ERROR_COUNT_KEYS: &[&str] = &[
"error_count",
"errors",
"failures",
"failure_count",
"retries",
"retry_count",
];
const STATUS_CODE_KEYS: &[&str] = &["status", "status_code", "http_status", "code"];
fn has_structural_failure_signal(item: &Value) -> bool {
let Value::Object(map) = item else {
return false;
};
for (k, v) in map {
let lower = k.to_ascii_lowercase();
if STATUS_FALSE_KEYS.contains(&lower.as_str()) && v == &Value::Bool(false) {
return true;
}
if ERROR_COUNT_KEYS.contains(&lower.as_str()) && v.as_f64().is_some_and(|f| f > 0.0) {
return true;
}
if STATUS_CODE_KEYS.contains(&lower.as_str())
&& v.as_i64().is_some_and(|n| (400..=599).contains(&n))
{
return true;
}
}
false
}
fn structural_fingerprint(item: &Value) -> String {
match item {
Value::Object(map) => {
let mut keys: Vec<&str> = map.keys().map(|k| k.as_str()).collect();
keys.sort_unstable();
keys.join(",")
}
Value::Array(_) => "array".to_string(),
Value::Number(_) => "number".to_string(),
Value::String(_) => "string".to_string(),
Value::Bool(_) => "bool".to_string(),
Value::Null => "null".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::token_estimator::ByteHeuristicEstimator;
fn opts(ratio: f64) -> LossyOptions {
LossyOptions {
preserve_paths: Vec::new(),
ratio,
namespace: "default".to_string(),
}
}
fn padding() -> String {
"x".repeat(150)
}
#[test]
fn ratio_at_or_above_one_is_a_clean_noop() {
let input = br#"{"items":[{"a":1},{"a":2},{"a":3}]}"#;
assert!(
prune(input, &opts(1.0), &ByteHeuristicEstimator)
.unwrap()
.is_none()
);
}
#[test]
fn no_eligible_arrays_is_a_clean_noop() {
let input = br#"{"a":1,"items":[1]}"#; assert!(
prune(input, &opts(0.1), &ByteHeuristicEstimator)
.unwrap()
.is_none()
);
}
#[test]
fn preserved_array_is_never_touched_even_at_zero_ratio() {
let input = serde_json::json!({"items": [{"a":1},{"a":2},{"a":3},{"a":4}]});
let mut o = opts(0.0);
o.preserve_paths = vec!["items".to_string()];
let bytes = serde_json::to_vec(&input).unwrap();
assert!(
prune(&bytes, &o, &ByteHeuristicEstimator)
.unwrap()
.is_none()
);
}
#[test]
fn preserved_array_alongside_a_prunable_one_never_leaks_into_dropped_or_the_report() {
let p = padding();
let keep_items: Vec<Value> = (0..3)
.map(|i| serde_json::json!({"a": i, "guard": "KEEP_ME", "pad": p}))
.collect();
let prune_items: Vec<Value> = (0..4)
.map(|i| serde_json::json!({"a": i, "pad": p}))
.collect();
let input = serde_json::json!({"keep_me": keep_items, "prune_me": prune_items});
let bytes = serde_json::to_vec(&input).unwrap();
let mut o = opts(0.1);
o.preserve_paths = vec!["keep_me".to_string()];
let outcome = prune(&bytes, &o, &ByteHeuristicEstimator).unwrap().unwrap();
let keep_me_bytes: Vec<Vec<u8>> = keep_items
.iter()
.map(|v| serde_json::to_vec(v).unwrap())
.collect();
for d in &outcome.dropped {
assert!(
!keep_me_bytes.contains(&d.bytes),
"a preserved item leaked into outcome.dropped: {:?}",
String::from_utf8_lossy(&d.bytes)
);
}
assert_eq!(
outcome.report.total_candidates,
outcome.report.preserved_candidates
+ outcome.report.kept_candidates
+ outcome.report.dropped_candidates
);
assert_eq!(outcome.report.preserved_candidates, 3);
let out_keep_me = outcome.json["keep_me"].as_array().unwrap();
assert_eq!(out_keep_me, &keep_items);
}
#[test]
fn large_uniform_array_prunes_without_quadratic_blowup() {
let p = "x".repeat(150);
let items: Vec<Value> = (0..3000)
.map(|i| serde_json::json!({"n": i, "pad": p}))
.collect();
let input = serde_json::json!({"items": items});
let bytes = serde_json::to_vec(&input).unwrap();
let start = std::time::Instant::now();
let outcome = prune(&bytes, &opts(0.3), &ByteHeuristicEstimator).unwrap();
let elapsed = start.elapsed();
assert!(
elapsed < std::time::Duration::from_secs(5),
"prune() on 3000 uniform items took {elapsed:?} -- likely a quadratic regression"
);
assert!(outcome.is_some());
}
#[test]
fn zero_ratio_drops_low_priority_items_from_an_unpreserved_array() {
let p = padding();
let input = serde_json::json!({"items": (0..6).map(|i| serde_json::json!({"a": i, "pad": p})).collect::<Vec<_>>()});
let bytes = serde_json::to_vec(&input).unwrap();
let outcome = prune(&bytes, &opts(0.0), &ByteHeuristicEstimator)
.unwrap()
.expect("some items should drop at ratio 0.0");
assert!(outcome.report.dropped_candidates > 0);
assert_eq!(
outcome.report.dropped_candidates + outcome.report.kept_candidates,
outcome.report.total_candidates
);
let s = serde_json::to_string(&outcome.json).unwrap();
assert_eq!(s.matches("$tf_ref").count(), outcome.dropped.len());
}
#[test]
fn mad_zero_and_equal_to_median_has_no_outlier_signal() {
let stats = FieldStats {
median: 0.0,
mad: 0.0,
};
assert_eq!(modified_z_score(0.0, &stats), 0.0);
}
#[test]
fn mad_zero_and_different_from_median_is_a_strong_discrete_outlier() {
let stats = FieldStats {
median: 0.0,
mad: 0.0,
};
assert_eq!(modified_z_score(1.0, &stats), DISCRETE_OUTLIER_SCORE);
}
#[test]
fn mad_positive_uses_the_standard_modified_z_score_formula() {
let stats = FieldStats {
median: 10.0,
mad: 2.0,
};
let expected = (15.0_f64 - 10.0).abs() / (1.4826 * 2.0);
assert!((modified_z_score(15.0, &stats) - expected).abs() < 1e-9);
}
#[test]
fn structural_failure_signal_is_value_aware_not_substring_matched() {
assert!(!has_structural_failure_signal(
&serde_json::json!({"error_count": 0})
));
assert!(!has_structural_failure_signal(
&serde_json::json!({"failed": false})
));
assert!(has_structural_failure_signal(
&serde_json::json!({"success": false})
));
assert!(has_structural_failure_signal(
&serde_json::json!({"error_count": 3})
));
assert!(has_structural_failure_signal(
&serde_json::json!({"status_code": 503})
));
assert!(!has_structural_failure_signal(
&serde_json::json!({"status_code": 200})
));
}
#[test]
fn structural_failure_signal_survives_the_full_pipeline_at_low_ratio() {
let p = padding();
let mut items: Vec<Value> = (0..20)
.map(|i| serde_json::json!({"id": i, "success": true, "pad": p}))
.collect();
items[10] = serde_json::json!({"id": 10, "success": false, "pad": p});
let input = serde_json::json!({"items": items});
let bytes = serde_json::to_vec(&input).unwrap();
let outcome = prune(&bytes, &opts(0.1), &ByteHeuristicEstimator)
.unwrap()
.unwrap();
let arr = outcome.json["items"].as_array().unwrap();
assert_eq!(arr[10]["success"], serde_json::json!(false));
}
#[test]
fn is_deterministic_across_repeated_runs() {
let p = padding();
let input = serde_json::json!({"items": (0..15).map(|i| serde_json::json!({"n": i, "pad": p})).collect::<Vec<_>>()});
let bytes = serde_json::to_vec(&input).unwrap();
let a = prune(&bytes, &opts(0.3), &ByteHeuristicEstimator)
.unwrap()
.unwrap();
let b = prune(&bytes, &opts(0.3), &ByteHeuristicEstimator)
.unwrap()
.unwrap();
assert_eq!(a.json, b.json);
assert_eq!(
a.dropped.iter().map(|d| &d.hash).collect::<Vec<_>>(),
b.dropped.iter().map(|d| &d.hash).collect::<Vec<_>>()
);
}
#[test]
fn diversity_walk_spreads_across_fingerprint_groups_before_draining_one() {
let p = padding();
let mut items = Vec::new();
for i in 0..6 {
items.push(serde_json::json!({"kind_a": i, "pad": p}));
}
for i in 0..6 {
items.push(serde_json::json!({"kind_b": i, "pad": p}));
}
let input = serde_json::json!({"items": items});
let bytes = serde_json::to_vec(&input).unwrap();
let outcome = prune(&bytes, &opts(0.4), &ByteHeuristicEstimator)
.unwrap()
.unwrap();
let arr = outcome.json["items"].as_array().unwrap();
let kind_a_kept = arr[0..6]
.iter()
.filter(|v| v.get("kind_a").is_some())
.count();
let kind_b_kept = arr[6..12]
.iter()
.filter(|v| v.get("kind_b").is_some())
.count();
assert!(
kind_a_kept > 0,
"round-robin should keep at least one kind_a item"
);
assert!(
kind_b_kept > 0,
"round-robin should keep at least one kind_b item"
);
}
#[test]
fn pruned_output_is_always_valid_json() {
let p = padding();
let input = serde_json::json!({"items": (0..10).map(|i| serde_json::json!({"n": i, "pad": p})).collect::<Vec<_>>()});
let bytes = serde_json::to_vec(&input).unwrap();
let outcome = prune(&bytes, &opts(0.5), &ByteHeuristicEstimator)
.unwrap()
.unwrap();
let round_trip = serde_json::to_vec(&outcome.json).unwrap();
assert!(serde_json::from_slice::<Value>(&round_trip).is_ok());
}
#[test]
fn dropped_item_hashes_match_their_own_bytes() {
let p = padding();
let input = serde_json::json!({"items": (0..8).map(|i| serde_json::json!({"n": i, "pad": p})).collect::<Vec<_>>()});
let bytes = serde_json::to_vec(&input).unwrap();
let outcome = prune(&bytes, &opts(0.1), &ByteHeuristicEstimator)
.unwrap()
.unwrap();
for d in &outcome.dropped {
assert_eq!(hex_sha256(&d.bytes), d.hash);
}
}
#[test]
fn nested_arrays_are_found_but_not_recursed_into_when_the_parent_is_eligible() {
let input = serde_json::json!({
"groups": [
{"users": [1,2,3]},
{"users": [4,5,6]},
]
});
let bytes = serde_json::to_vec(&input).unwrap();
let mut arrays = Vec::new();
let value: Value = serde_json::from_slice(&bytes).unwrap();
collect_eligible_arrays(&value, String::new(), &mut arrays);
assert_eq!(
arrays.len(),
1,
"only the outer array is eligible, not the nested ones"
);
assert_eq!(arrays[0].path, "groups");
}
#[test]
fn revert_markers_restores_only_the_named_hash_and_leaves_everything_else_alone() {
let p = padding();
let input = serde_json::json!({"items": (0..10).map(|i| serde_json::json!({"n": i, "pad": p})).collect::<Vec<_>>()});
let bytes = serde_json::to_vec(&input).unwrap();
let outcome = prune(&bytes, &opts(0.1), &ByteHeuristicEstimator)
.unwrap()
.unwrap();
assert!(!outcome.dropped.is_empty());
let mut restore = std::collections::HashMap::new();
let first = &outcome.dropped[0];
let original: Value = serde_json::from_slice(&first.bytes).unwrap();
restore.insert(first.hash.clone(), original.clone());
let reverted = revert_markers(&outcome.json, &restore);
let s = serde_json::to_string(&reverted).unwrap();
let remaining_markers = outcome.dropped.len() - 1;
assert_eq!(s.matches("$tf_ref").count(), remaining_markers);
let arr = reverted["items"].as_array().unwrap();
assert!(arr.contains(&original));
}
#[test]
fn preserve_path_naming_something_inside_an_eligible_array_protects_that_array() {
let p = padding();
let input = serde_json::json!({
"groups": (0..6).map(|i| serde_json::json!({"users": [1,2,3], "a": i, "pad": p})).collect::<Vec<_>>()
});
let bytes = serde_json::to_vec(&input).unwrap();
let mut o = opts(0.0); o.preserve_paths = vec!["groups.users".to_string()];
let outcome = prune(&bytes, &o, &ByteHeuristicEstimator).unwrap();
assert!(
outcome.is_none(),
"\"groups.users\" must protect the whole \"groups\" array, leaving nothing to prune"
);
}
#[test]
fn preserve_path_protects_an_eligible_root_array_too() {
let p = padding();
let input = serde_json::json!(
(0..6)
.map(|i| serde_json::json!({"users": [1,2,3], "a": i, "pad": p}))
.collect::<Vec<_>>()
);
let bytes = serde_json::to_vec(&input).unwrap();
let mut o = opts(0.0); o.preserve_paths = vec!["users".to_string()];
assert!(
prune(&bytes, &o, &ByteHeuristicEstimator)
.unwrap()
.is_none(),
"a preserve path must protect the eligible ROOT array, leaving nothing to prune"
);
assert!(
prune(&bytes, &opts(0.0), &ByteHeuristicEstimator)
.unwrap()
.is_some()
);
}
#[test]
fn an_unrelated_preserve_path_does_not_protect_a_named_sibling_array() {
let p = padding();
let input = serde_json::json!({"items": (0..6).map(|i| serde_json::json!({"a": i, "pad": p})).collect::<Vec<_>>()});
let bytes = serde_json::to_vec(&input).unwrap();
let mut o = opts(0.0);
o.preserve_paths = vec!["other.thing".to_string(), "items_extra".to_string()];
assert!(
prune(&bytes, &o, &ByteHeuristicEstimator)
.unwrap()
.is_some(),
"neither an unrelated path nor a non-dot prefix extension may protect \"items\""
);
}
#[test]
fn a_tier_three_acceptance_charges_the_exact_delta_not_the_independent_estimate() {
let p = padding();
let items: Vec<Value> = (0..40)
.map(|i| serde_json::json!({"n": i, "pad": p, "note": format!("row {i}")}))
.collect();
let input = serde_json::json!({"items": items});
let bytes = serde_json::to_vec(&input).unwrap();
let est = ByteHeuristicEstimator;
for ratio in [0.0, 0.2, 0.5, 0.9] {
let Some(outcome) = prune(&bytes, &opts(ratio), &est).unwrap() else {
continue;
};
let out_bytes = serde_json::to_vec(&outcome.json).unwrap();
assert!(
est.count_bytes(&out_bytes) <= est.count_bytes(&bytes),
"ratio {ratio}: pruned output ({}) costs more than the input ({})",
est.count_bytes(&out_bytes),
est.count_bytes(&bytes)
);
}
}
#[test]
fn sibling_eligible_arrays_at_different_paths_are_both_found() {
let input = serde_json::json!({
"a": [1,2,3],
"b": {"c": [4,5,6]},
});
let bytes = serde_json::to_vec(&input).unwrap();
let value: Value = serde_json::from_slice(&bytes).unwrap();
let mut arrays = Vec::new();
collect_eligible_arrays(&value, String::new(), &mut arrays);
let mut paths: Vec<&str> = arrays.iter().map(|a| a.path.as_str()).collect();
paths.sort_unstable();
assert_eq!(paths, vec!["a", "b.c"]);
}
}