open lists: order by origin time (an import is as old as it was in the giver), clip each line's item text, name the dropped follow-ups' campaigns
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
31b924704c
commit
94f235e7ce
4 changed files with 251 additions and 30 deletions
|
|
@ -170,7 +170,8 @@ pub(crate) const SHORT_MAX_BYTES: usize = 200;
|
|||
/// The most bytes each open-item list of the responsibilities section carries, outside `GOALS_MAX_BYTES`.
|
||||
const OPEN_COMMITTED_MAX_BYTES: usize = 4 * 1024;
|
||||
const OPEN_ASKED_MAX_BYTES: usize = 4 * 1024;
|
||||
/// The most open items one list fetches; the rest are counted.
|
||||
/// The page size of an open-item fetch. It bounds the cost of one read, not what shows: every page is read, because the
|
||||
/// order is the item's origin time and a page of admission order is not that order.
|
||||
const OPEN_FETCH: u32 = 64;
|
||||
/// The most event ids a Persona memory cites as its sources.
|
||||
pub(crate) const SOURCES_MAX: usize = 8;
|
||||
|
|
@ -2319,6 +2320,22 @@ fn ruling_ids_in(text: &str) -> Vec<String> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// The campaigns whose dropped follow-ups the note names; the rest are counted together.
|
||||
const DROPPED_CAMPAIGNS_NAMED: usize = 6;
|
||||
|
||||
/// The note that counts the open follow-ups the bound left out and says where they are, the biggest campaigns first.
|
||||
fn dropped_note(dropped: &BTreeMap<String, usize>) -> String {
|
||||
let mut by_campaign: Vec<(&String, &usize)> = dropped.iter().collect();
|
||||
by_campaign.sort_by(|a, b| b.1.cmp(a.1).then(a.0.cmp(b.0)));
|
||||
let total: usize = dropped.values().sum();
|
||||
let mut where_: Vec<String> = by_campaign.iter().take(DROPPED_CAMPAIGNS_NAMED).map(|(campaign, count)| format!("{campaign} {count}")).collect();
|
||||
if by_campaign.len() > DROPPED_CAMPAIGNS_NAMED {
|
||||
let rest: usize = by_campaign[DROPPED_CAMPAIGNS_NAMED..].iter().map(|(_, count)| **count).sum();
|
||||
where_.push(format!("{} more campaigns {rest}", by_campaign.len() - DROPPED_CAMPAIGNS_NAMED));
|
||||
}
|
||||
format!("({total} more open follow-ups not shown: {}; `huginn open <campaign>` lists them.)\n", where_.join(", "))
|
||||
}
|
||||
|
||||
/// What she is responsible for, from her own mind: her stewardships in force,
|
||||
/// always whole, with each record's note; her open commitments, then her open asks, each oldest first with
|
||||
/// its id and its own bound; her follow-ups in force, oldest
|
||||
|
|
@ -2370,15 +2387,24 @@ pub(crate) fn render_responsibilities(mind: &dyn MindPort, instance: &Slug, name
|
|||
(MemorySort::Committed, "You committed to, oldest first:", "commitments", OPEN_COMMITTED_MAX_BYTES),
|
||||
(MemorySort::Asked, "Asked of you, not yet taken on, oldest first:", "asks", OPEN_ASKED_MAX_BYTES),
|
||||
] {
|
||||
// Ascending, which is admission order: the oldest open items first, the newest left to the count.
|
||||
let selection = Selection {
|
||||
let mut asking = Selection {
|
||||
limit: Some(OPEN_FETCH),
|
||||
projection: "document".into(),
|
||||
..any_of(in_force(of_kind(PipelineKind::Memory)), "sort", &[format!("{sort:?}").as_str()])
|
||||
};
|
||||
let page = mind.query(&selection)?;
|
||||
let matched = page.matched as usize;
|
||||
let PipelinePageItems::Documents(views) = page.items else { bail!("the mind answered headers to a document selection") };
|
||||
let mut views = Vec::new();
|
||||
loop {
|
||||
let page = mind.query(&asking)?;
|
||||
let PipelinePageItems::Documents(items) = page.items else { bail!("the mind answered headers to a document selection") };
|
||||
views.extend(items);
|
||||
match page.next {
|
||||
Some(cursor) => asking.cursor = Some(cursor),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
// Oldest first by origin: an imported item is as old as it was in the giver, not as its import.
|
||||
views.sort_by(|a, b| recall::origin_order(&a.admission).cmp(&recall::origin_order(&b.admission)));
|
||||
let matched = views.len();
|
||||
let (mut section, mut shown) = (String::new(), 0usize);
|
||||
for view in &views {
|
||||
let Some(line) = recall::memory_line(view, names, true) else { continue };
|
||||
|
|
@ -2403,10 +2429,11 @@ pub(crate) fn render_responsibilities(mind: &dyn MindPort, instance: &Slug, name
|
|||
}
|
||||
// The open_ids items are outside the follow-ups' and rulings' bound.
|
||||
let open_len = out.len() - open_start;
|
||||
// Ascending, which is admission order: oldest first is the reminder of work she has carried longest, and the bound drops the newest.
|
||||
let open = in_force(of_kind(PipelineKind::FollowUp));
|
||||
let follow_ups = headers(mind, &open)?;
|
||||
let mut dropped = 0;
|
||||
// Oldest first by origin: an imported follow-up is as old as it was in the giver, not as its import. Work that has waited
|
||||
// longest leads, and the bound drops the newest.
|
||||
let mut follow_ups = headers(mind, &in_force(of_kind(PipelineKind::FollowUp)))?;
|
||||
follow_ups.sort_by(|a, b| recall::origin_order(&a.admission).cmp(&recall::origin_order(&b.admission)));
|
||||
let mut dropped: BTreeMap<String, usize> = BTreeMap::new();
|
||||
// Every open follow-up counts as open work for its campaign, whether or not the bound below lets its line show.
|
||||
let mut open_roots: Vec<String> = Vec::new();
|
||||
for follow_up in &follow_ups {
|
||||
|
|
@ -2423,9 +2450,9 @@ pub(crate) fn render_responsibilities(mind: &dyn MindPort, instance: &Slug, name
|
|||
let asked = origin.as_ref().map_or_else(String::new, |origin| format!(", {}", recall::origin_phrase(origin, names)));
|
||||
let campaign = follow_up.id.id.0.split_once(":follow_up:").map_or("", |(campaign, _)| campaign);
|
||||
let held = if claimed.contains(&follow_up.id) { " [claimed by a run]" } else { "" };
|
||||
let line = format!("- {campaign}/{} ({}{asked}){held}: {}\n", label.0, repo.0, item.0.lines().next().unwrap_or(""));
|
||||
let line = format!("- {campaign}/{} ({}{asked}){held}: {}\n", label.0, repo.0, recall::clipped(item.0.lines().next().unwrap_or("")));
|
||||
if out.len() - open_len + line.len() > GOALS_MAX_BYTES - GOALS_RULINGS_RESERVE {
|
||||
dropped += 1;
|
||||
*dropped.entry(campaign.to_owned()).or_default() += 1;
|
||||
continue;
|
||||
}
|
||||
if source.kind == PipelineKind::Ruling {
|
||||
|
|
@ -2438,8 +2465,8 @@ pub(crate) fn render_responsibilities(mind: &dyn MindPort, instance: &Slug, name
|
|||
if follow_ups.is_empty() {
|
||||
out.push_str("- no follow-up is open in your mind.\n");
|
||||
}
|
||||
if dropped > 0 {
|
||||
out.push_str(&format!("({dropped} more open follow-ups not shown: `huginn open <campaign>` lists them all.)\n"));
|
||||
if !dropped.is_empty() {
|
||||
out.push_str(&dropped_note(&dropped));
|
||||
}
|
||||
let mut dropped_rulings = cited.len().saturating_sub(GOALS_RULINGS_MAX);
|
||||
cited.truncate(GOALS_RULINGS_MAX);
|
||||
|
|
|
|||
|
|
@ -2289,13 +2289,13 @@ fn a_long_sender_id_stays_out_of_the_schema_and_never_poisons_a_turn() {
|
|||
assert!(input_of(&rig, 0).contains("hello there"));
|
||||
}
|
||||
|
||||
/// Twelve open asks of about 900 bytes, heavy memories about the sender and matching the words, and a few short deeds.
|
||||
/// Twenty open asks of about 900 bytes (each listed clipped), heavy memories about the sender and matching the words, and a few short deeds.
|
||||
#[test]
|
||||
fn caps_count_what_they_leave_out() {
|
||||
let (libby, op) = ("@libby:chat", "@op:chat");
|
||||
let heavy = |tag: &str| format!("{tag} hello {}", "x".repeat(880));
|
||||
let rig = Rig::new(vec![delivery(1, "swarm", op, "hello")], &[says(&[("swarm", "hi")])]);
|
||||
for index in 0..12 {
|
||||
for index in 0..20 {
|
||||
rig.mind.committed(vec![crate::testkit::memory(&format!("ask-{index:02}"), MemorySort::Asked, &heavy(&format!("ASK{index:02}")), &[libby])]);
|
||||
}
|
||||
for index in 0..6 {
|
||||
|
|
@ -2312,10 +2312,10 @@ fn caps_count_what_they_leave_out() {
|
|||
// The asks: shown with ids, the rest counted exactly, and the settle enum is exactly what was shown.
|
||||
let asked = under(responsibilities_of(&input), "Asked of you, not yet taken on, oldest first:");
|
||||
let shown_ids: Vec<String> = asked.lines().filter(|line| line.starts_with("- ") && line.contains(":memory:")).map(|line| line[2..].split(' ').next().unwrap().to_string()).collect();
|
||||
assert!(!shown_ids.is_empty() && shown_ids.len() < 12, "{} shown: {asked}", shown_ids.len());
|
||||
assert!(!shown_ids.is_empty() && shown_ids.len() < 20, "{} shown: {asked}", shown_ids.len());
|
||||
let expected_oldest: Vec<String> = (0..shown_ids.len()).map(|index| format!("yggdrasil:memory:ask-{index:02}")).collect();
|
||||
assert_eq!(shown_ids, expected_oldest, "the oldest asks show, in admission order, and the newest are counted");
|
||||
assert!(asked.contains(&format!("({} more asks not shown", 12 - shown_ids.len())), "{} shown, ids {shown_ids:?}", shown_ids.len());
|
||||
assert!(asked.contains(&format!("({} more asks not shown", 20 - shown_ids.len())), "{} shown, ids {shown_ids:?}", shown_ids.len());
|
||||
let mut enum_ids = settle_enum(&rig, 0);
|
||||
enum_ids.sort();
|
||||
let mut expected = shown_ids.clone();
|
||||
|
|
@ -2367,22 +2367,25 @@ fn each_open_list_holds_its_own_bound_exactly_and_counts_what_it_drops() {
|
|||
(MemorySort::Committed, "You committed to, oldest first:", "commitments", OPEN_COMMITTED_MAX_BYTES),
|
||||
(MemorySort::Asked, "Asked of you, not yet taken on, oldest first:", "asks", OPEN_ASKED_MAX_BYTES),
|
||||
] {
|
||||
let render = |adjustable: usize| {
|
||||
// Short fillers make the lines fine-grained; the oldest item's text (under the clip) tunes the total to the byte.
|
||||
let render = |fillers: usize, adjustable: usize| {
|
||||
let mind = TestMind::seeded();
|
||||
mind.committed(vec![crate::testkit::memory("m0", sort, &"w".repeat(adjustable), &[])]);
|
||||
for index in 1..4 {
|
||||
mind.committed(vec![crate::testkit::memory(&format!("m{index}"), sort, &"f".repeat(1000), &[])]);
|
||||
for index in 1..=fillers {
|
||||
mind.committed(vec![crate::testkit::memory(&format!("m{index:02}"), sort, "f", &[])]);
|
||||
}
|
||||
render_responsibilities(&mind, &slug, &names).unwrap().text
|
||||
};
|
||||
let used = |text: &str| under(text, heading).lines().filter(|line| line.starts_with("- ") && line.contains(":memory:")).map(|line| line.len() + 1).sum::<usize>();
|
||||
let lines = |text: &str| under(text, heading).lines().filter(|line| line.starts_with("- ") && line.contains(":memory:")).count();
|
||||
let fit = 100 + bound - used(&render(100));
|
||||
let fits = render(fit);
|
||||
assert_eq!((used(&fits), lines(&fits)), (bound, 4), "an exact fit is shown: {fits}");
|
||||
let fillers = (1..100).take_while(|count| lines(&render(*count, 0)) == count + 1).last().unwrap();
|
||||
let fit = bound - used(&render(fillers, 0));
|
||||
assert!(fit < 150, "{fit}");
|
||||
let fits = render(fillers, fit);
|
||||
assert_eq!((used(&fits), lines(&fits)), (bound, fillers + 1), "an exact fit is shown: {fits}");
|
||||
assert!(!fits.contains("not shown"), "{fits}");
|
||||
let over = render(fit + 1);
|
||||
assert_eq!(lines(&over), 3, "{over}");
|
||||
let over = render(fillers, fit + 1);
|
||||
assert_eq!(lines(&over), fillers, "{over}");
|
||||
assert!(over.contains(&format!("(1 more {noun} not shown: settle what is finished and they come back into view.)")), "{over}");
|
||||
}
|
||||
// Both lists full and a follow-up still has its room: the open lists are outside the goals bound.
|
||||
|
|
@ -7355,3 +7358,129 @@ fn a_sleep_holder_opens_one_process_with_the_night_schema_and_her_tools_never_a_
|
|||
assert_eq!(rig.chat.pending_calls.get(), 0);
|
||||
assert!(rig.model.requests.borrow()[0].input.contains("Tonight you consolidate your memory."));
|
||||
}
|
||||
|
||||
/// A follow-up of `campaign` (whose spec `src` and campaign the caller has admitted), with its item text.
|
||||
fn follow_up_in(campaign: &str, label: &str, item: &str) -> PipelineDocument {
|
||||
let PipelineDocument::FollowUp(mut follow_up) = follow_up(label, &reference(K::CutSpec, &format!("{campaign}:cut_spec:cut-src.r1"))) else { unreachable!("a follow-up") };
|
||||
follow_up.campaign = Slug(campaign.into());
|
||||
follow_up.item = Line(item.into());
|
||||
PipelineDocument::FollowUp(follow_up)
|
||||
}
|
||||
|
||||
/// The labels of the follow-up lines `text` lists, in order.
|
||||
fn listed_follow_ups(text: &str) -> Vec<String> {
|
||||
text.lines()
|
||||
.filter_map(|line| line.strip_prefix("- "))
|
||||
.filter(|line| line.contains(" (GameCult/"))
|
||||
.filter_map(|line| line.split(' ').next())
|
||||
.map(|id| id.rsplit('/').next().unwrap().to_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// An imported item is as old as it was in the giver: it sorts by that time, not by when it was imported.
|
||||
#[test]
|
||||
fn an_imported_item_sorts_by_its_origin_time_not_its_import() {
|
||||
let mind = TestMind::seeded();
|
||||
mind.committed(chain(CAMPAIGN, "src", REPO));
|
||||
mind.committed(vec![follow_up_in(CAMPAIGN, "native", "Native, admitted first of the two.")]);
|
||||
mind.committed_imported("2020-01-01T00:00:00Z", vec![follow_up_in(CAMPAIGN, "imported-old", "Imported, born in the giver long ago.")]);
|
||||
mind.committed_imported("2999-01-01T00:00:00Z", vec![follow_up_in(CAMPAIGN, "imported-new", "Imported, born in the giver later than anything.")]);
|
||||
mind.committed(vec![crate::testkit::memory("m-native", MemorySort::Asked, "native ask", &[])]);
|
||||
mind.committed_imported("2020-01-01T00:00:00Z", vec![crate::testkit::memory("m-imported", MemorySort::Asked, "imported ask", &[])]);
|
||||
let text = render_responsibilities(&mind, &Slug(INSTANCE.into()), &BTreeMap::new()).unwrap().text;
|
||||
assert_eq!(listed_follow_ups(&text), ["imported-old", "native", "imported-new"], "{text}");
|
||||
let at = |needle: &str| text.find(needle).unwrap_or_else(|| panic!("{needle} in {text}"));
|
||||
assert!(at("m-imported") < at("m-native"), "{text}");
|
||||
assert!(text.contains("(2020-01-01, "), "an imported memory shows the date it began: {text}");
|
||||
}
|
||||
|
||||
/// A listed line carries about 160 bytes of the item's text, cut on a character boundary with an ellipsis; the whole stays one `huginn view` away.
|
||||
#[test]
|
||||
fn a_listed_line_clips_the_item_text() {
|
||||
assert_eq!(recall::clipped(&"w".repeat(160)), "w".repeat(160));
|
||||
assert_eq!(recall::clipped(&"w".repeat(161)), format!("{}…", "w".repeat(160)));
|
||||
let accents = "é".repeat(100);
|
||||
assert_eq!(recall::clipped(&accents), format!("{}…", "é".repeat(80)), "a cut inside a character backs up to its start");
|
||||
let mind = TestMind::seeded();
|
||||
mind.committed(chain(CAMPAIGN, "src", REPO));
|
||||
mind.committed(vec![follow_up_in(CAMPAIGN, "long", &"w".repeat(500))]);
|
||||
mind.committed(vec![crate::testkit::memory("m-long", MemorySort::Committed, &"v".repeat(500), &[])]);
|
||||
let text = render_responsibilities(&mind, &Slug(INSTANCE.into()), &BTreeMap::new()).unwrap().text;
|
||||
assert!(text.contains(&format!("{}…", "w".repeat(160))) && !text.contains(&"w".repeat(161)), "{text}");
|
||||
assert!(text.contains(&format!("{}…", "v".repeat(160))) && !text.contains(&"v".repeat(161)), "{text}");
|
||||
let whole = recall::memory_line(&mind.view(K::Memory, &format!("{INSTANCE}:memory:m-long")).unwrap(), &BTreeMap::new(), false).unwrap();
|
||||
assert!(whole.contains(&"v".repeat(500)), "recall shows a memory whole: {whole}");
|
||||
}
|
||||
|
||||
/// A line that does not fit is skipped and a later short one may still show, in the follow-ups and in the open memories.
|
||||
#[test]
|
||||
fn a_line_that_does_not_fit_is_skipped_and_a_later_short_one_shows() {
|
||||
// Follow-ups: fill the bound, then an older long-labelled line that cannot fit, then a newer short one.
|
||||
let build = |fillers: usize, with_short: bool| {
|
||||
let mind = TestMind::seeded();
|
||||
mind.committed(chain(CAMPAIGN, "src", REPO));
|
||||
for index in 0..fillers {
|
||||
mind.committed(vec![follow_up_in(CAMPAIGN, &format!("fill{index:03}"), "filler")]);
|
||||
}
|
||||
mind.committed(vec![follow_up_in(CAMPAIGN, &format!("big{}", "g".repeat(190)), &"b".repeat(160))]);
|
||||
if with_short {
|
||||
mind.committed(vec![follow_up_in(CAMPAIGN, "short", "s")]);
|
||||
}
|
||||
render_responsibilities(&mind, &Slug(INSTANCE.into()), &BTreeMap::new()).unwrap().text
|
||||
};
|
||||
let fillers = (1..200).find(|count| !build(*count, false).contains("big")).expect("the bound is reached");
|
||||
let text = build(fillers, true);
|
||||
assert!(!text.contains("big") && listed_follow_ups(&text).last().map(String::as_str) == Some("short"), "{text}");
|
||||
assert!(text.contains("(1 more open follow-ups not shown: eureka-body 1;"), "{text}");
|
||||
// Open memories: the same.
|
||||
for sort in [MemorySort::Committed, MemorySort::Asked] {
|
||||
let build = |fillers: usize, with_short: bool| {
|
||||
let mind = TestMind::seeded();
|
||||
for index in 0..fillers {
|
||||
mind.committed(vec![crate::testkit::memory(&format!("fill{index:03}"), sort, "filler", &[])]);
|
||||
}
|
||||
let about: Vec<String> = (0..8).map(|index| format!("{index}{}", "p".repeat(190))).collect();
|
||||
let about: Vec<&str> = about.iter().map(String::as_str).collect();
|
||||
mind.committed(vec![crate::testkit::memory("big", sort, &"b".repeat(160), &about)]);
|
||||
if with_short {
|
||||
mind.committed(vec![crate::testkit::memory("short", sort, "s", &[])]);
|
||||
}
|
||||
render_responsibilities(&mind, &Slug(INSTANCE.into()), &BTreeMap::new()).unwrap().text
|
||||
};
|
||||
let fillers = (1..200).find(|count| !build(*count, false).contains(":memory:big")).expect("the bound is reached");
|
||||
let text = build(fillers, true);
|
||||
assert!(!text.contains(":memory:big") && text.contains(":memory:short"), "{sort:?}: {text}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The note that counts the dropped follow-ups says where they are, the biggest campaign first, and stays bounded.
|
||||
#[test]
|
||||
fn the_dropped_follow_ups_note_names_their_campaigns() {
|
||||
let mind = TestMind::seeded();
|
||||
for campaign in [CAMPAIGN, "hidden-in", "third"] {
|
||||
if campaign != CAMPAIGN {
|
||||
mind.committed(campaign_on(campaign, &[REPO]));
|
||||
}
|
||||
mind.committed(chain(campaign, "src", REPO));
|
||||
}
|
||||
let long = "w".repeat(400);
|
||||
for index in 0..30 {
|
||||
mind.committed(vec![follow_up_in(CAMPAIGN, &format!("fu-{index:02}"), &long)]);
|
||||
}
|
||||
for index in 0..5 {
|
||||
mind.committed(vec![follow_up_in("hidden-in", &format!("h{index}"), &long)]);
|
||||
}
|
||||
for index in 0..7 {
|
||||
mind.committed(vec![follow_up_in("third", &format!("t{index}"), &long)]);
|
||||
}
|
||||
let text = render_responsibilities(&mind, &Slug(INSTANCE.into()), &BTreeMap::new()).unwrap().text;
|
||||
let listed = listed_follow_ups(&text).len();
|
||||
let note = text.lines().find(|line| line.contains("more open follow-ups not shown")).unwrap_or_else(|| panic!("{text}"));
|
||||
let native_dropped = 30 - listed;
|
||||
assert_eq!(note, format!("({} more open follow-ups not shown: eureka-body {native_dropped}, third 7, hidden-in 5; `huginn open <campaign>` lists them.)", native_dropped + 12), "{text}");
|
||||
let many: BTreeMap<String, usize> = (0..9).map(|index| (format!("c{index}"), 10 - index)).collect();
|
||||
assert_eq!(
|
||||
dropped_note(&many),
|
||||
"(54 more open follow-ups not shown: c0 10, c1 9, c2 8, c3 7, c4 6, c5 5, 3 more campaigns 9; `huginn open <campaign>` lists them.)\n"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ use anyhow::{Result, bail};
|
|||
use chrono::{DateTime, Utc};
|
||||
use cultnet_rs::Selection;
|
||||
use eureka_pipeline::{MemoryOrigin, MemorySort, PipelineDocument, PipelineKind};
|
||||
use huginn_mind::{PipelineDocumentView, PipelinePageItems};
|
||||
use huginn_mind::{AdmissionFacts, PipelineDocumentView, PipelinePageItems};
|
||||
|
||||
use crate::persona::quoted;
|
||||
use crate::queue::{MindPort, any_of, in_force, of_kind};
|
||||
|
|
@ -215,8 +215,10 @@ pub(crate) fn origin_phrase(origin: &MemoryOrigin, names: &BTreeMap<&str, &str>)
|
|||
/// recall and for the open items the responsibilities section shows, which pass
|
||||
/// `with_id` to lead with the id she settles it by. `about` is never the place.
|
||||
pub(crate) fn memory_line(view: &PipelineDocumentView, names: &BTreeMap<&str, &str>, with_id: bool) -> Option<String> {
|
||||
// An open item of the responsibilities (the one caller that leads with the id) shows a clipped text; recall shows it whole.
|
||||
let PipelineDocument::Memory(memory) = &view.document else { return None };
|
||||
let date = view.admission.admitted_at.get(..10).unwrap_or(&view.admission.admitted_at);
|
||||
let when = origin_time(&view.admission);
|
||||
let date = when.get(..10).unwrap_or(when);
|
||||
let origin = origin_phrase(&memory.origin, names);
|
||||
let about = who(memory.about.iter().map(|id| id.0.as_str()), names);
|
||||
let sort = match (memory.sort, memory.about.is_empty()) {
|
||||
|
|
@ -227,7 +229,30 @@ pub(crate) fn memory_line(view: &PipelineDocumentView, names: &BTreeMap<&str, &s
|
|||
(sort, false) => format!("{sort:?} about {about}"),
|
||||
};
|
||||
let lead = if with_id { format!("{} ", view.id.id.0) } else { String::new() };
|
||||
Some(format!("- {lead}({date}, {origin}, {sort}): {}\n", quoted(&memory.text.0)))
|
||||
Some(format!("- {lead}({date}, {origin}, {sort}): {}\n", quoted(&if with_id { clipped(&memory.text.0) } else { memory.text.0.clone() })))
|
||||
}
|
||||
|
||||
/// The most bytes of an item's text a listed line carries; the rest is one `huginn view` away.
|
||||
pub(crate) const LINE_TEXT_MAX_BYTES: usize = 160;
|
||||
|
||||
/// `text` cut to about `LINE_TEXT_MAX_BYTES` on a character boundary, with an ellipsis where it was cut.
|
||||
pub(crate) fn clipped(text: &str) -> String {
|
||||
if text.len() <= LINE_TEXT_MAX_BYTES {
|
||||
return text.to_owned();
|
||||
}
|
||||
let cut = (0..=LINE_TEXT_MAX_BYTES).rev().find(|at| text.is_char_boundary(*at)).unwrap_or(0);
|
||||
format!("{}…", &text[..cut])
|
||||
}
|
||||
|
||||
/// When a document first stood, as the time it was admitted where it began: an imported document carries the source
|
||||
/// receipt's time, any other its own admission.
|
||||
pub(crate) fn origin_time(admission: &AdmissionFacts) -> &str {
|
||||
admission.provenance.imported_from.as_ref().map_or(admission.admitted_at.as_str(), |source| source.committed_at.0.as_str())
|
||||
}
|
||||
|
||||
/// The sort key of oldest first: origin time, then the mind's own admission ordinal for equal times.
|
||||
pub(crate) fn origin_order(admission: &AdmissionFacts) -> (&str, u64) {
|
||||
(origin_time(admission), admission.ordinal)
|
||||
}
|
||||
|
||||
/// One recalled document as its line, or None for a kind recall does not show.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use eureka_pipeline::{
|
|||
PipelineTarget, QuestionOption, ResolutionOutcome, RulingAuthority, RunOperator, RunTurn, Sha, Slug, StructuralDelta, TargetInvariant,
|
||||
};
|
||||
use huginn_mind::{
|
||||
Faculty, HuginnMindRequest, HuginnMindResponse, Mind, OwnedRedbMessagePackBackingStore, PipelineAdmissionBatch, PipelineAdmissionOutcome,
|
||||
Faculty, HuginnMindRequest, ImportSource, ImportSource, HuginnMindResponse, Mind, OwnedRedbMessagePackBackingStore, PipelineAdmissionBatch, PipelineAdmissionOutcome,
|
||||
PipelineDocumentView, PipelineProvenance, PipelineSelectionPage,
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
|
@ -155,6 +155,46 @@ impl TestMind {
|
|||
self.mind.borrow_mut().admit(batch, self.now.get())
|
||||
}
|
||||
|
||||
/// Admits as an import of a giver's receipt committed at `committed_at`, one second after the last admission here.
|
||||
pub(crate) fn committed_imported(&self, committed_at: &str, documents: Vec<PipelineDocument>) {
|
||||
self.now.set(self.now.get() + Duration::seconds(1));
|
||||
let source = ImportSource {
|
||||
instance: Slug("giver".into()),
|
||||
receipt_id: format!("giver-{committed_at}").as_str().into(),
|
||||
ordinal: 1,
|
||||
committed_at: committed_at.into(),
|
||||
faculty: Faculty::Hands,
|
||||
agent: "test".into(),
|
||||
session: "test".into(),
|
||||
tool: "test".into(),
|
||||
external: vec![],
|
||||
};
|
||||
let provenance = PipelineProvenance { faculty: Faculty::Hands, agent: "test".into(), session: "test".into(), tool: "test".into(), imported_from: Some(source) };
|
||||
let batch = PipelineAdmissionBatch { instance: self.instance.clone(), provenance, documents };
|
||||
let outcome = self.mind.borrow_mut().admit(batch, self.now.get());
|
||||
assert!(matches!(outcome, PipelineAdmissionOutcome::Committed { .. }), "{outcome:?}");
|
||||
}
|
||||
|
||||
/// Admits as an import of a giver's receipt committed at `committed_at`, one second after the last admission here.
|
||||
pub(crate) fn committed_imported(&self, committed_at: &str, documents: Vec<PipelineDocument>) {
|
||||
self.now.set(self.now.get() + Duration::seconds(1));
|
||||
let source = ImportSource {
|
||||
instance: Slug("giver".into()),
|
||||
receipt_id: format!("giver-{committed_at}").as_str().into(),
|
||||
ordinal: 1,
|
||||
committed_at: committed_at.into(),
|
||||
faculty: Faculty::Hands,
|
||||
agent: "test".into(),
|
||||
session: "test".into(),
|
||||
tool: "test".into(),
|
||||
external: vec![],
|
||||
};
|
||||
let provenance = PipelineProvenance { faculty: Faculty::Hands, agent: "test".into(), session: "test".into(), tool: "test".into(), imported_from: Some(source) };
|
||||
let batch = PipelineAdmissionBatch { instance: self.instance.clone(), provenance, documents };
|
||||
let outcome = self.mind.borrow_mut().admit(batch, self.now.get());
|
||||
assert!(matches!(outcome, PipelineAdmissionOutcome::Committed { .. }), "{outcome:?}");
|
||||
}
|
||||
|
||||
pub(crate) fn committed(&self, documents: Vec<PipelineDocument>) {
|
||||
let outcome = self.put(documents);
|
||||
assert!(matches!(outcome, PipelineAdmissionOutcome::Committed { .. }), "{outcome:?}");
|
||||
|
|
|
|||
Loading…
Reference in a new issue