1use std::collections::HashSet;
9use std::path::Path;
10
11use serde::Deserialize;
12
13use crate::collective::{self, Aliases};
14use crate::trust;
15
16#[derive(Debug, Clone, Deserialize)]
17pub struct Challenge {
18 pub id: String,
19 pub origin_ms: i64,
21 pub question: String,
22 #[serde(default)]
24 pub situations: Vec<String>,
25 #[serde(default)]
26 pub consequence: String,
27 #[serde(default)]
29 pub area: String,
30 #[serde(default)]
32 pub engineers: Vec<String>,
33 #[serde(default)]
35 pub joined: std::collections::BTreeMap<String, i64>,
36 #[serde(default)]
38 pub supersedes: Vec<String>,
39 #[serde(default)]
41 pub source: String,
42 #[serde(default)]
44 pub scope: Vec<String>,
45 #[serde(default)]
47 pub overrides: String,
48 #[serde(default)]
51 pub facets: Vec<String>,
52 #[serde(default)]
54 pub repo: String,
55 #[serde(default)]
57 pub reach: String,
58 #[serde(default)]
60 pub episodes: Vec<String>,
61 #[serde(default)]
64 pub version: u32,
65 #[serde(skip)]
69 pub correction: String,
70 #[serde(skip)]
72 pub off_system: bool,
73 #[serde(skip, default = "one")]
76 pub weight: f64,
77 #[serde(skip)]
79 pub dormant: bool,
80 #[serde(skip)]
82 pub consolidated: bool,
83 #[serde(skip, default = "one")]
87 pub explore: f64,
88 #[serde(skip, default = "uniform")]
91 pub belief: (f64, f64),
92 #[serde(skip, default = "neutral")]
94 pub trust: f64,
95 #[serde(skip)]
98 pub activation: f64,
99}
100
101fn one() -> f64 {
102 1.0
103}
104
105fn uniform() -> (f64, f64) {
106 (1.0, 1.0)
107}
108
109fn neutral() -> f64 {
110 trust::NEUTRAL
111}
112
113impl Default for Challenge {
114 fn default() -> Self {
115 Challenge {
116 id: String::new(),
117 origin_ms: 0,
118 question: String::new(),
119 situations: Vec::new(),
120 consequence: String::new(),
121 area: trust::OTHER.into(),
122 engineers: Vec::new(),
123 joined: Default::default(),
124 supersedes: Vec::new(),
125 source: String::new(),
126 scope: Vec::new(),
127 overrides: String::new(),
128 facets: Vec::new(),
129 repo: String::new(),
130 reach: String::new(),
131 episodes: Vec::new(),
132 version: 0,
133 correction: String::new(),
134 off_system: false,
135 weight: 1.0,
136 dormant: false,
137 consolidated: false,
138 explore: 1.0,
139 belief: uniform(),
140 trust: trust::NEUTRAL,
141 activation: 0.0,
142 }
143 }
144}
145
146pub fn load(path: &Path, before_ms: Option<i64>) -> Vec<Challenge> {
153 load_with(path, before_ms, collective::aliases())
154}
155
156pub fn load_with(path: &Path, before_ms: Option<i64>, aliases: &Aliases) -> Vec<Challenge> {
158 load_as(path, before_ms, aliases, None, false)
159}
160
161pub fn load_for(path: &Path, before_ms: Option<i64>, session: Option<&str>) -> Vec<Challenge> {
164 load_as(path, before_ms, collective::aliases(), session, false)
165}
166
167pub fn load_store_for(path: &Path, before_ms: Option<i64>, session: Option<&str>) -> Vec<Challenge> {
172 load_as(path, before_ms, collective::aliases(), session, true)
173}
174
175pub fn load_store_with(path: &Path, before_ms: Option<i64>, aliases: &Aliases) -> Vec<Challenge> {
177 load_as(path, before_ms, aliases, None, true)
178}
179
180fn load_as(path: &Path, before_ms: Option<i64>, aliases: &Aliases, session: Option<&str>, personal: bool) -> Vec<Challenge> {
181 let outcomes = crate::outcome::read_team(path);
182 let mut out = load_unweighted_with(path, before_ms, aliases);
183 if personal {
184 with_personal(&mut out, path, before_ms, aliases);
185 }
186 let memory = crate::memory::derive(&outcomes, &out, session);
187 let trust = trust::Trust::compute(&out, &outcomes);
188 for c in &mut out {
189 if let Some(m) = memory.get(&c.id) {
190 c.weight = m.weight(&c.id, None);
194 c.explore = if session.is_some() { m.weight(&c.id, session) / c.weight } else { 1.0 };
195 c.belief = (m.alpha(), m.beta());
196 c.dormant = m.dormant;
197 c.consolidated = m.consolidated;
198 c.activation = m.activation;
199 }
200 c.trust = trust.lesson(c);
201 }
202 out.extend(constraints(path, before_ms, aliases, &out));
203 out
204}
205
206pub fn with_corrections(lessons: &Path, cs: &mut [Challenge]) {
211 use std::io::BufRead;
212 if crate::whole::line_format() != crate::whole::Line::A2 || cs.iter().all(|c| c.episodes.is_empty()) {
213 return;
214 }
215 let Ok(f) = std::fs::File::open(lessons.with_file_name("episodes.jsonl")) else { return };
216 let wanted: HashSet<&str> = cs.iter().flat_map(|c| c.episodes.iter().map(String::as_str)).collect();
217 #[derive(Deserialize)]
218 struct Said {
219 id: String,
220 #[serde(default)]
221 correction: String,
222 }
223 let mut said: std::collections::HashMap<String, String> = std::collections::HashMap::new();
224 for line in std::io::BufReader::new(f).lines().map_while(Result::ok) {
225 if let Ok(e) = serde_json::from_str::<Said>(&line)
226 && wanted.contains(e.id.as_str())
227 && !e.correction.trim().is_empty()
228 {
229 said.entry(e.id).or_insert(e.correction);
230 }
231 }
232 for c in cs {
233 if let Some(text) = c.episodes.iter().find_map(|e| said.get(e)) {
234 c.correction.clone_from(text);
235 }
236 }
237}
238
239pub const CONSTRAINT: &str = "constraint";
241
242impl Challenge {
243 pub fn standing(&self) -> bool {
248 self.source == CONSTRAINT
249 }
250}
251
252fn constraints(path: &Path, before_ms: Option<i64>, aliases: &Aliases, lessons: &[Challenge]) -> Vec<Challenge> {
261 let Some(dir) = path.parent() else { return Vec::new() };
262 let Ok(text) = std::fs::read_to_string(crate::veto::constraints_path(dir)) else { return Vec::new() };
263 let mut by: std::collections::BTreeMap<String, (i64, String, Challenge)> = std::collections::BTreeMap::new();
264 for v in text.lines().filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok()) {
265 let s = |k: &str| v[k].as_str().unwrap_or_default().trim().to_string();
266 let (id, question, ms) = (s("id"), s("text"), v["ms"].as_i64().unwrap_or(0));
267 if id.is_empty() || question.is_empty() || before_ms.is_some_and(|b| ms >= b) {
268 continue;
269 }
270 let author = aliases.canon(&s("by"));
271 if by.get(&id).is_some_and(|(m, a, _)| (*m, a.as_str()) <= (ms, author.as_str())) {
272 continue;
273 }
274 let line_scope: Vec<String> = v["scope"].as_array().into_iter().flatten().filter_map(|x| x.as_str()).map(str::to_string).collect();
275 let (scope, repo, reach, area) = match lessons.iter().find(|l| l.id == s("lesson")) {
276 Some(l) => {
277 let repo = if l.repo.is_empty() { s("repo") } else { l.repo.clone() };
278 (l.scope.clone(), repo, crate::reach::narrower(&s("reach"), &l.reach).to_string(), l.area.clone())
279 }
280 None if s("repo").is_empty() => continue,
281 None => (line_scope, s("repo"), crate::reach::narrower(&s("reach"), crate::reach::REPO).to_string(), trust::OTHER.into()),
282 };
283 let c = Challenge {
284 id: id.clone(),
285 origin_ms: ms,
286 question,
287 source: CONSTRAINT.into(),
288 engineers: Some(author.clone()).filter(|a| !a.is_empty()).into_iter().collect(),
289 scope,
290 repo,
291 reach,
292 area,
293 consolidated: true,
294 ..Challenge::default()
295 };
296 by.insert(id, (ms, author, c));
297 }
298 by.into_values().map(|(_, _, c)| c).collect()
299}
300
301fn with_personal(out: &mut Vec<Challenge>, path: &Path, before_ms: Option<i64>, aliases: &Aliases) {
304 let mut mine = load_unweighted_with(&path.with_file_name(crate::reach::PERSONAL_FILE), before_ms, aliases);
305 mine.retain(|c| c.question != crate::reach::MOVED);
307 if !mine.is_empty() {
308 out.retain(|c| !mine.iter().any(|m| m.id == c.id));
309 out.extend(mine.into_iter().map(|c| Challenge { reach: String::new(), ..c }));
310 }
311}
312
313pub fn load_store_unweighted(path: &Path, before_ms: Option<i64>) -> Vec<Challenge> {
316 let aliases = collective::aliases();
317 let mut out = load_unweighted_with(path, before_ms, aliases);
318 with_personal(&mut out, path, before_ms, aliases);
319 out
320}
321
322pub fn load_unweighted(path: &Path, before_ms: Option<i64>) -> Vec<Challenge> {
325 load_unweighted_with(path, before_ms, collective::aliases())
326}
327
328pub fn load_unweighted_with(path: &Path, before_ms: Option<i64>, aliases: &Aliases) -> Vec<Challenge> {
330 let Ok(text) = std::fs::read_to_string(path) else { return Vec::new() };
331 let mut out: Vec<Challenge> = Vec::new();
333 let mut at: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
334 for mut c in text.lines().filter_map(|l| serde_json::from_str::<Challenge>(l).ok()) {
335 c.area = trust::area(&c.area).into();
336 canonical(&mut c, aliases);
337 if before_ms.is_some_and(|b| c.origin_ms >= b) {
338 continue;
339 }
340 match at.get(&c.id) {
341 Some(&i) => out[i] = c,
342 None => {
343 at.insert(c.id.clone(), out.len());
344 out.push(c);
345 }
346 }
347 }
348 out
349}
350
351fn canonical(c: &mut Challenge, aliases: &Aliases) {
356 let mut engineers: Vec<String> = Vec::new();
357 for e in c.engineers.iter().map(|e| aliases.canon(e)).filter(|e| !e.is_empty()) {
358 if !engineers.contains(&e) {
359 engineers.push(e);
360 }
361 }
362 let joined = engineers
363 .iter()
364 .filter_map(|who| {
365 let raws = c.engineers.iter().filter(|r| aliases.canon(r) == *who);
366 let at: Option<Vec<i64>> = raws.map(|r| c.joined.get(r).copied()).collect();
367 Some((who.clone(), at?.into_iter().min()?))
368 })
369 .collect();
370 (c.engineers, c.joined) = (engineers, joined);
371}
372
373pub fn tokens(text: &str) -> Vec<String> {
375 let mut out = Vec::new();
376 for raw in text.split(|c: char| !c.is_alphanumeric() && c != '_') {
377 if raw.len() < 3 {
378 continue;
379 }
380 out.push(raw.to_lowercase());
381 let mut word = String::new();
382 for ch in raw.chars() {
383 if ch.is_uppercase() && !word.is_empty() {
384 if word.len() >= 3 && word.len() < raw.len() {
385 out.push(word.to_lowercase());
386 }
387 word.clear();
388 }
389 word.push(ch);
390 }
391 if word.len() >= 3 && word.len() < raw.len() {
392 out.push(word.to_lowercase());
393 }
394 }
395 out
396}
397
398pub fn rank<'a>(challenges: &'a [Challenge], context: &str) -> Vec<(f64, &'a Challenge)> {
417 rank_scoped(challenges, context, context)
418}
419
420pub fn work_text(context: &str) -> std::borrow::Cow<'_, str> {
424 if !context.contains("#borg-") {
425 return std::borrow::Cow::Borrowed(context);
426 }
427 std::borrow::Cow::Owned(context.split_inclusive('\n').filter(|l| !l.starts_with("#borg-")).collect())
428}
429
430pub fn rank_scoped<'a>(challenges: &'a [Challenge], context: &str, scope_text: &str) -> Vec<(f64, &'a Challenge)> {
433 rank_raw(challenges, raw(challenges, context), scope_text)
434}
435
436pub fn raw(challenges: &[Challenge], context: &str) -> Vec<f64> {
440 let groups: Vec<Vec<Vec<String>>> = challenges.iter().map(documents).collect();
441 bm25_groups(&groups, context, 0.0)
442}
443
444pub fn rank_raw<'a>(challenges: &'a [Challenge], mut raw: Vec<f64>, scope_text: &str) -> Vec<(f64, &'a Challenge)> {
446 if challenges.iter().any(|c| !c.scope.is_empty()) {
447 let have = Names::of(scope_text);
448 for (r, c) in raw.iter_mut().zip(challenges) {
449 if !in_scope(c, &have) || c.off_system {
450 *r = 0.0;
451 }
452 }
453 }
454 let best = raw.iter().copied().fold(0.0, f64::max);
456 for (r, c) in raw.iter_mut().zip(challenges) {
457 if c.dormant && *r < STRONG * best {
458 *r = 0.0;
459 }
460 }
461 let successors = trust::successors(challenges);
462 let mut relevance = raw.clone();
463 if !successors.is_empty() {
464 let at: std::collections::HashMap<&str, usize> = challenges.iter().enumerate().map(|(i, c)| (c.id.as_str(), i)).collect();
465 for (i, c) in challenges.iter().enumerate() {
466 if let Some(&j) = successors.get(&c.id).and_then(|s| at.get(s.as_str())) {
467 relevance[j] = relevance[j].max(raw[i]);
468 }
469 }
470 }
471 let mut scored: Vec<(f64, &Challenge)> = relevance
472 .into_iter()
473 .zip(challenges)
474 .filter(|(_, c)| !successors.contains_key(&c.id))
475 .map(|(s, c)| (s * c.weight * c.trust / trust::NEUTRAL, c))
476 .filter(|(s, _)| *s > 0.0)
477 .collect();
478 scored.sort_by(|a, b| b.0.total_cmp(&a.0).then(b.1.origin_ms.cmp(&a.1.origin_ms)));
479 scored
480}
481
482pub const STRONG: f64 = 0.8;
486
487pub fn in_scope(c: &Challenge, have: &Names) -> bool {
492 c.scope.is_empty() || c.scope.iter().any(|s| have.contains(s))
493}
494
495pub fn segments(text: &str) -> Vec<String> {
498 let mut out = Vec::new();
499 for raw in text.split(|c: char| !c.is_alphanumeric()).filter(|r| !r.is_empty()) {
500 let cs: Vec<char> = raw.chars().collect();
501 let mut word = String::new();
502 for (i, &ch) in cs.iter().enumerate() {
503 let hump = i > 0
504 && ch.is_uppercase()
505 && (cs[i - 1].is_lowercase() || cs[i - 1].is_numeric() || (cs[i - 1].is_uppercase() && cs.get(i + 1).is_some_and(|n| n.is_lowercase())));
506 if hump && !word.is_empty() {
507 out.push(std::mem::take(&mut word).to_lowercase());
508 }
509 word.push(ch);
510 }
511 out.push(word.to_lowercase());
512 }
513 out
514}
515
516pub struct Names {
518 seq: Vec<String>,
519 at: std::collections::HashMap<String, Vec<usize>>,
520}
521
522impl Names {
523 pub fn of(text: &str) -> Self {
524 Self::of_all([text])
525 }
526
527 pub fn of_all<'a>(texts: impl IntoIterator<Item = &'a str>) -> Self {
529 let mut seq = Vec::new();
530 for t in texts {
531 seq.extend(segments(t));
532 seq.push(String::new()); }
534 let mut at: std::collections::HashMap<String, Vec<usize>> = std::collections::HashMap::new();
535 for (i, s) in seq.iter().enumerate() {
536 at.entry(s.clone()).or_default().push(i);
537 }
538 Names { seq, at }
539 }
540
541 pub fn contains(&self, name: &str) -> bool {
543 let need = segments(name);
544 let Some(first) = need.first() else { return false };
545 self.at.get(first).is_some_and(|starts| starts.iter().any(|&i| self.seq.get(i..i + need.len()).is_some_and(|w| w == need.as_slice())))
546 }
547}
548
549pub const BM25_K1: f64 = 1.2;
553pub const BM25_B: f64 = 0.75;
554
555pub fn bm25(docs: &[Vec<String>], query: &str) -> Vec<f64> {
557 let (k1, b) = (BM25_K1, BM25_B);
558 let n = docs.len().max(1) as f64;
559 let avg = docs.iter().map(Vec::len).sum::<usize>() as f64 / n;
560 let mut df: std::collections::HashMap<&str, f64> = std::collections::HashMap::new();
561 for d in docs {
562 for t in d.iter().map(String::as_str).collect::<HashSet<_>>() {
563 *df.entry(t).or_default() += 1.0;
564 }
565 }
566 let query: HashSet<String> = tokens(query).into_iter().collect();
567 docs.iter()
568 .map(|d| {
569 let mut tf: std::collections::HashMap<&str, f64> = std::collections::HashMap::new();
570 for t in d {
571 *tf.entry(t.as_str()).or_default() += 1.0;
572 }
573 query
574 .iter()
575 .filter_map(|t| tf.get(t.as_str()).map(|f| (t, *f)))
576 .map(|(t, f)| {
577 let dft = df[t.as_str()];
578 let idf = (1.0 + (n - dft + 0.5) / (dft + 0.5)).ln();
579 idf * f * (k1 + 1.0) / (f + k1 * (1.0 - b + b * d.len() as f64 / avg))
580 })
581 .sum()
582 })
583 .collect()
584}
585
586pub fn documents(c: &Challenge) -> Vec<Vec<String>> {
588 std::iter::once(tokens(&c.question)).chain(c.facets.iter().map(|f| tokens(f)).filter(|d| !d.is_empty())).collect()
589}
590
591pub fn bm25_groups(groups: &[Vec<Vec<String>>], query: &str, second: f64) -> Vec<f64> {
597 let (k1, b) = (BM25_K1, BM25_B);
598 let n = groups.len().max(1) as f64;
599 let ndocs = groups.iter().map(Vec::len).sum::<usize>().max(1) as f64;
600 let avg = groups.iter().flatten().map(Vec::len).sum::<usize>() as f64 / ndocs;
601 let mut index: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
602 for t in tokens(query) {
603 let next = index.len();
604 index.entry(t).or_insert(next);
605 }
606 let counted: Vec<Vec<Vec<(usize, f64)>>> = groups
608 .iter()
609 .map(|g| {
610 g.iter()
611 .map(|d| {
612 let mut tf: Vec<(usize, f64)> = Vec::new();
613 for q in d.iter().filter_map(|t| index.get(t.as_str())) {
614 match tf.iter_mut().find(|(x, _)| x == q) {
615 Some((_, f)) => *f += 1.0,
616 None => tf.push((*q, 1.0)),
617 }
618 }
619 tf.sort_by_key(|(q, _)| *q);
620 tf
621 })
622 .collect()
623 })
624 .collect();
625 let mut df = vec![0.0f64; index.len()];
626 let mut seen: Vec<usize> = Vec::new();
627 for g in &counted {
628 seen.clear();
629 for &(q, _) in g.iter().flatten() {
630 if !seen.contains(&q) {
631 seen.push(q);
632 df[q] += 1.0;
633 }
634 }
635 }
636 let idf: Vec<f64> = df.iter().map(|&d| (1.0 + (n - d + 0.5) / (d + 0.5)).ln()).collect();
637 counted
638 .iter()
639 .zip(groups)
640 .map(|(g, docs)| {
641 let (mut best, mut next) = (0.0f64, 0.0f64);
642 for (tf, d) in g.iter().zip(docs) {
643 let norm = k1 * (1.0 - b + b * d.len() as f64 / avg);
644 let s: f64 = tf.iter().map(|&(q, f)| idf[q] * f * (k1 + 1.0) / (f + norm)).sum();
645 if s > best {
646 (best, next) = (s, best);
647 } else if s > next {
648 next = s;
649 }
650 }
651 best + second * next
652 })
653 .collect()
654}
655
656pub fn select<'a>(challenges: &'a [Challenge], context: &str, k: usize) -> Vec<&'a Challenge> {
658 select_scoped(challenges, context, context, k)
659}
660
661pub fn select_scoped<'a>(challenges: &'a [Challenge], context: &str, scope_text: &str, k: usize) -> Vec<&'a Challenge> {
663 rank_scoped(challenges, context, scope_text).into_iter().take(k).map(|(_, c)| c).collect()
664}
665
666pub const RELATIVE: f64 = 0.5;
673pub const BLOCK: f64 = 0.8;
677pub const LOW: f64 = 0.3;
680pub const CAP: usize = 10;
684pub const GENERIC_CAP: usize = 3;
687
688pub fn budget<'a>(ranked: &[(f64, &'a Challenge)], done: &HashSet<&str>) -> Vec<&'a Challenge> {
697 in_scope_first(explored(budget_pool(ranked, done)))
698}
699
700pub fn budget_pool<'a>(ranked: &[(f64, &'a Challenge)], done: &HashSet<&str>) -> Vec<(f64, &'a Challenge)> {
703 let Some(&(top, _)) = ranked.first() else { return Vec::new() };
704 if !ranked.iter().any(|(s, c)| *s >= BLOCK * top && !done.contains(c.id.as_str())) {
705 return Vec::new();
706 }
707 ranked.iter().copied().filter(|(s, c)| *s >= LOW * top && !done.contains(c.id.as_str())).collect()
708}
709
710pub fn widen<'a>(ranked: &[(f64, &'a Challenge)], predicted: &[&'a Challenge], done: &HashSet<&str>) -> Vec<&'a Challenge> {
713 in_scope_first(explored(widen_pool(ranked, predicted, done)))
714}
715
716pub fn widen_pool<'a>(ranked: &[(f64, &'a Challenge)], predicted: &[&'a Challenge], done: &HashSet<&str>) -> Vec<(f64, &'a Challenge)> {
719 if predicted.is_empty() {
720 return Vec::new();
721 }
722 let top = ranked.first().map_or(0.0, |r| r.0);
723 let score = |c: &Challenge| ranked.iter().find(|(_, x)| x.id == c.id).map_or(0.0, |r| r.0);
724 let mut pool: Vec<(f64, &Challenge)> = predicted.iter().map(|c| (score(c).max(top), *c)).collect();
726 for &(s, c) in ranked {
727 if s >= LOW * top && !done.contains(c.id.as_str()) && !pool.iter().any(|(_, x)| x.id == c.id) {
728 pool.push((s, c));
729 }
730 }
731 pool
732}
733
734pub fn graded<'a>(applied: &[(&'a Challenge, u8)]) -> (Vec<&'a Challenge>, Vec<&'a Challenge>) {
742 graded_capped(applied, ask_cap())
743}
744
745pub fn ask_cap() -> Option<usize> {
748 std::env::var("BORG_STOP_ASK_CAP").ok().and_then(|v| v.trim().parse().ok()).filter(|n| *n > 0)
749}
750
751pub fn graded_capped<'a>(applied: &[(&'a Challenge, u8)], cap: Option<usize>) -> (Vec<&'a Challenge>, Vec<&'a Challenge>) {
753 graded_without(applied, cap, &[])
754}
755
756pub fn graded_without<'a>(applied: &[(&'a Challenge, u8)], cap: Option<usize>, had: &[String]) -> (Vec<&'a Challenge>, Vec<&'a Challenge>) {
761 let of = |g: u8| applied.iter().filter(move |(_, x)| *x == g).map(|(c, _)| *c);
762 if of(3).next().is_none() {
763 return (Vec::new(), of(2).chain(of(1)).filter(|c| !had.contains(&c.id)).take(CAP).collect());
764 }
765 if let Some(n) = cap {
766 return (of(3).take(n).collect(), Vec::new());
767 }
768 let mut ask: Vec<&Challenge> = of(3).chain(of(2)).collect();
769 let room = CAP.saturating_sub(ask.len());
770 ask.extend(of(1).take(room));
771 (ask, Vec::new())
772}
773
774pub fn explored<'a>(passed: impl IntoIterator<Item = (f64, &'a Challenge)>) -> Vec<&'a Challenge> {
777 let mut v: Vec<(f64, &Challenge)> = passed.into_iter().map(|(s, c)| (s * c.explore, c)).collect();
778 v.sort_by(|a, b| b.0.total_cmp(&a.0));
779 v.into_iter().map(|(_, c)| c).collect()
780}
781
782pub fn in_scope_first<'a>(candidates: impl IntoIterator<Item = &'a Challenge>) -> Vec<&'a Challenge> {
784 let (scoped, generic): (Vec<&Challenge>, Vec<&Challenge>) = candidates.into_iter().partition(|c| !c.scope.is_empty());
785 scoped.into_iter().chain(generic.into_iter().take(GENERIC_CAP)).take(CAP).collect()
786}
787
788pub const P_DRAWS: usize = 512;
791
792pub fn propensity(pool: &[(f64, &Challenge)], session: &str) -> std::collections::HashMap<String, f64> {
798 if pool.iter().filter(|(_, c)| c.scope.is_empty()).count() <= GENERIC_CAP && pool.len() <= CAP {
799 return pool.iter().map(|(_, c)| (c.id.clone(), 1.0)).collect();
800 }
801 kept_share(pool, session, |order| in_scope_first(order).into_iter().map(|c| c.id.clone()).collect())
802}
803
804fn kept_share(pool: &[(f64, &Challenge)], session: &str, keep: impl Fn(Vec<&Challenge>) -> Vec<String>) -> std::collections::HashMap<String, f64> {
807 let draw = |c: &Challenge, i: usize| {
808 let (a, b) = c.belief;
809 let x = crate::memory::draw(a, b, crate::memory::seed(&format!("{session}#p{i}"), &c.id));
810 (0.5 + x) / (0.5 + a / (a + b))
811 };
812 let mut kept: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
813 for i in 0..P_DRAWS {
814 let mut v: Vec<(f64, &Challenge)> = pool.iter().map(|&(s, c)| (s * draw(c, i), c)).collect();
815 v.sort_by(|a, b| b.0.total_cmp(&a.0));
816 for id in keep(v.into_iter().map(|(_, c)| c).collect()) {
817 *kept.entry(id).or_default() += 1;
818 }
819 }
820 pool.iter().map(|(_, c)| (c.id.clone(), kept.get(&c.id).copied().unwrap_or(0) as f64 / P_DRAWS as f64)).collect()
821}
822
823pub fn pool_base(s: f64, c: &Challenge) -> f64 {
828 let (a, b) = c.belief;
829 s / (0.5 + a / (a + b))
830}
831
832pub const EARLY_CAP: usize = 5;
834pub const EARLY_CHARS: usize = 2000;
838pub const EARLY_QUESTION_CHARS: usize = 300;
842pub const EARLY_CONSEQUENCE_CHARS: usize = 200;
845pub const EARLY_OVERRIDES_CHARS: usize = 150;
848
849pub fn named_scopes(challenges: &[Challenge], text: &str) -> Vec<String> {
851 let have = Names::of(text);
852 let mut out: Vec<String> = Vec::new();
853 for s in challenges.iter().flat_map(|c| &c.scope) {
854 if !out.contains(s) && have.contains(s) {
855 out.push(s.clone());
856 }
857 }
858 out
859}
860
861pub fn early<'a>(challenges: &'a [Challenge], context: &str, topics: &[String], done: &HashSet<&str>) -> Vec<&'a Challenge> {
867 let pool: Vec<(f64, &Challenge)> = standing_pool(challenges, topics, done).into_iter().chain(early_pool(challenges, context, topics, done)).collect();
868 explored(pool).into_iter().take(EARLY_CAP).collect()
869}
870
871pub fn early_pool<'a>(challenges: &'a [Challenge], context: &str, topics: &[String], done: &HashSet<&str>) -> Vec<(f64, &'a Challenge)> {
874 let on_topic = Names::of_all(topics.iter().map(String::as_str));
875 let any = topics.iter().any(|t| t == ANY);
876 let ranked: Vec<(f64, &Challenge)> = rank(challenges, context)
877 .into_iter()
878 .filter(|(_, c)| !c.standing() && ((!c.scope.is_empty() && c.scope.iter().any(|s| on_topic.contains(s))) || (any && c.consolidated)))
879 .collect();
880 let Some(&(top, _)) = ranked.first() else { return Vec::new() };
882 ranked.into_iter().filter(|(s, c)| *s >= RELATIVE * top && !done.contains(c.id.as_str())).collect()
883}
884
885pub fn standing_pool<'a>(challenges: &'a [Challenge], topics: &[String], done: &HashSet<&str>) -> Vec<(f64, &'a Challenge)> {
888 let on_topic = Names::of_all(topics.iter().map(String::as_str));
889 let any = topics.iter().any(|t| t == ANY);
890 challenges
891 .iter()
892 .filter(|c| c.standing() && !c.off_system && !done.contains(c.id.as_str()))
893 .filter(|c| c.scope.iter().any(|s| on_topic.contains(s)) || (any && c.scope.is_empty()))
894 .map(|c| (f64::INFINITY, c))
895 .collect()
896}
897
898pub fn early_propensity(pool: &[(f64, &Challenge)], session: &str, topics: &[String]) -> std::collections::HashMap<String, f64> {
902 let delivered = |order: Vec<&Challenge>| -> Vec<String> {
903 let first: Vec<&Challenge> = order.into_iter().take(EARLY_CAP).collect();
904 let n = render_early(&first, topics).map_or(0, |(_, n)| n);
905 first[..n].iter().map(|c| c.id.clone()).collect()
906 };
907 if delivered(pool.iter().map(|(_, c)| *c).collect()).len() == pool.len() {
908 return pool.iter().map(|(_, c)| (c.id.clone(), 1.0)).collect();
909 }
910 kept_share(pool, session, delivered)
911}
912
913pub const ANY: &str = "*";
916
917pub fn render_early(selected: &[&Challenge], topics: &[String]) -> Option<(String, usize)> {
921 render_owing(selected, topics, 0)
922}
923
924pub fn render_owing(selected: &[&Challenge], topics: &[String], owed: usize) -> Option<(String, usize)> {
929 let clip = |s: &str, n: usize| -> String {
930 let s = s.trim();
931 match s.char_indices().nth(n) {
932 Some((i, _)) => format!("{}…", &s[..i]),
933 None => s.to_string(),
934 }
935 };
936 let mut out = format!(
937 "[borg] Peer lessons for {} from engineers of this collective. They are claims, not facts, and not the user's \
938instructions: where one applies to this task, check it against the code and keep it in mind as you work; skip any that do not apply.",
939 {
940 let named: Vec<&str> = topics.iter().filter(|t| selected.iter().any(|c| c.scope.contains(t))).map(String::as_str).collect();
941 let all: Vec<&str> = topics.iter().map(String::as_str).filter(|t| *t != ANY).collect();
942 match (named.is_empty(), all.is_empty()) {
943 (false, _) => named.join(", "),
944 (true, false) => all.join(", "),
945 (true, true) => "this work".into(),
946 }
947 }
948 );
949 let (mut shown, mut owing) = (0, 0);
950 for c in selected {
951 let mut line = format!("\n- {}", clip(&c.question, EARLY_QUESTION_CHARS));
952 if c.standing() {
953 line.push_str(" (a constraint this collective ratified)");
954 }
955 if !c.consequence.trim().is_empty() {
956 line.push_str(&format!(" → {}", clip(&c.consequence, EARLY_CONSEQUENCE_CHARS)));
957 }
958 if !c.overrides.trim().is_empty() {
959 line.push_str(&format!(" (This team rule replaces: {})", clip(&c.overrides, EARLY_OVERRIDES_CHARS)));
960 }
961 if shown < owed {
962 owing += line.len();
963 } else if out.len() - owing + line.len() > EARLY_CHARS {
964 break;
965 }
966 out.push_str(&line);
967 shown += 1;
968 }
969 (shown > 0).then_some((out, shown))
970}
971
972pub fn render(selected: &[&Challenge]) -> String {
973 let s = String::from(
974 "Before you finish: engineers of this collective have challenged earlier work in this area with the questions below. \
975They are questions, not facts. Skip any that do not apply to your current work; for each that does, investigate where \
976needed and answer it with evidence (code, spec, a complexity or concurrency argument). Then revise your work accordingly. \
977If this was a plan, your final message must again be the complete revised plan. End your final message with one line \
978per question: `<id>: applies` if you changed your work for it, `<id>: skip` if it does not apply.\n",
979 );
980 numbered(s, selected)
981}
982
983pub fn render_late(selected: &[&Challenge], excerpt: &str) -> String {
988 let turn = if excerpt.trim().is_empty() { "your last turn".to_string() } else { format!("your turn that ended with \"{}…\"", excerpt.trim()) };
989 let s = format!(
990 "[borg] After {turn}, engineers of this collective challenged that work with the questions below. They are \
991questions, not facts. Skip any that do not apply, and skip one the work since then already settled; for each that \
992does apply, investigate where needed and answer it with evidence (code, spec, a complexity or concurrency argument), \
993then revise your work accordingly. End your next final message with one line per question: `<id>: applies` if you \
994changed your work for it, `<id>: skip` if it does not apply.\n"
995 );
996 numbered(s, selected)
997}
998
999fn numbered(mut s: String, selected: &[&Challenge]) -> String {
1001 let groups = disagreements(selected);
1003 let mut n = 0;
1004 let mut shown: HashSet<usize> = HashSet::new();
1005 for (i, c) in selected.iter().enumerate() {
1006 match groups.iter().position(|g| g.contains(&i)) {
1007 None => {
1008 n += 1;
1009 s.push_str(&format!("\n{n}. [{}] {}", c.id, c.question));
1010 if !c.overrides.trim().is_empty() {
1011 s.push_str(&format!(" (This team rule replaces: {})", c.overrides.trim()));
1012 }
1013 }
1014 Some(g) if shown.insert(g) => {
1015 n += 1;
1016 let members: Vec<&Challenge> = groups[g].iter().map(|&j| selected[j]).collect();
1017 let ids: Vec<&str> = members.iter().map(|c| c.id.as_str()).collect();
1018 s.push_str(&format!("\n{n}. [{}] {}", ids.join(", "), disagreement(&members)));
1019 }
1020 Some(_) => {}
1021 }
1022 }
1023 s
1024}
1025
1026pub fn disagreement_ids(selected: &[&Challenge]) -> Vec<Vec<String>> {
1028 disagreements(selected).into_iter().map(|g| g.into_iter().map(|i| selected[i].id.clone()).collect()).collect()
1029}
1030
1031pub fn disagreements(selected: &[&Challenge]) -> Vec<Vec<usize>> {
1035 let mut pairs = crate::queen::conflict_pairs(selected);
1036 for (i, a) in selected.iter().enumerate() {
1037 for (j, b) in selected.iter().enumerate().skip(i + 1) {
1038 if a.supersedes.contains(&b.id) || b.supersedes.contains(&a.id) {
1039 pairs.push((i, j));
1040 }
1041 }
1042 }
1043 fn root(g: &mut [usize], mut i: usize) -> usize {
1045 while g[i] != i {
1046 g[i] = g[g[i]];
1047 i = g[i];
1048 }
1049 i
1050 }
1051 let mut parent: Vec<usize> = (0..selected.len()).collect();
1052 for (a, b) in pairs {
1053 let (ra, rb) = (root(&mut parent, a), root(&mut parent, b));
1054 parent[ra.max(rb)] = ra.min(rb);
1055 }
1056 let mut groups: Vec<(usize, Vec<usize>)> = Vec::new();
1057 for i in 0..selected.len() {
1058 let r = root(&mut parent, i);
1059 match groups.iter_mut().find(|(x, _)| *x == r) {
1060 Some((_, g)) => g.push(i),
1061 None => groups.push((r, vec![i])),
1062 }
1063 }
1064 groups.into_iter().map(|(_, g)| g).filter(|g| g.len() > 1).collect()
1065}
1066
1067pub fn disagreement(members: &[&Challenge]) -> String {
1070 let mut m: Vec<&Challenge> = members.to_vec();
1071 m.sort_by(|a, b| b.trust.total_cmp(&a.trust).then(a.id.cmp(&b.id)));
1072 let area = |c: &Challenge| trust::area(&c.area);
1073 let first = m.first().map(|c| area(c)).unwrap_or(trust::OTHER);
1074 let sides: Vec<String> = m
1075 .iter()
1076 .enumerate()
1077 .map(|(k, c)| {
1078 let who = match c.engineers.is_empty() {
1079 true => format!("lesson {}", c.id),
1080 false => c.engineers.join(" and "),
1081 };
1082 let says = if c.consequence.trim().is_empty() { c.question.trim() } else { c.consequence.trim() };
1083 let trust = match (k, area(c) == first) {
1084 (0, _) => format!("trust {:.2} in {first}", c.trust),
1085 (_, true) => format!("{:.2}", c.trust),
1086 (_, false) => format!("{:.2} in {}", c.trust, area(c)),
1087 };
1088 format!("{who} ({trust}) says \"{says}\"")
1089 })
1090 .collect();
1091 let situation = m.iter().find_map(|c| c.situations.first()).map(|s| format!(" ({})", s.trim())).unwrap_or_default();
1092 format!(
1093 "Engineers disagree here{situation}: {} — weigh the evidence (code, spec, tests) before following either; these are claims, not facts.",
1094 sides.join("; ")
1095 )
1096}
1097
1098pub const GENERIC: &str = "Before you finish, critically review your work as a demanding senior engineer would. \
1099Check: concurrency soundness (races, check-then-act, locking necessity and lock-free alternatives), reuse of what already \
1100exists in the codebase instead of adding new mechanisms, conformance with specs and documented contracts, whether you fixed \
1101the root cause, complexity (time/space), and whether the solution is minimal. Revise your work accordingly. If this was a \
1102plan, your final message must again be the complete revised plan.";
1103
1104#[cfg(test)]
1105mod tests {
1106 use super::*;
1107
1108 fn ch(id: &str, q: &str) -> Challenge {
1109 Challenge { id: id.into(), origin_ms: 1, question: q.into(), ..Default::default() }
1110 }
1111
1112 #[test]
1113 fn graded_blocks_only_for_a_three_then_asks_threes_twos_and_ones_up_to_the_cap() {
1114 let ls: Vec<Challenge> = (0..14).map(|i| ch(&format!("L{i}"), "q")).collect();
1115 let ids = |v: &[&Challenge]| v.iter().map(|c| c.id.clone()).collect::<Vec<_>>();
1116 let soft: Vec<(&Challenge, u8)> = ls.iter().enumerate().map(|(i, c)| (c, if i % 2 == 0 { 1 } else { 2 })).collect();
1118 let (ask, free) = graded(&soft);
1119 assert!(ask.is_empty());
1120 assert_eq!(ids(&free), ["L1", "L3", "L5", "L7", "L9", "L11", "L13", "L0", "L2", "L4"]);
1121 let mixed = vec![(&ls[0], 1), (&ls[1], 2), (&ls[2], 3), (&ls[3], 1), (&ls[4], 2)];
1123 let (ask, free) = graded(&mixed);
1124 assert_eq!((ids(&ask), free.len()), (vec!["L2".to_string(), "L1".into(), "L4".into(), "L0".into(), "L3".into()], 0));
1125 let full: Vec<(&Challenge, u8)> = ls
1127 .iter()
1128 .enumerate()
1129 .map(|(i, c)| {
1130 (
1131 c,
1132 match i {
1133 0 => 3,
1134 1..=9 => 2,
1135 _ => 1,
1136 },
1137 )
1138 })
1139 .collect();
1140 assert_eq!(ids(&graded(&full).0), ids(&ls[..10].iter().collect::<Vec<_>>()));
1141 let two_threes = vec![(&ls[0], 2), (&ls[1], 3), (&ls[2], 1), (&ls[3], 3)];
1143 let (ask, free) = graded_capped(&two_threes, Some(1));
1144 assert_eq!((ids(&ask), free.len()), (vec!["L1".to_string()], 0));
1145 assert_eq!(ids(&graded_capped(&two_threes, Some(5)).0), ["L1", "L3"]);
1146 let (ask, free) = graded_capped(&soft, Some(1));
1147 assert!(ask.is_empty() && free.len() == CAP);
1148 let (ask, free) = graded(&[]);
1149 assert!(ask.is_empty() && free.is_empty());
1150 }
1151
1152 #[test]
1153 fn outcome_weight_reorders_the_shortlist() {
1154 use crate::outcome::{ASKED, CORRECTED, IGNORED, NOT_CORRECTED, Outcome, append, path_beside};
1155 let dir = std::env::temp_dir().join(format!("borg-conscience-{}", std::process::id()));
1156 let _ = std::fs::remove_dir_all(&dir);
1157 let lessons = dir.join("lessons.jsonl");
1158 std::fs::create_dir_all(&dir).unwrap();
1159 let line =
1161 |id: &str, ms: i64| format!("{}\n", serde_json::json!({"id": id, "origin_ms": ms, "question": "Is the check-then-act on the shared flag atomic?"}));
1162 std::fs::write(&lessons, line("specific", 1) + &line("generic", 2)).unwrap();
1163 let plan = "check-then-act on the shared closed flag";
1164 let ids = |cs: &[Challenge]| select(cs, plan, 2).iter().map(|c| c.id.clone()).collect::<Vec<_>>();
1165 assert_eq!(ids(&load(&lessons, None)), ["generic", "specific"]);
1166 let o = |kind: &str, s: &str, ms, l: &str| Outcome {
1170 kind: kind.into(),
1171 session: s.into(),
1172 lessons: vec![l.into()],
1173 ms,
1174 repo: "r".into(),
1175 ..Default::default()
1176 };
1177 for x in [
1178 o(ASKED, "s1", 1, "generic"),
1179 o(CORRECTED, "s1", 2, "generic"),
1180 o(ASKED, "s2", 3, "generic"),
1181 o(IGNORED, "s2", 4, "generic"),
1182 o(NOT_CORRECTED, "s2", 4, "generic"),
1183 ] {
1184 append(&path_beside(&lessons), &x).unwrap();
1185 }
1186 let cs = load(&lessons, None);
1187 let w = |cs: &[Challenge], id: &str| cs.iter().find(|c| c.id == id).unwrap().weight;
1188 let sq = |age: f64| crate::memory::squash(age.powf(-0.5).ln());
1189 assert!((w(&cs, "generic") - sq(2.0) * 0.7).abs() < 1e-12 && (w(&cs, "specific") - sq(3.0)).abs() < 1e-12);
1190 assert_eq!(ids(&cs), ["specific", "generic"]);
1191 for x in [
1193 o(ASKED, "s3", 5, "generic"),
1194 o(crate::outcome::CHANGED, "s3", 6, "generic"),
1195 o(ASKED, "s4", 7, "generic"),
1196 o(crate::outcome::CHANGED, "s4", 8, "generic"),
1197 ] {
1198 append(&path_beside(&lessons), &x).unwrap();
1199 }
1200 let cs = load(&lessons, None);
1201 assert!(w(&cs, "generic") > w(&cs, "specific"), "{} {}", w(&cs, "generic"), w(&cs, "specific"));
1202 assert_eq!(ids(&cs), ["generic", "specific"]);
1203 let _ = std::fs::remove_dir_all(dir);
1204 }
1205
1206 fn store(name: &str, lines: &[serde_json::Value], outcomes: &[(&str, &str, &str)]) -> std::path::PathBuf {
1207 let dir = std::env::temp_dir().join(format!("borg-conscience-{name}-{}", std::process::id()));
1208 let _ = std::fs::remove_dir_all(&dir);
1209 std::fs::create_dir_all(&dir).unwrap();
1210 let text: String = lines.iter().map(|l| format!("{l}\n")).collect();
1211 std::fs::write(dir.join("lessons.jsonl"), text).unwrap();
1212 for (kind, session, id) in outcomes {
1213 let o = crate::outcome::Outcome {
1214 kind: kind.to_string(),
1215 session: session.to_string(),
1216 lessons: vec![id.to_string()],
1217 ms: 1,
1218 repo: "r".into(),
1219 ..Default::default()
1220 };
1221 crate::outcome::append(&dir.join("outcomes.jsonl"), &o).unwrap();
1222 }
1223 dir
1224 }
1225
1226 #[test]
1229 fn the_unweighted_store_has_the_personal_lessons_over_the_teams() {
1230 use serde_json::json;
1231 let l = |id: &str, q: &str| json!({"id": id, "origin_ms": 1, "question": q});
1232 let dir = store("unweighted-personal", &[l("T", "team words"), l("P", "tombstone")], &[]);
1233 let lessons = dir.join("lessons.jsonl");
1234 std::fs::write(dir.join(crate::reach::PERSONAL_FILE), format!("{}\n{}\n", l("P", "my own words"), l("M", "mine alone"))).unwrap();
1235 let mut got: Vec<(String, String)> = load_store_unweighted(&lessons, None).into_iter().map(|c| (c.id, c.question)).collect();
1236 got.sort();
1237 assert_eq!(got, [("M".into(), "mine alone".into()), ("P".into(), "my own words".into()), ("T".into(), "team words".into())]);
1238 assert_eq!(load_unweighted(&lessons, None).len(), 2, "the team file alone");
1239 let _ = std::fs::remove_dir_all(dir);
1240 }
1241
1242 #[test]
1243 fn author_trust_in_the_area_reorders_the_shortlist() {
1244 use crate::outcome::{CORRECTED, NOT_CORRECTED};
1245 use serde_json::json;
1246 let q = "Is the check-then-act on the shared flag atomic?";
1247 let lines = [
1251 json!({"id": "strong", "origin_ms": 1, "question": q, "area": "concurrency", "engineers": ["ann@x"]}),
1252 json!({"id": "weak", "origin_ms": 2, "question": q, "area": "concurrency", "engineers": ["bob@x"]}),
1253 json!({"id": "ann-old", "origin_ms": 0, "question": "unrelated", "area": "concurrency", "engineers": ["ann@x"]}),
1254 json!({"id": "bob-old", "origin_ms": 0, "question": "unrelated", "area": "concurrency", "engineers": ["bob@x"]}),
1255 json!({"id": "bob-sec", "origin_ms": 0, "question": "unrelated", "area": "security", "engineers": ["bob@x"]}),
1256 ];
1257 let plan = "check-then-act on the shared closed flag";
1258 let ids = |cs: &[Challenge]| select(cs, plan, 2).iter().map(|c| c.id.clone()).collect::<Vec<_>>();
1259 let neutral = store("trust-neutral", &lines, &[]);
1260 let cs = load(&neutral.join("lessons.jsonl"), None);
1261 assert_eq!(ids(&cs), ["weak", "strong"]);
1262 assert!(cs.iter().all(|c| c.trust == trust::NEUTRAL && c.weight == 1.0));
1263 let raw = bm25(&cs.iter().map(|c| tokens(&c.question)).collect::<Vec<_>>(), plan)[0];
1265 assert!((rank(&cs, plan)[1].0 - raw).abs() < 1e-12);
1266 let outs = [(CORRECTED, "s1", "bob-old"), (CORRECTED, "s2", "bob-old"), (NOT_CORRECTED, "s1", "ann-old"), (NOT_CORRECTED, "s3", "bob-sec")];
1268 let earned = store("trust-earned", &lines, &outs);
1269 let cs = load(&earned.join("lessons.jsonl"), None);
1270 let t = |id: &str| cs.iter().find(|c| c.id == id).unwrap().trust;
1271 assert_eq!((t("strong"), t("weak")), (2.0 / 3.0, 0.25));
1272 assert_eq!(ids(&cs), ["strong", "weak"]);
1273 for d in [neutral, earned] {
1274 let _ = std::fs::remove_dir_all(d);
1275 }
1276 }
1277
1278 #[test]
1279 fn a_superseded_lesson_is_not_asked_its_successor_is() {
1280 use serde_json::json;
1281 let lines = [
1282 json!({"id": "lock", "origin_ms": 1, "question": "Is the closed flag guarded by the delivery ReentrantLock?", "engineers": ["ann@x"]}),
1283 json!({"id": "atomic", "origin_ms": 2, "question": "Would a CAS suffice?", "engineers": ["ann@x"], "supersedes": ["lock"]}),
1285 ];
1286 let dir = store("superseded", &lines, &[]);
1287 let cs = load(&dir.join("lessons.jsonl"), None);
1288 let plan = "guard the closed flag with the delivery ReentrantLock";
1289 let sel = select(&cs, plan, 5);
1290 assert_eq!(sel.iter().map(|c| c.id.as_str()).collect::<Vec<_>>(), ["atomic"], "the successor inherits the relevance");
1291 assert!(!render(&sel).contains("ReentrantLock"));
1292 assert_eq!(cs.iter().find(|c| c.id == "lock").unwrap().trust, NEUTRAL_REVERSED);
1293 let _ = std::fs::remove_dir_all(dir);
1294 }
1295
1296 const NEUTRAL_REVERSED: f64 = 0.25;
1299
1300 #[test]
1301 fn disagreeing_lessons_render_as_one_conflict_with_trust() {
1302 let mk = |id: &str, eng: &str, area: &str, consequence: &str, t: f64| Challenge {
1303 id: id.into(),
1304 origin_ms: 1,
1305 question: format!("question of {id}"),
1306 situations: vec!["check-then-act on a shared mutable flag across threads".into()],
1307 consequence: consequence.into(),
1308 area: area.into(),
1309 engineers: vec![eng.into()],
1310 trust: t,
1311 ..Default::default()
1312 };
1313 let a = mk("A", "bob@x", "concurrency", "guard it with the existing ReentrantLock", 0.4);
1314 let b = mk("B", "ann@x", "concurrency", "use a lock-free compareAndSet instead", 0.8);
1315 let other = Challenge { id: "C".into(), question: "Is the spec cited?".into(), ..Default::default() };
1316 let text = render(&[&a, &other, &b]);
1317 assert!(
1318 text.contains("\n1. [A, B] Engineers disagree here (check-then-act on a shared mutable flag across threads): ann@x (trust 0.80 in concurrency) says \"use a lock-free compareAndSet instead\"; bob@x (0.40) says \"guard it with the existing ReentrantLock\" — weigh the evidence"),
1319 "{text}"
1320 );
1321 assert!(text.contains("\n2. [C] Is the spec cited?") && !text.contains("question of"), "{text}");
1322 let x = Challenge { id: "X".into(), question: "Lock?".into(), supersedes: vec!["Y".into()], engineers: vec!["ann@x".into()], ..Default::default() };
1324 let y = Challenge { id: "Y".into(), question: "Atomic?".into(), supersedes: vec!["X".into()], area: "scope".into(), ..Default::default() };
1325 let text = render(&[&x, &y]);
1326 assert!(
1327 text.contains("1. [X, Y] Engineers disagree here: ann@x (trust 0.50 in other) says \"Lock?\"; lesson Y (0.50 in scope) says \"Atomic?\""),
1328 "{text}"
1329 );
1330 assert!(!text.contains("\n2."));
1331 let c = mk("C2", "cy@x", "concurrency", "guard it with the existing ReentrantLock", 0.5);
1333 assert!(render(&[&a, &c]).contains("\n2. [C2] question of C2"));
1334 }
1335
1336 #[test]
1337 fn a_less_trusted_contradiction_is_shown_as_a_disagreement_not_hidden() {
1338 use crate::outcome::{CORRECTED, NOT_CORRECTED};
1339 use serde_json::json;
1340 let q = "Is the closed flag guarded by the delivery ReentrantLock?";
1341 let lines = [
1342 json!({"id": "lock", "origin_ms": 1, "question": q, "consequence": "guard it with the existing ReentrantLock",
1343 "situations": ["check-then-act on the closed flag"], "area": "concurrency", "engineers": ["ann@x"]}),
1344 json!({"id": "cas", "origin_ms": 2, "question": "Would a CAS on the closed flag ReentrantLock path suffice?", "consequence": "use compareAndSet",
1345 "situations": ["check-then-act on the closed flag"], "area": "concurrency", "engineers": ["bob@x"], "supersedes": ["lock"]}),
1346 json!({"id": "ann-old", "origin_ms": 0, "question": "unrelated", "area": "concurrency", "engineers": ["ann@x"]}),
1347 json!({"id": "bob-old", "origin_ms": 0, "question": "unrelated", "area": "concurrency", "engineers": ["bob@x"]}),
1348 ];
1349 let dir = store("weak-supersedes", &lines, &[(NOT_CORRECTED, "s1", "ann-old"), (CORRECTED, "s2", "bob-old")]);
1350 let cs = load(&dir.join("lessons.jsonl"), None);
1351 let plan = "guard the closed flag with the delivery ReentrantLock";
1352 let sel = select(&cs, plan, 5);
1353 let ids: Vec<&str> = sel.iter().map(|c| c.id.as_str()).collect();
1354 assert!(ids.contains(&"lock") && ids.contains(&"cas"), "{ids:?}");
1355 let text = render(&sel);
1356 assert!(text.contains("Engineers disagree here") && text.contains("ann@x (trust 0.67 in concurrency)"), "{text}");
1357 assert_eq!(disagreement_ids(&sel), [vec!["lock".to_string(), "cas".to_string()]]);
1358 let _ = std::fs::remove_dir_all(dir);
1359 }
1360
1361 #[test]
1362 fn a_scoped_lesson_is_asked_only_where_its_scope_is_named() {
1363 let drain = Challenge {
1364 id: "drain".into(),
1365 question: "Is each per-turn place drained by an explicit consumer instead of a new reset arc?".into(),
1366 scope: vec!["Orion".into()],
1367 overrides: "upstream petri-net-design seeds a budget with a reset arc on the starting transition".into(),
1368 ..ch("drain", "")
1369 };
1370 let other = Challenge { scope: vec!["search lab".into()], ..ch("lab", "Is the reset arc per-turn place budget pinned?") };
1371 let open = ch("open", "Does every per-turn token have a consumer?");
1372 let cs = vec![drain, other, open];
1373 let ids = |ctx: &str| select(&cs, ctx, 5).iter().map(|c| c.id.clone()).collect::<Vec<_>>();
1374 assert_eq!(ids("PressNet: reset arc on the per-turn reprint budget place"), ["open"]);
1376 let k1 = ids("Read /w/orion-text/src/FlowConfig.java; add a per-turn hint place with a reset arc");
1378 assert!(k1.contains(&"drain".to_string()) && !k1.contains(&"lab".to_string()), "{k1:?}");
1379 assert!(ids("the search-lab reset arc per-turn budget").contains(&"lab".to_string()));
1381 assert!(ids("SearchLab: reset arc per-turn budget").contains(&"lab".to_string()));
1382 assert!(!ids("search the lab: reset arc per-turn budget").contains(&"lab".to_string()));
1383 let text = render(&[&cs[0]]);
1384 assert!(text.contains("instead of a new reset arc? (This team rule replaces: upstream petri-net-design seeds"), "{text}");
1385 let succ = Challenge { supersedes: vec!["drain".into()], ..ch("succ", "Unrelated wording") };
1387 let cs2 = vec![cs[0].clone(), succ];
1388 assert!(select(&cs2, "per-turn place reset arc consumer", 5).is_empty());
1389 }
1390
1391 #[test]
1392 fn scope_names_match_as_phrases_or_identifiers_never_as_scattered_words() {
1393 assert_eq!(
1394 segments("HTTPClientFactory cart-link-v2 harbor_orion SR3Review"),
1395 ["http", "client", "factory", "cart", "link", "v2", "harbor", "orion", "sr3", "review"]
1396 );
1397 let k2 = Names::of(
1399 "Read /w/press/src/PressNet.java. The jam transition's guard now checks the counter; \
1400 the reprint transition's output arc goes to Ready; reset arc removed. A button links here.",
1401 );
1402 for name in ["Answer Guard", "pay-button", "cart-link-v2", "Orion", "harbor_orion", "ticket-triage-agent"] {
1403 assert!(!k2.contains(name), "{name} must not match k2");
1404 }
1405 assert!(Names::of("wire the AnswerGuard bean").contains("Answer Guard"));
1406 assert!(Names::of("src/quality-acceptance-test/java").contains("quality-acceptance-test"));
1407 assert!(Names::of("the pay-button component").contains("pay-button"));
1408 assert!(!Names::of("the button").contains("pay-button"), "a short part is never dropped");
1409 assert!(Names::of("/w/orion-text/src/OrionConfig.java").contains("Orion"));
1410 assert!(!Names::of_all(["answer", "guard"]).contains("Answer Guard"), "never across two texts");
1411 assert!(!Names::of("").contains("Orion") && !Names::of("x").contains("--"));
1412 }
1413
1414 #[test]
1418 fn k2_stop_context_asks_no_orion_lesson() {
1419 let guard = Challenge {
1420 scope: vec!["Orion".into(), "harbor_orion".into(), "LiveCheckService".into(), "QuickReplySubnet".into(), "Answer Guard".into()],
1421 ..ch(
1422 "I0a1b2c3d4e5f",
1423 "Have you listed every LLM call whose output reaches the customer, traced through the Petri net from the \
1424guard's output place to each send action, and stated per surface whether the Answer Guard sees it?",
1425 )
1426 };
1427 let reset = Challenge {
1428 scope: vec!["Orion".into(), "pay-button".into(), "cart-link-v2".into()],
1429 ..ch("reset", "Is per-turn state drained instead of cleared by a reset arc on the transition?")
1430 };
1431 let generic = ch("generic", "Does the demo show the changed behaviour for every job, not only the first?");
1432 let cs = vec![guard, reset, generic];
1433 let w = "/tmp/e3live/0a1b2c3d/work/press-queue";
1434 let k2 = format!(
1435 "Operators say only the first jammed job of a shift gets reprinted; every jam after that goes straight to failed. \
1436Every job should get one reprint after a jam before we mark it failed.\n{w}/src/printshop/press/PressNet.java\n{w}/src/printshop/press/ShiftDemo.java\n\
1437The reprint budget was one shift-wide token. Now the jam transition's guard checks the job's reprint count, the reprint transition's \
1438output arc puts the job back into Queued, and the shift-start reset arc is removed. A button in the link view shows it."
1439 );
1440 let ids: Vec<String> = select(&cs, &k2, 5).iter().map(|c| c.id.clone()).collect();
1441 assert_eq!(ids, ["generic"]);
1442 assert!(select(&cs, &format!("{k2}\n/w/harbor_orion/src/AnswerGuard.java"), 5).iter().any(|c| c.id == "I0a1b2c3d4e5f"));
1444 }
1445
1446 #[test]
1447 fn early_delivers_only_scoped_lessons_on_a_named_topic_that_score_near_the_best() {
1448 let sc = |id: &str, scope: &[&str], q: &str| Challenge { scope: scope.iter().map(|s| s.to_string()).collect(), ..ch(id, q) };
1449 let cs = vec![
1450 sc("live", &["Beacon"], "Does the Beacon replay run its services on live hosts?"),
1451 sc("judge", &["Beacon", "search-lab"], "Is the replay judge exactly the named model?"),
1452 sc("weak", &["Beacon"], "Is the facet mapping evaluated at the relaxed tier?"),
1453 sc("drain", &["Orion"], "Is the replay place drained instead of reset?"),
1454 ch("generic", "Does the replay plan name a control arm and a live judge?"),
1455 ];
1456 let ctx = "Set up the Beacon replay on live hosts with the named judge model";
1457 assert_eq!(named_scopes(&cs, ctx), ["Beacon"]);
1458 assert_eq!(named_scopes(&cs, "/w/search-lab/x and /w/orion-text/y"), ["search-lab", "Orion"]);
1459 let topics = vec!["Beacon".to_string()];
1460 let ids = |done: &HashSet<&str>| early(&cs, ctx, &topics, done).iter().map(|c| c.id.clone()).collect::<Vec<_>>();
1461 assert_eq!(ids(&HashSet::new()), ["judge", "live"]);
1463 assert_eq!(ids(&HashSet::from(["live"])), ["judge"]);
1464 assert_eq!(ids(&HashSet::from(["judge"])), ["live"]);
1465 assert!(early(&cs, ctx, &[], &HashSet::new()).is_empty());
1466 let (text, shown) = render_early(&early(&cs, ctx, &topics, &HashSet::new()), &topics).unwrap();
1467 assert_eq!(shown, 2);
1468 assert!(text.starts_with("[borg] Peer lessons for Beacon") && text.ends_with("\n- Does the Beacon replay run its services on live hosts?"), "{text}");
1469 assert!(render_early(&[], &topics).is_none());
1470 let long: Vec<Challenge> = (0..9).map(|i| Challenge { consequence: "c".repeat(900), ..sc(&format!("l{i}"), &["Beacon"], &"q ".repeat(400)) }).collect();
1472 let (text, shown) = render_early(&long.iter().collect::<Vec<_>>(), &topics).unwrap();
1473 assert!(text.chars().count() <= EARLY_CHARS && (1..9).contains(&shown), "{shown} {}", text.len());
1474 }
1475
1476 #[test]
1477 fn the_stop_blocks_only_for_a_strong_lesson_not_done_and_then_asks_generously() {
1478 let sc = |id: &str| Challenge { scope: vec!["Beacon".into()], ..ch(id, "") };
1479 let (s1, s2, s3, s4) = (sc("s1"), sc("s2"), sc("s3"), sc("s4"));
1480 let gs: Vec<Challenge> = (0..6).map(|i| ch(&format!("g{i}"), "")).collect();
1481 let ids = |v: Vec<&Challenge>| v.iter().map(|c| c.id.clone()).collect::<Vec<_>>();
1482 let none = HashSet::new();
1483 let ranked: Vec<(f64, &Challenge)> =
1485 vec![(10.0, &gs[0]), (9.0, &s1), (8.0, &gs[1]), (7.0, &gs[2]), (6.0, &s2), (5.5, &gs[3]), (3.0, &s3), (2.9, &s4), (2.0, &gs[4])];
1486 assert_eq!(ids(budget(&ranked, &none)), ["s1", "s2", "s3", "g0", "g1", "g2"]);
1487 let generic: Vec<(f64, &Challenge)> = gs.iter().map(|g| (5.0, g)).collect();
1489 assert_eq!(budget(&generic, &none).len(), GENERIC_CAP);
1490 let many: Vec<Challenge> = (0..14).map(|i| sc(&format!("a{i}"))).collect();
1492 let ranked_many: Vec<(f64, &Challenge)> = many.iter().map(|c| (5.0, c)).collect();
1493 assert_eq!(budget(&ranked_many, &none).len(), CAP);
1494 assert!(budget(&[], &none).is_empty(), "nothing scores, nothing is asked");
1495 assert!(budget(&ranked, &HashSet::from(["g0", "s1", "g1"])).is_empty());
1498 assert_eq!(ids(budget(&ranked, &HashSet::from(["g0", "g1"]))), ["s1", "s2", "s3", "g2", "g3"]);
1499 assert!(widen(&ranked, &[], &none).is_empty(), "the model abstained: no block");
1501 let w = ids(widen(&ranked, &[&gs[4]], &HashSet::from(["g0"])));
1502 assert_eq!(w, ["s1", "s2", "s3", "g4", "g1", "g2"], "the model's pick leads, below the bar or not");
1503 }
1504
1505 #[test]
1506 fn a_pool_that_fits_has_propensity_one() {
1507 let sc = |id: &str| Challenge { scope: vec!["Beacon".into()], belief: (1.0, 40.0), ..ch(id, "") };
1508 let ls: Vec<Challenge> = (0..7).map(|i| sc(&format!("s{i}"))).chain((0..3).map(|i| ch(&format!("g{i}"), ""))).collect();
1509 let pool: Vec<(f64, &Challenge)> = ls.iter().enumerate().map(|(i, c)| (10.0 - i as f64, c)).collect();
1510 let p = propensity(&pool, "s");
1511 assert_eq!(p.len(), CAP);
1512 assert!(p.values().all(|&x| x == 1.0), "{p:?}");
1513 }
1514
1515 #[test]
1516 fn propensities_fill_the_slots() {
1517 let ls: Vec<Challenge> = (0..14).map(|i| Challenge { scope: vec!["Beacon".into()], ..ch(&format!("a{i}"), "") }).collect();
1518 let pool: Vec<(f64, &Challenge)> = ls.iter().map(|c| (5.0, c)).collect();
1519 let p = propensity(&pool, "s");
1520 let sum: f64 = p.values().sum();
1521 assert!((sum - CAP as f64).abs() < 0.05, "{sum}");
1522 assert!(p.values().all(|&x| x > 0.0 && x < 1.0), "{p:?}");
1523 let gs: Vec<Challenge> = (0..6).map(|i| ch(&format!("g{i}"), "")).collect();
1525 let pool: Vec<(f64, &Challenge)> = gs.iter().map(|c| (5.0, c)).collect();
1526 assert!((propensity(&pool, "s").values().sum::<f64>() - GENERIC_CAP as f64).abs() < 0.05);
1527 }
1528
1529 #[test]
1530 fn a_constraint_never_reaches_further_than_its_lesson() {
1531 use serde_json::json;
1532 let lessons = [
1533 json!({"id": "Lrepo", "origin_ms": 1, "question": "q", "repo": "app", "reach": "repo", "scope": ["Orion"]}),
1534 json!({"id": "Lgone", "origin_ms": 1, "question": "q", "repo": "app", "reach": "personal"}),
1535 json!({"id": "Lteam", "origin_ms": 1, "question": "q", "repo": "app"}),
1536 ];
1537 let dir = store("constraint-reach", &lessons, &[]);
1538 let line = |id: &str, lesson: &str, extra: serde_json::Value| {
1539 let mut v = json!({"id": id, "text": format!("rule {id}"), "lesson": lesson, "by": "lead@x", "ms": 5});
1540 v.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone());
1541 format!("{v}\n")
1542 };
1543 let text = [
1544 line("c1", "Lrepo", json!({})),
1545 line("c2", "Lgone", json!({"reach": ""})),
1546 line("c3", "Lteam", json!({"reach": "repo", "repo": "app"})),
1547 line("c4", "Lmissing", json!({"repo": "app", "scope": ["Beacon"]})),
1548 line("c5", "Lmissing", json!({})),
1549 ]
1550 .concat();
1551 std::fs::write(crate::veto::constraints_path(&dir), text).unwrap();
1552 let all = load_with(&dir.join("lessons.jsonl"), None, &Aliases::default());
1553 let c = |id: &str| all.iter().find(|c| c.id == id && c.standing());
1554 let of = |id: &str| c(id).map(|c| (c.reach.as_str(), c.repo.as_str(), c.scope.clone()));
1555 assert_eq!(of("c1"), Some(("repo", "app", vec!["Orion".to_string()])), "a repo convention's constraint stays in its repo");
1556 assert_eq!(of("c2").map(|x| x.0), Some("personal"), "a withdrawn lesson takes its constraint along");
1557 assert_eq!(of("c3").map(|x| x.0), Some("repo"), "the line's narrower reach holds over a team lesson");
1558 assert_eq!(of("c4"), Some(("repo", "app", vec!["Beacon".to_string()])), "an unknown lesson: the line's scope, bound to its repo");
1559 assert!(c("c5").is_none(), "an unknown lesson and no repo: never delivered everywhere");
1560 let here = ["other".to_string()];
1561 assert!(["c1", "c2", "c3", "c4"].iter().all(|id| !crate::reach::deliverable(c(id).unwrap(), &here)));
1562 let _ = std::fs::remove_dir_all(dir);
1563 }
1564
1565 #[test]
1566 fn early_propensities_fill_its_slots_and_what_it_shows() {
1567 let topics = vec!["Beacon".to_string()];
1568 let ls: Vec<Challenge> = (0..8).map(|i| Challenge { scope: vec!["Beacon".into()], ..ch(&format!("a{i}"), "Is it short?") }).collect();
1569 let pool: Vec<(f64, &Challenge)> = ls.iter().map(|c| (5.0, c)).collect();
1570 assert!(early_propensity(&pool[..EARLY_CAP], "s", &topics).values().all(|&x| x == 1.0), "the whole pool shows");
1571 let p = early_propensity(&pool, "s", &topics);
1572 let sum: f64 = p.values().sum();
1573 assert!((sum - EARLY_CAP as f64).abs() < 0.05, "{sum}");
1574 assert!(p.values().all(|&x| x > 0.0 && x < 1.0), "{p:?}");
1575 let long: Vec<Challenge> =
1577 (0..4).map(|i| Challenge { scope: vec!["Beacon".into()], consequence: "y".repeat(199), ..ch(&format!("l{i}"), &"x".repeat(299)) }).collect();
1578 let pool: Vec<(f64, &Challenge)> = long.iter().map(|c| (5.0, c)).collect();
1579 let shown = render_early(&pool.iter().map(|(_, c)| *c).collect::<Vec<_>>(), &topics).unwrap().1;
1580 let sum: f64 = early_propensity(&pool, "s", &topics).values().sum();
1581 assert!(shown < pool.len() && (sum - shown as f64).abs() < 1e-9, "{shown} {sum}");
1582 }
1583
1584 #[test]
1585 fn a_proven_lesson_outdraws_an_uncertain_one() {
1586 let sc = |id: &str, belief: (f64, f64)| Challenge { scope: vec!["Beacon".into()], belief, ..ch(id, "") };
1587 let mut ls: Vec<Challenge> = (0..10).map(|i| sc(&format!("a{i}"), (1.0, 1.0))).collect();
1588 ls.push(sc("proven", (40.0, 2.0)));
1589 ls.push(sc("harmful", (1.0, 40.0)));
1590 let pool: Vec<(f64, &Challenge)> = ls.iter().map(|c| (5.0, c)).collect();
1591 let p = propensity(&pool, "s");
1592 assert!(p["proven"] > p["harmful"], "{p:?}");
1593 }
1594
1595 #[test]
1598 fn the_raw_word_match_then_rank_raw_is_rank_scoped() {
1599 let mut cs = vec![
1600 ch("a", "websocket deadlock on reconnect"),
1601 ch("b", "cache invalidation after deploy"),
1602 Challenge { scope: vec!["Orion".into()], ..ch("c", "orion websocket buffer") },
1603 Challenge { dormant: true, ..ch("d", "deadlock in the websocket pool") },
1604 Challenge { supersedes: vec!["b".into()], source: "queen".into(), ..ch("e", "cache invalidation on deploy") },
1605 ];
1606 cs[0].weight = 0.4;
1607 cs[4].trust = 0.9;
1608 let (context, scope) = ("websocket deadlock cache deploy", "repo beacon");
1609 let raw = raw(&cs, context);
1610 assert_eq!(raw.len(), cs.len());
1611 let split: Vec<(f64, String)> = rank_raw(&cs, raw, scope).into_iter().map(|(s, c)| (s, c.id.clone())).collect();
1612 let whole: Vec<(f64, String)> = rank_scoped(&cs, context, scope).into_iter().map(|(s, c)| (s, c.id.clone())).collect();
1613 assert!(!whole.is_empty());
1614 assert_eq!(split, whole);
1615 }
1616
1617 #[test]
1618 fn propensity_is_deterministic() {
1619 let ls: Vec<Challenge> =
1620 (0..12).map(|i| Challenge { scope: vec!["Beacon".into()], belief: (1.0 + i as f64, 2.5), ..ch(&format!("a{i}"), "") }).collect();
1621 let pool: Vec<(f64, &Challenge)> = ls.iter().enumerate().map(|(i, c)| (5.0 + i as f64 / 10.0, c)).collect();
1622 assert_eq!(propensity(&pool, "s"), propensity(&pool, "s"));
1623 assert_ne!(propensity(&pool, "s"), propensity(&pool, "t"), "another session draws again");
1624 }
1625
1626 #[test]
1627 fn bm25_ranks_the_matching_question_first() {
1628 let cs = vec![
1629 ch("log", "Am I diagnosing from the full application log rather than a truncated debug trace?"),
1630 ch("toctou", "Is any check-then-act on an AtomicBoolean flag TOCTOU-safe across threads?"),
1631 ch("css", "Does the stylesheet follow the design tokens?"),
1632 ];
1633 let plan = "The relay checks closed.get() on the AtomicBoolean before deliver(); reader and drainer threads race.";
1634 let sel = select(&cs, plan, 2);
1635 assert_eq!(sel[0].id, "toctou");
1636 assert!(select(&cs, "zzz qqq", 3).is_empty());
1637 }
1638
1639 #[test]
1640 fn a_store_without_facets_ranks_exactly_as_bm25_over_the_questions() {
1641 let qs = ["Is the closed flag guarded by the delivery lock?", "Does the plan cite the spec?", "", "Is the flag lock-free?", "Is the flag lock-free?"];
1642 let docs: Vec<Vec<String>> = qs.iter().map(|q| tokens(q)).collect();
1643 let groups: Vec<Vec<Vec<String>>> = docs.iter().map(|d| vec![d.clone()]).collect();
1644 for query in ["the closed flag and its lock", "spec spec plan", "nothing here", ""] {
1645 let (a, b) = (bm25(&docs, query), bm25_groups(&groups, query, 0.5));
1646 assert!(a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-12), "{query}: {a:?} {b:?}");
1647 }
1648 let b = bm25_groups(&groups, "lock-free flag", 0.0);
1649 assert_eq!(b[3].to_bits(), b[4].to_bits(), "equal documents score bit-for-bit equal");
1650 }
1651
1652 #[test]
1653 fn a_facet_finds_its_lesson_from_another_angle_and_counts_once_for_idf() {
1654 let mut fill = ch("fill", "Is P@8 split into fill@8 and relevance among filled slots?");
1655 fill.facets = vec![
1656 "empty result slots counted as irrelevant products make precision look worse".into(),
1657 "Leere Trefferplätze werden als irrelevante Dokumente gezählt".into(),
1658 ];
1659 let other = ch("judge", "Is the replay judge exactly the named model?");
1660 let cs = vec![fill.clone(), other];
1661 let ctx = "precision dropped because the search returned fewer products; empty slots look irrelevant";
1663 assert_eq!(select(&cs, ctx, 1)[0].id, "fill");
1664 assert!(select(&cs, "Plätze leer, Dokumente gezählt", 1).iter().any(|c| c.id == "fill"), "German facet");
1665 let groups: Vec<Vec<Vec<String>>> = cs.iter().map(documents).collect();
1667 let one = bm25_groups(&groups, "irrelevant", 0.0)[0];
1668 let mut twice = fill.clone();
1669 twice.facets.push("irrelevant".into());
1670 let g2: Vec<Vec<Vec<String>>> = [twice, cs[1].clone()].iter().map(documents).collect();
1671 let s = bm25_groups(&g2, "irrelevant", 0.0)[0];
1672 assert!(s > one, "the one-word facet is the better document");
1673 let with_second = bm25_groups(&g2, "irrelevant", 0.5)[0];
1674 assert!(with_second > s && with_second < 2.0 * s, "max + 0.5 x second");
1675 let x = Challenge { facets: vec!["irrelevant".into()], ..ch("x", "irrelevant") };
1678 let groups = [documents(&x), vec![tokens("other words entirely")]];
1679 let expect = 2f64.ln() * 2.2 / (1.0 + 1.2 * (0.25 + 0.75 * 0.6));
1680 assert!((bm25_groups(&groups, "irrelevant", 0.0)[0] - expect).abs() < 1e-12);
1681 }
1682}