@knolo/cli 6.2.0 → 6.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,692 @@
1
+ //! Exact minimum cover. Combinations are visited smallest size first.
2
+ //! A visit above `maxCoverVisits` is `SEARCH_BOUND_EXCEEDED`.
3
+
4
+ use std::collections::BTreeSet;
5
+
6
+ use crate::kar::closure::Admission;
7
+ use crate::kar::json::{digest_json, utf16_cmp, Json};
8
+ use crate::kar::model::{floor_micros, lookup_floor, lookup_requirement, Plan, FRONTIERS};
9
+
10
+ #[derive(Debug, Clone)]
11
+ pub struct Choice {
12
+ pub evidence_id: String,
13
+ pub frontiers: Vec<String>,
14
+ }
15
+
16
+ #[derive(Debug, Clone)]
17
+ pub struct CoveragePair {
18
+ pub covered: i64,
19
+ pub required: i64,
20
+ }
21
+
22
+ #[derive(Debug, Clone)]
23
+ pub struct Rational {
24
+ pub numerator: i64,
25
+ pub denominator: i64,
26
+ }
27
+
28
+ #[derive(Debug, Clone)]
29
+ pub struct Decision {
30
+ pub status: String,
31
+ pub profile: String,
32
+ pub cardinality: i64,
33
+ pub coverage: Vec<(String, CoveragePair)>,
34
+ pub redundancy: Option<Rational>,
35
+ }
36
+
37
+ #[derive(Debug, Clone)]
38
+ pub struct CoverOutcome {
39
+ pub status: String,
40
+ pub evidence_ids: Vec<String>,
41
+ pub choices: Vec<Choice>,
42
+ pub decision: Decision,
43
+ }
44
+
45
+ #[derive(Clone)]
46
+ struct Candidate {
47
+ id: String,
48
+ mask: String,
49
+ requirements: Vec<(String, BTreeSet<String>)>,
50
+ nonempty: BTreeSet<String>,
51
+ tokens: Vec<String>,
52
+ bits: Vec<(String, BTreeSet<usize>)>,
53
+ }
54
+
55
+ struct Rat {
56
+ n: i128,
57
+ d: i128,
58
+ }
59
+
60
+ pub fn select_cover(
61
+ plan: &Plan,
62
+ admissions: &[Admission],
63
+ texts: &[(String, String)],
64
+ ) -> CoverOutcome {
65
+ let micros = frontier_micros(plan);
66
+ let base = build_candidates(plan, admissions, texts);
67
+ let pool = minimum_cover_pool(plan, &base);
68
+ let limit = plan.bounds.max_cover_visits;
69
+ let mut visits: i64 = 0;
70
+ let max_size = plan.cardinality_bound.min(pool.len() as i64).max(0) as usize;
71
+
72
+ if plan.profile == "minimum-cover" {
73
+ for size in 0..=max_size {
74
+ let mut matched = None;
75
+ let exceeded = visit_combinations(&pool, size, &mut visits, limit, |combo| {
76
+ let scored = coverage_of(plan, combo, &micros);
77
+ if scored.ok {
78
+ matched = Some((combo.to_vec(), scored.pairs));
79
+ true
80
+ } else {
81
+ false
82
+ }
83
+ });
84
+ if exceeded {
85
+ return finish(plan, "SEARCH_BOUND_EXCEEDED", &[], &zero_pairs(plan), None);
86
+ }
87
+ if let Some((combo, pairs)) = matched {
88
+ return finish(plan, "SATISFIED", &combo, &pairs, None);
89
+ }
90
+ }
91
+ return finish(
92
+ plan,
93
+ "UNSATISFIED_EVIDENCE_REQUIREMENTS",
94
+ &[],
95
+ &zero_pairs(plan),
96
+ None,
97
+ );
98
+ }
99
+
100
+ let mut best: Option<ScoredCombo> = None;
101
+ for size in 0..=max_size {
102
+ let mut feasible = Vec::new();
103
+ let exceeded = visit_combinations(&pool, size, &mut visits, limit, |combo| {
104
+ let scored = coverage_of(plan, combo, &micros);
105
+ if scored.ok {
106
+ feasible.push(ScoredCombo {
107
+ combo: combo.to_vec(),
108
+ pairs: scored.pairs,
109
+ counts: scored.counts,
110
+ });
111
+ }
112
+ false
113
+ });
114
+ if exceeded {
115
+ return finish(plan, "SEARCH_BOUND_EXCEEDED", &[], &zero_pairs(plan), None);
116
+ }
117
+ if plan.profile == "minimum-cover-redundancy-v1" && !feasible.is_empty() {
118
+ feasible.sort_by(|left, right| {
119
+ let order = compare_rat(&redundancy(&left.combo), &redundancy(&right.combo));
120
+ if order != std::cmp::Ordering::Equal {
121
+ return order;
122
+ }
123
+ utf16_cmp(&join_ids(&left.combo), &join_ids(&right.combo))
124
+ });
125
+ let winner = &feasible[0];
126
+ let redundant = redundancy(&winner.combo);
127
+ return finish(
128
+ plan,
129
+ "SATISFIED",
130
+ &winner.combo,
131
+ &winner.pairs,
132
+ Some(redundant),
133
+ );
134
+ }
135
+ if plan.profile == "exp1-lexicographic" {
136
+ for item in feasible {
137
+ let replace = match &best {
138
+ None => true,
139
+ Some(current) => better_exp1(&item, current),
140
+ };
141
+ if replace {
142
+ best = Some(item);
143
+ }
144
+ }
145
+ }
146
+ }
147
+ if let Some(best) = best {
148
+ return finish(plan, "SATISFIED", &best.combo, &best.pairs, None);
149
+ }
150
+ finish(
151
+ plan,
152
+ "UNSATISFIED_EVIDENCE_REQUIREMENTS",
153
+ &[],
154
+ &zero_pairs(plan),
155
+ None,
156
+ )
157
+ }
158
+
159
+ struct ScoredCombo {
160
+ combo: Vec<Candidate>,
161
+ pairs: Vec<(String, CoveragePair)>,
162
+ counts: Vec<(String, i64)>,
163
+ }
164
+
165
+ pub fn covers_plan(
166
+ plan: &Plan,
167
+ admissions: &[Admission],
168
+ texts: &[(String, String)],
169
+ evidence_ids: &[String],
170
+ ) -> bool {
171
+ let micros = frontier_micros(plan);
172
+ let base = build_candidates(plan, admissions, texts);
173
+ let chosen: Vec<Candidate> = base
174
+ .into_iter()
175
+ .filter(|candidate| evidence_ids.iter().any(|id| id == &candidate.id))
176
+ .collect();
177
+ coverage_of(plan, &chosen, &micros).ok
178
+ }
179
+
180
+ fn frontier_micros(plan: &Plan) -> Vec<(String, i64)> {
181
+ FRONTIERS
182
+ .iter()
183
+ .map(|frontier| {
184
+ (
185
+ (*frontier).to_string(),
186
+ floor_micros(lookup_floor(plan, frontier)).unwrap_or(0),
187
+ )
188
+ })
189
+ .collect()
190
+ }
191
+
192
+ fn visit_combinations(
193
+ pool: &[Candidate],
194
+ size: usize,
195
+ visits: &mut i64,
196
+ limit: i64,
197
+ mut on_combo: impl FnMut(&[Candidate]) -> bool,
198
+ ) -> bool {
199
+ if size == 0 {
200
+ *visits += 1;
201
+ if *visits > limit {
202
+ return true;
203
+ }
204
+ on_combo(&[]);
205
+ return false;
206
+ }
207
+ if size > pool.len() {
208
+ return false;
209
+ }
210
+ let mut index: Vec<usize> = (0..size).collect();
211
+ loop {
212
+ *visits += 1;
213
+ if *visits > limit {
214
+ return true;
215
+ }
216
+ let combo: Vec<Candidate> = index.iter().map(|offset| pool[*offset].clone()).collect();
217
+ if on_combo(&combo) {
218
+ return false;
219
+ }
220
+ if !next_combination(&mut index, pool.len()) {
221
+ return false;
222
+ }
223
+ }
224
+ }
225
+
226
+ fn next_combination(index: &mut [usize], n: usize) -> bool {
227
+ let size = index.len();
228
+ let mut cursor = size as isize - 1;
229
+ while cursor >= 0 && index[cursor as usize] == n - size + cursor as usize {
230
+ cursor -= 1;
231
+ }
232
+ if cursor < 0 {
233
+ return false;
234
+ }
235
+ index[cursor as usize] += 1;
236
+ let mut next = cursor as usize + 1;
237
+ while next < size {
238
+ index[next] = index[next - 1] + 1;
239
+ next += 1;
240
+ }
241
+ true
242
+ }
243
+
244
+ fn build_candidates(
245
+ plan: &Plan,
246
+ admissions: &[Admission],
247
+ texts: &[(String, String)],
248
+ ) -> Vec<Candidate> {
249
+ let mut grouped: Vec<Candidate> = Vec::new();
250
+ for admission in admissions {
251
+ if !admission.applicable {
252
+ continue;
253
+ }
254
+ if !grouped
255
+ .iter()
256
+ .any(|candidate| candidate.id == admission.evidence_id)
257
+ {
258
+ grouped.push(Candidate {
259
+ id: admission.evidence_id.clone(),
260
+ mask: String::new(),
261
+ requirements: Vec::new(),
262
+ nonempty: BTreeSet::new(),
263
+ tokens: ascii_tokens(
264
+ texts
265
+ .iter()
266
+ .find(|(id, _)| id == &admission.evidence_id)
267
+ .map(|(_, text)| text.as_str())
268
+ .unwrap_or(""),
269
+ ),
270
+ bits: Vec::new(),
271
+ });
272
+ }
273
+ let candidate = grouped
274
+ .iter_mut()
275
+ .find(|candidate| candidate.id == admission.evidence_id)
276
+ .unwrap();
277
+ candidate.nonempty.insert(admission.frontier.clone());
278
+ if let Some((_, bucket)) = candidate
279
+ .requirements
280
+ .iter_mut()
281
+ .find(|(frontier, _)| frontier == &admission.frontier)
282
+ {
283
+ for requirement in &admission.requirements {
284
+ bucket.insert(requirement.clone());
285
+ }
286
+ } else {
287
+ candidate.requirements.push((
288
+ admission.frontier.clone(),
289
+ admission.requirements.iter().cloned().collect(),
290
+ ));
291
+ }
292
+ }
293
+ let mut candidates: Vec<Candidate> = grouped
294
+ .into_iter()
295
+ .filter(|candidate| {
296
+ if plan.coverage_mode == "nonempty" {
297
+ !candidate.nonempty.is_empty()
298
+ } else {
299
+ !mask_key(&candidate.requirements, &candidate.nonempty, plan).is_empty()
300
+ }
301
+ })
302
+ .collect();
303
+ for candidate in &mut candidates {
304
+ candidate.mask = mask_key(&candidate.requirements, &candidate.nonempty, plan);
305
+ candidate.bits = requirement_bits(plan, &candidate.requirements);
306
+ }
307
+ candidates.sort_by(|left, right| utf16_cmp(&left.id, &right.id));
308
+ candidates
309
+ }
310
+
311
+ fn minimum_cover_pool(plan: &Plan, base: &[Candidate]) -> Vec<Candidate> {
312
+ if plan.profile != "minimum-cover" {
313
+ return base.to_vec();
314
+ }
315
+ let mut best: Vec<Candidate> = Vec::new();
316
+ for candidate in base {
317
+ if let Some(current) = best.iter_mut().find(|item| item.mask == candidate.mask) {
318
+ if utf16_cmp(&candidate.id, &current.id) == std::cmp::Ordering::Less {
319
+ *current = candidate.clone();
320
+ }
321
+ } else {
322
+ best.push(candidate.clone());
323
+ }
324
+ }
325
+ best.sort_by(|left, right| utf16_cmp(&left.id, &right.id));
326
+ best
327
+ }
328
+
329
+ fn mask_key(
330
+ requirements: &[(String, BTreeSet<String>)],
331
+ nonempty: &BTreeSet<String>,
332
+ plan: &Plan,
333
+ ) -> String {
334
+ if plan.coverage_mode == "nonempty" {
335
+ let mut ids: Vec<&str> = nonempty.iter().map(String::as_str).collect();
336
+ ids.sort_by(|left, right| utf16_cmp(left, right));
337
+ return ids.join(",");
338
+ }
339
+ let mut parts = Vec::new();
340
+ for frontier in FRONTIERS {
341
+ let mut ids: Vec<&str> = requirements
342
+ .iter()
343
+ .find(|(name, _)| name == frontier)
344
+ .map(|(_, set)| {
345
+ set.iter()
346
+ .filter(|id| {
347
+ lookup_requirement(plan, frontier)
348
+ .iter()
349
+ .any(|required| required == *id)
350
+ })
351
+ .map(String::as_str)
352
+ .collect()
353
+ })
354
+ .unwrap_or_default();
355
+ ids.sort_by(|left, right| utf16_cmp(left, right));
356
+ if !ids.is_empty() {
357
+ parts.push(format!("{frontier}={}", ids.join("+")));
358
+ }
359
+ }
360
+ parts.join("|")
361
+ }
362
+
363
+ fn requirement_bits(
364
+ plan: &Plan,
365
+ requirements: &[(String, BTreeSet<String>)],
366
+ ) -> Vec<(String, BTreeSet<usize>)> {
367
+ let mut bits = Vec::new();
368
+ for frontier in FRONTIERS {
369
+ let mut mask = BTreeSet::new();
370
+ if plan.coverage_mode == "requirements" {
371
+ if let Some((_, have)) = requirements.iter().find(|(name, _)| name == frontier) {
372
+ for (index, requirement) in lookup_requirement(plan, frontier).iter().enumerate() {
373
+ if have.contains(requirement) {
374
+ mask.insert(index);
375
+ }
376
+ }
377
+ }
378
+ }
379
+ bits.push(((*frontier).to_string(), mask));
380
+ }
381
+ bits
382
+ }
383
+
384
+ struct Scored {
385
+ pairs: Vec<(String, CoveragePair)>,
386
+ ok: bool,
387
+ counts: Vec<(String, i64)>,
388
+ }
389
+
390
+ fn coverage_of(plan: &Plan, chosen: &[Candidate], micros: &[(String, i64)]) -> Scored {
391
+ let mut pairs = Vec::new();
392
+ let mut counts = Vec::new();
393
+ let mut ok = true;
394
+ for frontier in FRONTIERS {
395
+ let required_list = lookup_requirement(plan, frontier);
396
+ let (covered, required) = if plan.coverage_mode == "requirements" {
397
+ let required = required_list.len() as i64;
398
+ let covered = if required == 0 {
399
+ 0
400
+ } else {
401
+ let mut mask = BTreeSet::new();
402
+ for candidate in chosen {
403
+ if let Some((_, bits)) =
404
+ candidate.bits.iter().find(|(name, _)| name == frontier)
405
+ {
406
+ mask.extend(bits.iter().copied());
407
+ }
408
+ }
409
+ mask.len() as i64
410
+ };
411
+ (covered, required)
412
+ } else {
413
+ let covered = if chosen
414
+ .iter()
415
+ .any(|candidate| candidate.nonempty.contains(frontier))
416
+ {
417
+ 1
418
+ } else {
419
+ 0
420
+ };
421
+ (covered, 1)
422
+ };
423
+ let micro = micros
424
+ .iter()
425
+ .find(|(name, _)| name == frontier)
426
+ .map(|(_, value)| *value)
427
+ .unwrap_or(0);
428
+ if !passes(covered, required, micro) {
429
+ ok = false;
430
+ }
431
+ pairs.push(((*frontier).to_string(), CoveragePair { covered, required }));
432
+ counts.push(((*frontier).to_string(), covered));
433
+ }
434
+ Scored { pairs, ok, counts }
435
+ }
436
+
437
+ fn passes(covered: i64, required: i64, micros: i64) -> bool {
438
+ if required == 0 {
439
+ return true;
440
+ }
441
+ (covered as i128) * 1_000_000 >= (micros as i128) * (required as i128)
442
+ }
443
+
444
+ fn zero_pairs(plan: &Plan) -> Vec<(String, CoveragePair)> {
445
+ FRONTIERS
446
+ .iter()
447
+ .map(|frontier| {
448
+ let required = if plan.coverage_mode == "requirements" {
449
+ lookup_requirement(plan, frontier).len() as i64
450
+ } else {
451
+ 1
452
+ };
453
+ (
454
+ (*frontier).to_string(),
455
+ CoveragePair {
456
+ covered: 0,
457
+ required,
458
+ },
459
+ )
460
+ })
461
+ .collect()
462
+ }
463
+
464
+ fn finish(
465
+ plan: &Plan,
466
+ status: &str,
467
+ combo: &[Candidate],
468
+ pairs: &[(String, CoveragePair)],
469
+ redundant: Option<Rat>,
470
+ ) -> CoverOutcome {
471
+ let mut evidence_ids: Vec<String> =
472
+ combo.iter().map(|candidate| candidate.id.clone()).collect();
473
+ evidence_ids.sort_by(|left, right| utf16_cmp(left, right));
474
+ let choices = evidence_ids
475
+ .iter()
476
+ .map(|evidence_id| {
477
+ let candidate = combo.iter().find(|item| &item.id == evidence_id).unwrap();
478
+ let mut frontiers: Vec<String> = candidate.nonempty.iter().cloned().collect();
479
+ frontiers.sort_by(|left, right| utf16_cmp(left, right));
480
+ Choice {
481
+ evidence_id: evidence_id.clone(),
482
+ frontiers,
483
+ }
484
+ })
485
+ .collect();
486
+ let recorded = if status == "SATISFIED" {
487
+ coverage_of(plan, combo, &frontier_micros(plan)).pairs
488
+ } else {
489
+ zero_pairs(plan)
490
+ };
491
+ let _ = pairs;
492
+ let decision = Decision {
493
+ status: status.to_string(),
494
+ profile: plan.profile.clone(),
495
+ cardinality: if status == "SATISFIED" {
496
+ evidence_ids.len() as i64
497
+ } else {
498
+ 0
499
+ },
500
+ coverage: recorded,
501
+ redundancy: if status == "SATISFIED" && plan.profile == "minimum-cover-redundancy-v1" {
502
+ redundant.map(|value| Rational {
503
+ numerator: value.n as i64,
504
+ denominator: value.d as i64,
505
+ })
506
+ } else {
507
+ None
508
+ },
509
+ };
510
+ CoverOutcome {
511
+ status: status.to_string(),
512
+ evidence_ids,
513
+ choices,
514
+ decision,
515
+ }
516
+ }
517
+
518
+ pub fn decision_json(decision: &Decision) -> Json {
519
+ let mut pairs = vec![
520
+ ("cardinality".to_string(), Json::Int(decision.cardinality)),
521
+ (
522
+ "coverage".to_string(),
523
+ Json::Object(
524
+ decision
525
+ .coverage
526
+ .iter()
527
+ .map(|(frontier, pair)| {
528
+ (
529
+ frontier.clone(),
530
+ Json::object(vec![
531
+ ("covered", Json::Int(pair.covered)),
532
+ ("required", Json::Int(pair.required)),
533
+ ]),
534
+ )
535
+ })
536
+ .collect(),
537
+ ),
538
+ ),
539
+ (
540
+ "profile".to_string(),
541
+ Json::String(decision.profile.clone()),
542
+ ),
543
+ ("status".to_string(), Json::String(decision.status.clone())),
544
+ ];
545
+ if let Some(redundancy) = &decision.redundancy {
546
+ pairs.push((
547
+ "redundancy".to_string(),
548
+ Json::object(vec![
549
+ ("denominator", Json::Int(redundancy.denominator)),
550
+ ("numerator", Json::Int(redundancy.numerator)),
551
+ ]),
552
+ ));
553
+ }
554
+ Json::Object(pairs)
555
+ }
556
+
557
+ pub fn choices_json(choices: &[Choice]) -> Json {
558
+ Json::Array(
559
+ choices
560
+ .iter()
561
+ .map(|choice| {
562
+ Json::object(vec![
563
+ ("evidenceId", Json::String(choice.evidence_id.clone())),
564
+ (
565
+ "frontiers",
566
+ Json::Array(choice.frontiers.iter().cloned().map(Json::String).collect()),
567
+ ),
568
+ ])
569
+ })
570
+ .collect(),
571
+ )
572
+ }
573
+
574
+ pub fn evidence_set_root(choices: &[Choice]) -> String {
575
+ digest_json(&choices_json(choices))
576
+ }
577
+
578
+ pub fn decision_root(decision: &Decision) -> String {
579
+ digest_json(&decision_json(decision))
580
+ }
581
+
582
+ fn ascii_tokens(text: &str) -> Vec<String> {
583
+ let folded: String = text
584
+ .chars()
585
+ .map(|ch| {
586
+ if ch.is_ascii_uppercase() {
587
+ char::from(ch as u8 + 32)
588
+ } else {
589
+ ch
590
+ }
591
+ })
592
+ .collect();
593
+ folded
594
+ .split(|ch: char| !(ch.is_ascii_digit() || ('a'..='z').contains(&ch)))
595
+ .filter(|token| !token.is_empty())
596
+ .map(|token| token.to_string())
597
+ .collect()
598
+ }
599
+
600
+ fn redundancy(items: &[Candidate]) -> Rat {
601
+ if items.len() < 2 {
602
+ return Rat { n: 0, d: 1 };
603
+ }
604
+ let mut sum_n = 0i128;
605
+ let mut sum_d = 1i128;
606
+ let mut pairs = 0i128;
607
+ for i in 0..items.len() {
608
+ for j in (i + 1)..items.len() {
609
+ let part = jaccard(&items[i].tokens, &items[j].tokens);
610
+ sum_n = sum_n * part.d + part.n * sum_d;
611
+ sum_d *= part.d;
612
+ let g = gcd(sum_n, sum_d);
613
+ sum_n /= g;
614
+ sum_d /= g;
615
+ pairs += 1;
616
+ }
617
+ }
618
+ reduce(sum_n, pairs * sum_d)
619
+ }
620
+
621
+ fn jaccard(left_tokens: &[String], right_tokens: &[String]) -> Rat {
622
+ let left: BTreeSet<&str> = left_tokens.iter().map(String::as_str).collect();
623
+ let right: BTreeSet<&str> = right_tokens.iter().map(String::as_str).collect();
624
+ if left.is_empty() && right.is_empty() {
625
+ return Rat { n: 1, d: 1 };
626
+ }
627
+ let inter = left.iter().filter(|token| right.contains(*token)).count() as i128;
628
+ let union = left.len() as i128 + right.len() as i128 - inter;
629
+ if union == 0 {
630
+ Rat { n: 0, d: 1 }
631
+ } else {
632
+ Rat { n: inter, d: union }
633
+ }
634
+ }
635
+
636
+ fn gcd(a: i128, b: i128) -> i128 {
637
+ let mut x = a.abs();
638
+ let mut y = b.abs();
639
+ while y != 0 {
640
+ let next = x % y;
641
+ x = y;
642
+ y = next;
643
+ }
644
+ if x == 0 {
645
+ 1
646
+ } else {
647
+ x
648
+ }
649
+ }
650
+
651
+ fn reduce(mut n: i128, mut d: i128) -> Rat {
652
+ if d < 0 {
653
+ n = -n;
654
+ d = -d;
655
+ }
656
+ let g = gcd(n, d);
657
+ Rat { n: n / g, d: d / g }
658
+ }
659
+
660
+ fn compare_rat(left: &Rat, right: &Rat) -> std::cmp::Ordering {
661
+ (left.n * right.d).cmp(&(right.n * left.d))
662
+ }
663
+
664
+ fn join_ids(combo: &[Candidate]) -> String {
665
+ combo
666
+ .iter()
667
+ .map(|item| item.id.as_str())
668
+ .collect::<Vec<_>>()
669
+ .join("\0")
670
+ }
671
+
672
+ fn better_exp1(left: &ScoredCombo, right: &ScoredCombo) -> bool {
673
+ for frontier in ["F_O", "F_S", "F_Q", "F_T", "F_A"] {
674
+ let left_count = count_of(&left.counts, frontier);
675
+ let right_count = count_of(&right.counts, frontier);
676
+ if left_count != right_count {
677
+ return left_count > right_count;
678
+ }
679
+ }
680
+ if left.combo.len() != right.combo.len() {
681
+ return left.combo.len() < right.combo.len();
682
+ }
683
+ utf16_cmp(&join_ids(&left.combo), &join_ids(&right.combo)) == std::cmp::Ordering::Less
684
+ }
685
+
686
+ fn count_of(counts: &[(String, i64)], frontier: &str) -> i64 {
687
+ counts
688
+ .iter()
689
+ .find(|(name, _)| name == frontier)
690
+ .map(|(_, value)| *value)
691
+ .unwrap_or(0)
692
+ }