M5c-2 plan: task 06, Honcho's results as recall items

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 18:48:35 -07:00
co-authored by Claude Opus 5.5
parent 9a4f0c5875
commit c2c8d47f37
4 changed files with 720 additions and 0 deletions
@@ -0,0 +1,151 @@
//! Honcho's search results as `recall` items (M5c spec, section 8): kept only when they share a
//! word with the query, one of each text per source, neutralised, on one line and capped like a
//! local snippet, labelled by their source, and shown after the local index's hits up to `k`.
//! Pure: no I/O.
use bxcore::DataClass;
use bxcore::sanitize::neutralise;
use roncho::{Conclusion, Message};
use crate::honcho::read::Label;
use crate::index::class_name;
use crate::recall::{SNIPPET, when};
/// One Honcho hit as `recall` shows it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Item {
/// `memory` or `memory-tainted`: its source's tag.
pub tag: &'static str,
/// Where it came from, after the tag in its header.
pub from: String,
pub class: DataClass,
pub untrusted: bool,
/// Neutralised, on one line, at most `SNIPPET` characters.
pub text: String,
}
impl Item {
/// The item as the model reads it: a header in the local format, `[n] {tag} · {from} ·
/// {class}`, then its text on one line indented by two spaces, so that no text can begin a
/// line the way a header does.
pub fn render(&self, n: usize) -> String {
format!(
"[{n}] {} · {} · {}\n {}",
self.tag,
self.from,
class_name(self.class),
self.text
)
}
}
/// The words of `text`, lowercase: every run of alphanumeric characters (`char::is_alphanumeric`),
/// as the local index's tokenizer splits them.
pub fn words(text: &str) -> Vec<String> {
text.split(|c: char| !c.is_alphanumeric())
.filter(|w| !w.is_empty())
.map(str::to_lowercase)
.collect()
}
/// Whether `text` holds one of `query` (the query's words) as a whole word.
pub fn relevant(query: &[String], text: &str) -> bool {
words(text).iter().any(|w| query.contains(w))
}
/// Text from Honcho as an item holds it: neutralised, every control character (a line break
/// among them) and every Unicode line or paragraph separator a space, then cut to its first
/// `SNIPPET` characters, as a local snippet is cut.
pub fn clean(text: &str) -> String {
neutralise(text)
.chars()
.map(|c| {
if c.is_control() || c == '\u{2028}' || c == '\u{2029}' {
' '
} else {
c
}
})
.take(SNIPPET)
.collect()
}
/// `{name}: {peer} said, {when}` for a message.
fn message_from(label: &Label, message: &Message) -> String {
format!(
"{}: {} said, {}",
label.name,
clean(&message.peer_id),
when(&message.created_at)
)
}
/// `{name}: a conclusion about {peer}, {when}` for a conclusion.
fn conclusion_from(label: &Label, conclusion: &Conclusion) -> String {
format!(
"{}: a conclusion about {}, {}",
label.name,
clean(&conclusion.observed_id),
when(&conclusion.created_at)
)
}
/// One source's results as items, in Honcho's order: the messages, then the conclusions about
/// the agent's peer, then those about the owner's (both empty for the fleet). A result is kept
/// only when it holds a word of `query`, and only once: one whose cleaned text equals an earlier
/// kept one's is dropped.
pub fn items(
label: &Label,
query: &str,
messages: &[Message],
agent: &[Conclusion],
owner: &[Conclusion],
) -> Vec<Item> {
// 1. `let query = words(query);`: the query's words.
// 2. The results in order: each message as (its `content`, `message_from(label, m)`), then each
// conclusion of `agent`, then each of `owner`, as (its `content`, `conclusion_from(label,
// c)`).
// 3. For each (content, from): skip it unless `relevant(&query, content)`; its text is
// `clean(content)`; skip it when an item already kept has that same text; otherwise keep
// Item { tag: label.tag(), from, class: label.class, untrusted: label.untrusted(), text }.
// 4. The kept items, in that order.
todo!()
}
/// The Honcho items `recall` shows after `local` hits of the local index, so that there are at
/// most `k` in all: each source's first, in the order given, then each one's second, and so on.
/// None when the local hits fill `k`.
pub fn interleave(local: usize, k: usize, sources: Vec<Vec<Item>>) -> Vec<Item> {
// `room` is `k.saturating_sub(local)`. Turn each source into an iterator (`into_iter`). Go
// round the sources in order, taking the next item of each that has one, until `room` items
// are taken or a whole round takes none. Return the items taken, in the order taken.
todo!()
}
/// `items` rendered one after another, numbered from `first`, joined by newlines.
pub fn render(first: usize, items: &[Item]) -> String {
items
.iter()
.enumerate()
.map(|(i, item)| item.render(first + i))
.collect::<Vec<_>>()
.join("\n")
}
/// The label the shown items give the answer: the highest of their classes, `private` at the
/// least, and untrusted when any is. Only items shown count.
pub fn label(items: &[Item]) -> (DataClass, bool) {
// The class: the highest `class` of `items` (`DataClass::max`), starting from
// `DataClass::Private`. Untrusted: any item's `untrusted` is true. Return (class, untrusted).
todo!()
}
/// One `{name}: unavailable` line per source that did not answer, in the order given, joined by
/// newlines; empty when every source answered.
pub fn unavailable(names: &[&str]) -> String {
names
.iter()
.map(|name| format!("{name}: unavailable"))
.collect::<Vec<_>>()
.join("\n")
}