@knolo/cli 6.2.0 → 6.2.2
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.
- package/README.md +82 -3
- package/bin/flight.mjs +367 -0
- package/bin/knolo.mjs +52 -0
- package/bin/setup.mjs +260 -0
- package/bin/skill.mjs +1308 -0
- package/package.json +2 -2
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/closure.rs +395 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/cover.rs +692 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/evaluate.rs +343 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/json.rs +486 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/mod.rs +14 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/model.rs +1068 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/session.rs +1156 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/lib.rs +7 -1
|
@@ -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, µs);
|
|
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, µs);
|
|
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, µs).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, ¤t.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
|
+
}
|