From e7908a0ed5796fd49d07bf9f1d1770bb5a294af7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 3 Nov 2026 05:09:15 +0100 Subject: [PATCH 1/9] wear-facts: BLOCKING lives in eureka_state::hold, one answer to what counts as blocking Co-Authored-By: Claude Opus 5.5 --- crates/eureka-state/src/bin/huginn/recipes.rs | 3 +-- crates/eureka-state/src/hold.rs | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/eureka-state/src/bin/huginn/recipes.rs b/crates/eureka-state/src/bin/huginn/recipes.rs index 8bcc46e..7d1c3b2 100644 --- a/crates/eureka-state/src/bin/huginn/recipes.rs +++ b/crates/eureka-state/src/bin/huginn/recipes.rs @@ -5,6 +5,7 @@ //! reads through `eureka_state::hold`, the decider the heartbeat's gate reads. use clap::{Args, Subcommand}; +use eureka_state::hold::BLOCKING; use huginn_mind::SemanticQuery; use huginn_mind::eureka_pipeline::{Line, PipelineKind}; use serde_json::{Value, json}; @@ -16,8 +17,6 @@ use crate::trouble::Trouble; const GAP_HOME: &str = "eureka-substrate"; /// How many neighbours `precedent` asks the index for. const PRECEDENT_TOP_K: u32 = 10; -/// Severities that block a cut. -const BLOCKING: [&str; 2] = ["Blocker", "High"]; #[derive(Args, Clone, Debug, Default)] pub struct Paging { diff --git a/crates/eureka-state/src/hold.rs b/crates/eureka-state/src/hold.rs index ec1fc24..a065afb 100644 --- a/crates/eureka-state/src/hold.rs +++ b/crates/eureka-state/src/hold.rs @@ -44,6 +44,10 @@ pub fn citing(target: &PipelineRef, role: Option<&str>) -> Citation { } } +/// Severities that block a cut: the one answer to what counts as blocking, read by `huginn findings --blocking` and by a Mind's +/// first line. +pub const BLOCKING: [&str; 2] = ["Blocker", "High"]; + pub fn of_kind(kind: PipelineKind) -> Selection { Selection { schemas: Some(vec![kind.type_id().to_string()]), From fbaf9bd33a86b5e8a0aab2d1a808f7591ad684f2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 3 Nov 2026 05:11:48 +0100 Subject: [PATCH 2/9] wear-facts: Mind::roots_touched, the newest admission per root with a campaign's repos in force, through the MindPort and the wire Co-Authored-By: Claude Opus 5.5 --- crates/eureka-state/src/bin/huginn/main.rs | 1 + crates/huginn-daemon/src/daemon.rs | 30 ++++ crates/huginn-mind/src/lib.rs | 2 +- crates/huginn-mind/src/query.rs | 160 +++++++++++++++++- crates/huginn-mind/src/wire.rs | 14 +- crates/mind-body/src/queue.rs | 48 +++++- crates/mind-body/src/testkit.rs | 9 + crates/mind-body/tests/public_surface.txt | 2 +- .../huginn.mind_request.v1.schema.json | 21 +++ .../huginn.mind_response.v1.schema.json | 44 +++++ 10 files changed, 323 insertions(+), 8 deletions(-) diff --git a/crates/eureka-state/src/bin/huginn/main.rs b/crates/eureka-state/src/bin/huginn/main.rs index 0ec6647..635d76c 100644 --- a/crates/eureka-state/src/bin/huginn/main.rs +++ b/crates/eureka-state/src/bin/huginn/main.rs @@ -509,6 +509,7 @@ fn unexpected(answer: &HuginnMindResponse) -> Trouble { HuginnMindResponse::PersonaPut(_) => "a persona put", HuginnMindResponse::Sources(_) => "source hits", HuginnMindResponse::Activity(_) => "repo activity", + HuginnMindResponse::RootsTouched(_) => "roots touched", HuginnMindResponse::Refused(_) => "a refusal", HuginnMindResponse::Deferred(_) => "a deferral", }; diff --git a/crates/huginn-daemon/src/daemon.rs b/crates/huginn-daemon/src/daemon.rs index 421d7f1..41220bc 100644 --- a/crates/huginn-daemon/src/daemon.rs +++ b/crates/huginn-daemon/src/daemon.rs @@ -200,6 +200,10 @@ impl> Daemon { HuginnMindRequest::Query { selection, semantic: Some(semantic), .. } => { return self.begin_search(selection, semantic); } + HuginnMindRequest::RootsTouched { .. } => match self.mind.roots_touched() { + Ok(roots) => HuginnMindResponse::RootsTouched(roots), + Err(refusal) => HuginnMindResponse::Refused(refusal), + }, HuginnMindRequest::StewardedActivity { instance, repos, after } => { let refused = |message: &str| MindRefusal::SelectionInvalid { field: "repos".into(), value: None, message: message.into() }; if repos.len() > ACTIVITY_REPOS_MAX { @@ -1073,6 +1077,32 @@ pub(crate) mod tests { } + /// A roots read through the daemon's handle answers from the mind it serves: the campaign with its repos in force and the + /// instance's own root with none; a request naming another instance is refused at the door, as every read is, before the mind + /// is asked. + #[test] + fn roots_touched_answers_on_the_wire() { + let (_root, mut daemon) = seeded_wide(); + let HuginnMindResponse::RootsTouched(roots) = daemon.handle(HuginnMindRequest::RootsTouched { instance: slug(INSTANCE) }, now()).answered() else { + panic!("a roots read answers the roots"); + }; + let by_root = |name: &str| roots.iter().find(|root| root.root.0 == name).unwrap_or_else(|| panic!("{name} in {roots:?}")); + assert_eq!(by_root(CAMPAIGN).repos, [OrgRepo(REPO.into())]); + assert!(by_root(INSTANCE).repos.is_empty()); + assert_eq!(roots.len(), 2, "{roots:?}"); + + let bytes = rmp_serde::to_vec_named(&HuginnMindResponse::RootsTouched(roots.clone())).unwrap(); + assert_eq!(rmp_serde::from_slice::(&bytes).unwrap(), HuginnMindResponse::RootsTouched(roots)); + + for declared in [OTHER, NEAR, PREFIXED, CASED] { + assert_eq!( + daemon.handle(HuginnMindRequest::RootsTouched { instance: slug(declared) }, now()).answered(), + HuginnMindResponse::Refused(MindRefusal::ForeignInstance { declared: declared.into(), mind: INSTANCE.into() }), + "{declared}" + ); + } + } + fn activity_request(repos: &[&str]) -> HuginnMindRequest { HuginnMindRequest::StewardedActivity { instance: slug(INSTANCE), repos: repos.iter().map(|repo| OrgRepo::from(*repo)).collect(), after: Some(1) } } diff --git a/crates/huginn-mind/src/lib.rs b/crates/huginn-mind/src/lib.rs index c868b99..526bffd 100644 --- a/crates/huginn-mind/src/lib.rs +++ b/crates/huginn-mind/src/lib.rs @@ -48,7 +48,7 @@ pub use mind::{HuginnMindEpoch, Mind}; pub use persona::{PersonaPutOutcome, PersonaStateView}; pub use query::{ AdmissionFacts, PipelineDocumentSummary, PipelineDocumentView, PipelineEdge, PipelineFacts, PipelinePageItems, - Members, PipelineSelectionPage, PipelineStatus, PipelineStatusSummary, Ranked, SUMMARY_MAX_BYTES, SemanticQuery, + Members, PipelineSelectionPage, PipelineStatus, PipelineStatusSummary, Ranked, RootTouched, SUMMARY_MAX_BYTES, SemanticQuery, }; pub use receipt::{DocumentVersion, Faculty, HuginnCommitReceipt, PipelineProvenance, RECEIPT_SCHEMA_VERSION}; pub use refusal::{MindRefusal, RunId}; diff --git a/crates/huginn-mind/src/query.rs b/crates/huginn-mind/src/query.rs index c722abf..1897849 100644 --- a/crates/huginn-mind/src/query.rs +++ b/crates/huginn-mind/src/query.rs @@ -28,7 +28,7 @@ use cultnet_rs::{ }; use eureka_pipeline::{ ClaimOutcome, CommitRange, Date, FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MemoryOrigin, MemorySort, OrgRepo, - PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, ResolutionOutcome, RulingAuthority, RunOperator, RunTurn, Sha, + PipelineCampaign, PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, ResolutionOutcome, RulingAuthority, RunOperator, RunTurn, Sha, Short, Slug, Title, pipeline_key, }; use schemars::JsonSchema; @@ -39,7 +39,7 @@ use crate::mind::{Mind, schema_cache}; use crate::persona::HuginnPersonaEntry; use crate::receipt::{self, PipelineProvenance}; use crate::refusal::MindRefusal; -use crate::rows::{SelectionRow, Vocabulary, refuse_values}; +use crate::rows::{SelectionRow, Vocabulary, refuse_values, root_and_local}; use crate::store::MindStore; /// What admission recorded about a document when it landed: the receipt that @@ -383,6 +383,23 @@ fn integrity(type_id: &str, key: &str, fault: &str) -> MindRefusal { MindRefusal::Unavailable { detail: format!("document {type_id}/{key} {fault}") } } +/// A root of the mind and the newest admission under it. `repos` is the repo set in force of a root that is an in-force campaign, +/// and empty for any other root. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct RootTouched { + pub root: Slug, + pub admitted_at: String, + #[schemars(range(min = 1))] + pub ordinal: u64, + pub repos: Vec, +} + +impl RootTouched { + fn of(root: &str, facts: &AdmissionFacts, repos: &[OrgRepo]) -> Self { + Self { root: Slug(root.to_string()), admitted_at: facts.admitted_at.clone(), ordinal: facts.ordinal, repos: repos.to_vec() } + } +} + /// One read call's working set: the image as of one snapshot and the facts it /// was built from. Built once per call and thrown away with it; nothing here /// outlives the answer. @@ -455,6 +472,31 @@ impl Reader { current } + fn roots_touched(&self) -> Vec { + let mut newest: BTreeMap<&str, &AdmissionFacts> = BTreeMap::new(); + for ((type_id, key), facts) in &self.facts.0 { + if kind_of_type(type_id).is_none() { + continue; + } + let root = root_and_local(key).0; + if newest.get(root).is_none_or(|seen| seen.ordinal < facts.ordinal) { + newest.insert(root, facts); + } + } + let docs = &self.docs; + let campaigns: BTreeMap<&str, &PipelineCampaign> = docs + .of_kind(PipelineKind::Campaign) + .filter_map(|(key, document)| match document { + PipelineDocument::Campaign(campaign) if docs.in_force(PipelineKind::Campaign, key) => Some((campaign.slug.0.as_str(), campaign)), + _ => None, + }) + .collect(); + newest + .into_iter() + .map(|(root, facts)| RootTouched::of(root, facts, campaigns.get(root).map_or(&[][..], |campaign| docs.repos_in_force(campaign)))) + .collect() + } + fn rows(&self) -> Result, MindRefusal> { self.docs .image @@ -563,6 +605,14 @@ impl Mind { Self::page(&selection, evaluation, as_of, None) } + /// Every root this mind holds a document under, with its newest admission and, for a root that is an in-force + /// campaign, the repos in force (`Docs::repos_in_force`, never the campaign row's opening set). A resolution's + /// key carries its subject's root, so a later resolution is a touch of that root. One pass at the head, no rows. + pub fn roots_touched(&self) -> Result, MindRefusal> { + let head = receipt::head(self)?; + Ok(Reader::at(self, head)?.roots_touched()) + } + /// What a semantic query must satisfy before anything is embedded: the /// selection passes the same two doors `query` does, carries no cursor and /// no `descending` (a ranked answer is neither pageable nor reversible), @@ -2521,4 +2571,110 @@ mod tests { let count = mind.read_back().unwrap(); println!("read back {count} stored documents in {:?}", started.elapsed()); } + + fn at_hour(hour: u32) -> chrono::DateTime { + Utc.with_ymd_and_hms(2026, 9, 16, 12 + hour, 0, 0).unwrap() + } + + fn touched(mind: &Mind) -> BTreeMap { + mind.roots_touched().unwrap().into_iter().map(|root| (root.root.0.clone(), root)).collect() + } + + /// A target revision naming `repos`, and the supersession of its predecessor. + fn repo_revision(campaign: &str, revision: u32, repos: &[&str]) -> Vec { + let D::Target(mut named) = target(revision, &[INVARIANT]) else { panic!("a target") }; + named.campaign = campaign.into(); + named.repos = repos.iter().map(|name| repo(name)).collect(); + let key = |n: u32| format!("{campaign}:target:r{n}"); + let mut batch = vec![D::Target(named)]; + if revision > 1 { + batch.push(resolution(r(K::Target, &key(revision - 1)), superseded(&[r(K::Target, &key(revision))]))); + } + batch + } + + /// Each root answers its newest admission: a later document counts, and so does a later resolution of an older one; an + /// instance's root is listed with no repos; a root is listed once, in the order of its name. + #[test] + fn roots_touched_is_the_newest_admission_per_root() { + let mut mind = seeded(); + let first = touched(&mind); + assert_eq!(first.keys().map(String::as_str).collect::>(), [CAMPAIGN, INSTANCE]); + assert_eq!((first[CAMPAIGN].admitted_at.as_str(), first[INSTANCE].admitted_at.as_str()), ("2026-09-16T12:00:00Z", "2026-09-16T12:00:00Z")); + assert!(first[INSTANCE].repos.is_empty(), "an instance root is no campaign"); + + let D::Campaign(mut other) = campaign(&["GameCult/Third"]) else { panic!("a campaign") }; + other.slug = "other-campaign".into(); + committed(admit_at(&mut mind, vec![D::Campaign(other)], at_hour(1))); + committed(admit_at(&mut mind, vec![question_n(1)], at_hour(2))); + let second = touched(&mind); + assert_eq!(second.keys().map(String::as_str).collect::>(), [CAMPAIGN, "other-campaign", INSTANCE]); + assert_eq!(second["other-campaign"].admitted_at, "2026-09-16T13:00:00Z"); + assert_eq!(second[CAMPAIGN].admitted_at, "2026-09-16T14:00:00Z", "a later document touches its root"); + assert_eq!(second[INSTANCE].admitted_at, "2026-09-16T12:00:00Z", "and no other root"); + assert!(second[INSTANCE].ordinal < second["other-campaign"].ordinal && second["other-campaign"].ordinal < second[CAMPAIGN].ordinal, "{second:?}"); + + committed(admit_at(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())], at_hour(5))); + let third = touched(&mind); + assert_eq!(third[CAMPAIGN].admitted_at, "2026-09-16T17:00:00Z", "a later resolution is a touch of its subject's root"); + assert!(third[CAMPAIGN].ordinal > second[CAMPAIGN].ordinal); + assert_eq!(third["other-campaign"], second["other-campaign"]); + } + + /// A campaign answers the repos in force, not the opening set: the target that changes the set decides, a closed target does + /// not, another campaign's target does not; a campaign closed answers none. + #[test] + fn roots_touched_repos_are_the_ones_in_force() { + let mut mind = seeded(); + assert_eq!(touched(&mind)[CAMPAIGN].repos, [repo(REPO)], "no target names repos: the opening set"); + + committed(admit(&mut mind, repo_revision(CAMPAIGN, 2, &[REPO, OTHER_REPO]))); + assert_eq!(touched(&mind)[CAMPAIGN].repos, [repo(REPO), repo(OTHER_REPO)], "a target that changes the set decides"); + + let D::Campaign(mut other) = campaign(&[REPO]) else { panic!("a campaign") }; + other.slug = "other-campaign".into(); + let mut batch = vec![D::Campaign(other)]; + batch.extend(repo_revision("other-campaign", 1, &["GameCult/Third"])); + committed(admit(&mut mind, batch)); + let both = touched(&mind); + assert_eq!(both["other-campaign"].repos, [repo("GameCult/Third")]); + assert_eq!(both[CAMPAIGN].repos, [repo(REPO), repo(OTHER_REPO)], "another campaign's higher target decides nothing here"); + + committed(admit(&mut mind, repo_revision(CAMPAIGN, 3, &[OTHER_REPO]))); + assert_eq!(touched(&mind)[CAMPAIGN].repos, [repo(OTHER_REPO)], "neither the opening set nor the closed revision decides"); + } + + /// A campaign with a resolution in force answers no repos, and one beside it that is open keeps its own. Admission has no row to + /// close a campaign, so the image is built by hand. + #[test] + fn a_closed_campaign_answers_no_repos() { + let held = |kind: K, key: String, document: D| Held { kind, key, document }; + let (open_key, closed_key, closing_key) = ("open:campaign:open".to_string(), "closed:campaign:closed".to_string(), "closed:resolution:campaign.n1".to_string()); + let named = |slug: &str, repo_name: &str| { + let D::Campaign(mut c) = campaign(&[repo_name]) else { panic!("a campaign") }; + c.slug = slug.into(); + D::Campaign(c) + }; + let image = vec![ + held(K::Campaign, open_key.clone(), named("open", REPO)), + held(K::Campaign, closed_key.clone(), named("closed", OTHER_REPO)), + held(K::Resolution, closing_key.clone(), resolution(r(K::Campaign, &closed_key), withdrawn())), + ]; + let landed = |ordinal: u64| AdmissionFacts { + receipt_id: format!("receipt-{ordinal}"), + admitted_at: "2026-09-16T12:00:00Z".into(), + provenance: provenance(Faculty::Hands), + ordinal, + }; + let facts = AdmissionIndex(BTreeMap::from([ + ((K::Campaign.type_id().to_string(), open_key), landed(1)), + ((K::Campaign.type_id().to_string(), closed_key), landed(2)), + ((K::Resolution.type_id().to_string(), closing_key), landed(3)), + ])); + let roots = Reader { docs: Docs { image, batch: Vec::new() }, facts }.roots_touched(); + let by_root: BTreeMap<&str, &RootTouched> = roots.iter().map(|root| (root.root.0.as_str(), root)).collect(); + assert_eq!(by_root["open"].repos, [repo(REPO)]); + assert!(by_root["closed"].repos.is_empty(), "{roots:?}"); + assert_eq!(by_root["closed"].ordinal, 3, "the closure is a touch of the campaign it closes"); + } } diff --git a/crates/huginn-mind/src/wire.rs b/crates/huginn-mind/src/wire.rs index c4415b4..4f438ab 100644 --- a/crates/huginn-mind/src/wire.rs +++ b/crates/huginn-mind/src/wire.rs @@ -14,7 +14,7 @@ use serde_json::Value; use crate::admission::{PipelineAdmissionBatch, PipelineAdmissionOutcome}; use crate::mind::Mind; use crate::persona::{PersonaPutOutcome, PersonaStateView}; -use crate::query::{PipelineDocumentView, PipelineSelectionPage, SemanticQuery}; +use crate::query::{PipelineDocumentView, PipelineSelectionPage, RootTouched, SemanticQuery}; use crate::receipt::{HuginnCommitReceipt, PipelineProvenance}; use crate::refusal::MindRefusal; use crate::store::MindStore; @@ -98,6 +98,9 @@ pub enum HuginnMindRequest { /// in time. The repos are the asker's to name (her stewardships); the /// instance's own login is dropped from the answer. StewardedActivity { instance: Slug, repos: Vec, after: Option }, + /// Every root the mind holds a document under, with its newest admission and a campaign's repos in force: the mind's own + /// records, one pass at the head. + RootsTouched { instance: Slug }, } /// The most repos one activity read names. @@ -250,6 +253,7 @@ impl HuginnMindRequest { Self::PersonaPut { .. } => "persona_put", Self::SearchSources { .. } => "search_sources", Self::StewardedActivity { .. } => "stewarded_activity", + Self::RootsTouched { .. } => "roots_touched", } } @@ -263,7 +267,8 @@ impl HuginnMindRequest { | Self::PersonaGet { instance } | Self::PersonaPut { instance, .. } | Self::SearchSources { instance, .. } - | Self::StewardedActivity { instance, .. } => Some(instance), + | Self::StewardedActivity { instance, .. } + | Self::RootsTouched { instance } => Some(instance), } } } @@ -282,6 +287,7 @@ pub enum HuginnMindResponse { PersonaPut(PersonaPutOutcome), Sources(SourceHits), Activity(RepoActivity), + RootsTouched(Vec), /// The answer did not fit one send and is being held for the client to /// fetch. Nothing in a mind produces this: the daemon's serve module does, /// in place of an answer, and the answer it stands for is the named @@ -417,6 +423,7 @@ mod tests { instance: slug(INSTANCE), query: SourceQuery { text: Line("reconcile".into()), top_k: 5, repo: Some(Short("Huginn".into())) }, }, + HuginnMindRequest::RootsTouched { instance: slug(INSTANCE) }, ] } @@ -466,6 +473,7 @@ mod tests { text: "x".into(), }], }), + HuginnMindResponse::RootsTouched(seeded().roots_touched().unwrap()), HuginnMindResponse::Deferred(DeferredAnswer { manifest: cultnet_rs::pack_content("huginn.mind_response", "package", "", "", "", b"body", 4) .unwrap() @@ -480,7 +488,7 @@ mod tests { #[test] fn the_wire_vocabulary_is_the_minds_methods_and_status_is_derived_from_the_response() { let names: Vec<&str> = requests().iter().map(|request| request.operation()).collect(); - assert_eq!(names, ["whoami", "admit", "view", "query", "persona_get", "persona_put", "search_sources"]); + assert_eq!(names, ["whoami", "admit", "view", "query", "persona_get", "persona_put", "search_sources", "roots_touched"]); for request in requests() { let declared = request.instance().is_none(); assert_eq!(declared, matches!(request, HuginnMindRequest::Whoami), "{request:?}"); diff --git a/crates/mind-body/src/queue.rs b/crates/mind-body/src/queue.rs index edf988d..8533ed3 100644 --- a/crates/mind-body/src/queue.rs +++ b/crates/mind-body/src/queue.rs @@ -17,7 +17,7 @@ use eureka_state::{ClientError, HuginnClient}; use huginn_mind::{ Faculty, HuginnMindRequest, HuginnMindResponse, PipelineAdmissionBatch, PipelineAdmissionOutcome, PipelineDocumentSummary, PipelineFacts, PipelineProvenance, - PipelineSelectionPage, PipelineStatusSummary, RepoActivity, SemanticQuery, + PipelineSelectionPage, PipelineStatusSummary, RepoActivity, RootTouched, SemanticQuery, }; /// The last `CONSECUTIVE` ended runs over an item, all empty, trip its breaker. @@ -46,6 +46,11 @@ pub trait MindPort { fn stewarded_activity(&self, _repos: &[OrgRepo], _after: Option) -> Result { bail!("this mind port has no forge to read") } + /// Every root her mind holds a document under, with its newest admission and a campaign's repos in force. A port that does not + /// read a mind refuses. + fn roots_touched(&self) -> Result> { + bail!("this mind port has no roots to read") + } /// Admits `documents` as one batch, attributed to `agent` in `session`. fn admit(&self, agent: &str, session: &str, documents: Vec) -> Result; } @@ -104,6 +109,15 @@ impl MindPort for HuginnMind<'_> { } } + fn roots_touched(&self) -> Result> { + let request = HuginnMindRequest::RootsTouched { instance: self.instance().clone() }; + match (self.call)(request).map_err(|error| anyhow!("the mind could not be asked: {}", client_error_code(&error)))? { + HuginnMindResponse::RootsTouched(roots) => Ok(roots), + HuginnMindResponse::Refused(_) => bail!("the mind refused the roots read"), + _ => bail!("the mind answered a roots read with something else"), + } + } + fn admit(&self, agent: &str, session: &str, documents: Vec) -> Result { let batch = PipelineAdmissionBatch { instance: self.instance().clone(), @@ -575,6 +589,38 @@ mod tests { assert!(format!("{:#}", port.stewarded_activity(&repos, None).unwrap_err()).contains("something else")); } + /// The roots ask goes out as the one `RootsTouched` request naming her instance, a real mind's answer comes back whole through + /// `HuginnMind` itself, and a refusal, a client error or any other answer is an error that carries nothing of the refusal. + #[test] + fn a_roots_read_goes_through_the_port_as_one_request() { + let mind = TestMind::seeded(); + let seen = std::cell::RefCell::new(Vec::new()); + let broken = std::cell::Cell::new(0u8); + let port = HuginnMind::over(Slug(INSTANCE.into()), Box::new(|request| { + seen.borrow_mut().push(request.clone()); + match broken.get() { + 0 => Ok(mind.answer(request)), + 1 => Ok(HuginnMindResponse::Refused(huginn_mind::MindRefusal::Unavailable { detail: "canary-roots-detail".into() })), + 2 => Ok(HuginnMindResponse::View(None)), + _ => Err(ClientError::Unencodable { detail: "canary-detail".into() }), + } + })); + let roots = port.roots_touched().unwrap(); + assert_eq!(seen.borrow()[0], HuginnMindRequest::RootsTouched { instance: Slug(INSTANCE.into()) }); + let campaign = roots.iter().find(|root| root.root.0 == CAMPAIGN).expect("the seeded campaign is a root"); + assert_eq!(campaign.repos, [OrgRepo::from(REPO)], "the campaign's repos in force come back through the port"); + assert!(roots.iter().any(|root| root.root.0 == INSTANCE && root.repos.is_empty()), "{roots:?}"); + + broken.set(1); + let refused = port.roots_touched().unwrap_err(); + assert!(format!("{refused:#}").contains("refused the roots read") && !format!("{refused:#}").contains("canary"), "{refused:#}"); + broken.set(2); + assert!(format!("{:#}", port.roots_touched().unwrap_err()).contains("something else")); + broken.set(3); + let down = port.roots_touched().unwrap_err(); + assert!(format!("{down:#}").contains("could not be asked") && !format!("{down:#}").contains("canary"), "{down:#}"); + } + #[test] fn runs_of_another_item_are_not_this_items_runs() { diff --git a/crates/mind-body/src/testkit.rs b/crates/mind-body/src/testkit.rs index e5a3f11..6d39aa6 100644 --- a/crates/mind-body/src/testkit.rs +++ b/crates/mind-body/src/testkit.rs @@ -157,6 +157,10 @@ impl TestMind { Err(refusal) => HuginnMindResponse::Refused(refusal), }, HuginnMindRequest::Admit(batch) => HuginnMindResponse::Admit(self.mind.borrow_mut().admit(batch, self.now.get())), + HuginnMindRequest::RootsTouched { .. } => match self.mind.borrow().roots_touched() { + Ok(roots) => HuginnMindResponse::RootsTouched(roots), + Err(refusal) => HuginnMindResponse::Refused(refusal), + }, other => panic!("the tests ask nothing else: {other:?}"), } } @@ -207,6 +211,11 @@ impl MindPort for TestMind { Ok(huginn_mind::RepoActivity { head: after.unwrap_or(0), events: vec![], capped: vec![], open_prs: vec![], unseen: vec![], moved: vec![] }) } + /// The roots of this mind as it holds them: the daemon's answer from the same mind, never a stub. + fn roots_touched(&self) -> Result> { + self.mind.borrow().roots_touched().map_err(|_| anyhow!("refused")) + } + fn admit(&self, agent: &str, session: &str, documents: Vec) -> Result { self.now.set(self.now.get() + Duration::seconds(1)); let batch = PipelineAdmissionBatch { diff --git a/crates/mind-body/tests/public_surface.txt b/crates/mind-body/tests/public_surface.txt index acb6bc8..7570af6 100644 --- a/crates/mind-body/tests/public_surface.txt +++ b/crates/mind-body/tests/public_surface.txt @@ -210,7 +210,7 @@ mind_body::queue: impl HuginnMind < 'static > mind_body::queue: impl HuginnMind < 'static >: fn new (client : HuginnClient) -> Self mind_body::queue: impl MindPort for HuginnMind < '_ > mind_body::queue: impl< 'a > HuginnMind < 'a > -mind_body::queue: pub trait MindPort { fn instance (& self) -> & Slug ; fn query (& self , selection : & Selection) -> Result < PipelineSelectionPage > ; fn query_near (& self , _selection : & Selection , _text : & str , _top_k : u32) -> Result < PipelineSelectionPage > { bail ! ("this mind port has no semantic index") } fn stewarded_activity (& self , _repos : & [OrgRepo] , _after : Option < u64 >) -> Result < RepoActivity > { bail ! ("this mind port has no forge to read") } fn admit (& self , agent : & str , session : & str , documents : Vec < PipelineDocument >) -> Result < PipelineAdmissionOutcome > ; } +mind_body::queue: pub trait MindPort { fn instance (& self) -> & Slug ; fn query (& self , selection : & Selection) -> Result < PipelineSelectionPage > ; fn query_near (& self , _selection : & Selection , _text : & str , _top_k : u32) -> Result < PipelineSelectionPage > { bail ! ("this mind port has no semantic index") } fn stewarded_activity (& self , _repos : & [OrgRepo] , _after : Option < u64 >) -> Result < RepoActivity > { bail ! ("this mind port has no forge to read") } fn roots_touched (& self) -> Result < Vec < RootTouched > > { bail ! ("this mind port has no roots to read") } fn admit (& self , agent : & str , session : & str , documents : Vec < PipelineDocument >) -> Result < PipelineAdmissionOutcome > ; } mind_body::queue: struct Held # [derive (Clone , Debug , PartialEq , Eq)] pub-fields[] mind_body::queue: struct HuginnMind< 'a > pub-fields[] mind_body::recall: impl Lane diff --git a/schemas/cultnet/huginn.mind_request.v1.schema.json b/schemas/cultnet/huginn.mind_request.v1.schema.json index f7a69cf..2c6a291 100644 --- a/schemas/cultnet/huginn.mind_request.v1.schema.json +++ b/schemas/cultnet/huginn.mind_request.v1.schema.json @@ -193,6 +193,27 @@ "required": [ "StewardedActivity" ] + }, + { + "description": "Every root the mind holds a document under, with its newest admission and a campaign's repos in force: the mind's own\nrecords, one pass at the head.", + "type": "object", + "properties": { + "RootsTouched": { + "type": "object", + "properties": { + "instance": { + "$ref": "#/$defs/Slug" + } + }, + "required": [ + "instance" + ] + } + }, + "additionalProperties": false, + "required": [ + "RootsTouched" + ] } ], "$defs": { diff --git a/schemas/cultnet/huginn.mind_response.v1.schema.json b/schemas/cultnet/huginn.mind_response.v1.schema.json index 45cace8..b16ac9d 100644 --- a/schemas/cultnet/huginn.mind_response.v1.schema.json +++ b/schemas/cultnet/huginn.mind_response.v1.schema.json @@ -125,6 +125,21 @@ "Activity" ] }, + { + "type": "object", + "properties": { + "RootsTouched": { + "type": "array", + "items": { + "$ref": "#/$defs/RootTouched" + } + } + }, + "additionalProperties": false, + "required": [ + "RootsTouched" + ] + }, { "description": "The answer did not fit one send and is being held for the client to\nfetch. Nothing in a mind produces this: the daemon's serve module does,\nin place of an answer, and the answer it stands for is the named\nMessagePack of the `HuginnMindResponse` the mind gave.", "type": "object", @@ -4652,6 +4667,35 @@ } ] }, + "RootTouched": { + "description": "A root of the mind and the newest admission under it. `repos` is the repo set in force of a root that is an in-force campaign,\nand empty for any other root.", + "type": "object", + "properties": { + "admitted_at": { + "type": "string" + }, + "ordinal": { + "type": "integer", + "format": "uint64", + "minimum": 1 + }, + "repos": { + "type": "array", + "items": { + "$ref": "#/$defs/OrgRepo" + } + }, + "root": { + "$ref": "#/$defs/Slug" + } + }, + "required": [ + "root", + "admitted_at", + "ordinal", + "repos" + ] + }, "RulingAuthority": { "type": "string", "enum": [ From d620918cbc0ac908c3a43c737fe730425ba386cb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 3 Nov 2026 05:19:45 +0100 Subject: [PATCH 3/9] wear-facts: a first line shows the specs with no report, open Soul findings and quiet campaigns her own mind holds, inside the one wear bound Co-Authored-By: Claude Opus 5.5 --- crates/mind-body/src/persona.rs | 217 ++++++++++++- crates/mind-body/src/persona/tests.rs | 421 +++++++++++++++++++++++++- crates/mind-body/src/testkit.rs | 16 +- 3 files changed, 640 insertions(+), 14 deletions(-) diff --git a/crates/mind-body/src/persona.rs b/crates/mind-body/src/persona.rs index 012b30e..515452a 100644 --- a/crates/mind-body/src/persona.rs +++ b/crates/mind-body/src/persona.rs @@ -54,13 +54,14 @@ use std::path::{Path, PathBuf}; use anyhow::{Result, anyhow, bail}; use chrono::{DateTime, Utc}; use cultcache_rs::{CultCache, DatabaseEntry, SingleFileMessagePackBackingStore}; -use cultnet_rs::Selection; +use cultnet_rs::{Incoming, Selection}; use eureka_pipeline::{ Date, Label, Line, MemoryOrigin, MemorySort, OrgRepo, PipelineDocument, PipelineFollowUp, PipelineKind, PipelineMemory, PipelinePulse, PipelineRef, PipelineResolution, ResolutionOutcome, RunOperator, RunTurn, SALIENCE_MAX_PERMILLE, Short, Slug, TEMPO_VOTE_MAX_S, TEMPO_VOTE_MIN_S, URGENCY_MAX_PERMILLE, pipeline_key, }; use eureka_state::HuginnClient; -use huginn_mind::{HuginnMindResponse, PipelineAdmissionOutcome, PipelineFacts, PipelinePageItems, PipelineStatusSummary, PrAction, RepoActivity, RepoChange, RepoEvent}; +use eureka_state::hold::BLOCKING; +use huginn_mind::{HuginnMindResponse, PipelineAdmissionOutcome, PipelineFacts, PipelinePageItems, PipelineStatusSummary, PrAction, RepoActivity, RepoChange, RepoEvent, RootTouched}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use uuid::Uuid; @@ -1059,7 +1060,8 @@ impl PersonaInbox<'_, C, S> { let names = sender_names(deliveries); let goals = render_responsibilities(organs.mind, &instance, &names)?; self.repos = goals.repos.clone(); - let wear = self.first_repo_read(); + let (wear_at, wear_facts) = self.first_repo_read(&goals.open_roots); + let wear = render_wear(wear_at, &wear_facts); let senders = other_senders(self.config, deliveries); let cue = recall::Cue { senders, texts: deliveries.iter().rev().map(|delivery| delivery.text.as_str()).collect(), names: names.clone(), listed: &goals.listed, now: organs.clock.now() }; let recalled = recall::select(organs.mind, &cue)?; @@ -1114,17 +1116,19 @@ impl PersonaInbox<'_, C, S> { } } - /// The first line's read of the forge for her stewarded repos: what is open, what the forge cannot show, what others changed in the - /// last three days, as the wear section, and the cursor the later reads continue from. A read that fails leaves the cursor unset, - /// puts one line in the section saying repo facts are missing, and never fails the session; a Mind with no repos asks nothing. - fn first_repo_read(&mut self) -> String { + /// The first line's reads for her stewarded repos, as the wear section's facts and the instant they were read at: what is open, + /// what the forge cannot show, what others changed in the last three days, the cursor the later reads continue from, then what + /// her own mind holds of her machine (`record_wear_facts`). A read that fails leaves the cursor unset, puts one line in the + /// section saying repo facts are missing, and never fails the session; a Mind with no repos asks nothing, of the forge or of her + /// mind. + fn first_repo_read(&mut self, open_roots: &[String]) -> (u64, Vec) { if self.repos.is_empty() { - return String::new(); + return (0, Vec::new()); } let organs = self.organs; let now = unix_ms(organs.clock.now()); self.repo_read_due_ms = now + REPO_READ_EVERY_S * 1000; - let facts = match organs.mind.stewarded_activity(&self.repos, None) { + let mut facts = match organs.mind.stewarded_activity(&self.repos, None) { Ok(activity) => { self.repo_head = Some(activity.head); wear_facts(&activity, &self.repos, organs.mind.instance(), now) @@ -1134,7 +1138,8 @@ impl PersonaInbox<'_, C, S> { vec![WearFact::Missing] } }; - render_wear(now, &facts) + facts.extend(record_wear_facts(organs.mind, &self.repos, open_roots, now)); + (now, facts) } /// Her stewarded repos changed by someone else become one notice, at most once in `REPO_READ_EVERY_S`. A read that fails keeps the cursor @@ -1851,6 +1856,14 @@ fn render_responsibilities(mind: &dyn MindPort, instance: &Slug, names: &BTreeMa let open = Selection { descending: true, ..in_force(of_kind(PipelineKind::FollowUp)) }; let follow_ups = headers(mind, &open)?; let mut dropped = 0; + // 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 = Vec::new(); + for follow_up in &follow_ups { + let campaign = follow_up.id.id.0.split_once(":follow_up:").map_or("", |(campaign, _)| campaign); + if !campaign.is_empty() && !open_roots.iter().any(|root| root == campaign) { + open_roots.push(campaign.to_owned()); + } + } if !follow_ups.is_empty() { out.push_str("Open follow-ups, newest first:\n"); } @@ -1901,7 +1914,7 @@ fn render_responsibilities(mind: &dyn MindPort, instance: &Slug, names: &BTreeMa if dropped_rulings > 0 { out.push_str(&format!("({dropped_rulings} more cited rulings not shown.)\n")); } - Ok(Responsibilities { text: out, listed, open: open_ids, people: open_people, repos }) + Ok(Responsibilities { text: out, listed, open: open_ids, people: open_people, repos, open_roots }) } /// The responsibilities section, and the ids of the follow-ups and rulings it lists, which recall @@ -1915,6 +1928,8 @@ struct Responsibilities { people: Vec, /// Her stewardships in force, by repo: the only map from her mind to the repos the forge is asked about. repos: Vec, + /// The campaigns that hold a follow-up in force in her mind, whether or not the bound showed its line. + open_roots: Vec, } /// Her latest runs with how each ended, one entry each, newest first. A Self @@ -2166,6 +2181,11 @@ fn repo_notice_line(activity: &RepoActivity, at_unix_ms: u64) -> Option /// The most the wear section may take of a first line. const WEAR_MAX_BYTES: usize = 1536; +/// A campaign of hers whose newest admission in her mind is this old or older, and which holds an unreported spec, an open Soul +/// finding or an open follow-up, is listed as quiet. It decides only what is listed: nothing gates or scores on it. +const STALE_AFTER_DAYS: u64 = 14; +/// The most entries one listing wear fact names; the rest are counted. +const WEAR_LIST_MAX: usize = 4; /// A fact about her machine from the forge. #[derive(Clone, Debug, PartialEq, Eq)] @@ -2174,22 +2194,45 @@ enum WearFact { Missing, /// The read stopped at a repo's page limit: the older entries there were not read. Capped(String), + /// A read of her own mind did not answer; `what` says which facts that leaves absent, not zero. + RecordsMissing(String), Awaiting { repo: String, number: u32, author: String, at: u64 }, /// A repo the anonymous read cannot see, and the repo the forge's redirect names for it when it names one. Unseen { repo: String, redirect: Option }, Changed { repo: String, words: String, at: u64 }, Yours { repo: String, number: u32, at: u64 }, + /// Specs in force with no report, on repos she stewards: `/.r` and the revision's admission, oldest first. + SpecsOnYours(Vec<(String, u64)>), + /// The same on repos she does not steward: how many, and the oldest admission. + SpecsElsewhere { count: usize, oldest: u64 }, + /// Open Soul findings in her mind, by campaign of her slice, largest first. + SoulFindings(Vec), + /// Campaigns of her slice with open work and no admission in her mind for `STALE_AFTER_DAYS`, by newest admission, oldest first. + QuietCampaigns(Vec<(String, u64)>), +} + +/// One campaign's open Soul findings in her mind: how many, how many block, and the oldest admission. +#[derive(Clone, Debug, PartialEq, Eq)] +struct FindingRow { + campaign: String, + open: usize, + blocking: usize, + oldest: u64, } impl WearFact { /// The priority order a full section drops from the end of. fn rank(&self) -> u8 { match self { - Self::Missing | Self::Capped(_) => 0, + Self::Missing | Self::RecordsMissing(_) | Self::Capped(_) => 0, Self::Awaiting { .. } => 1, Self::Unseen { .. } => 2, Self::Changed { .. } => 3, Self::Yours { .. } => 4, + Self::SpecsOnYours(_) => 5, + Self::SpecsElsewhere { .. } => 6, + Self::SoulFindings(_) => 7, + Self::QuietCampaigns(_) => 8, } } } @@ -2232,6 +2275,133 @@ fn wear_facts(activity: &RepoActivity, repos: &[OrgRepo], instance: &Slug, now_m facts } +/// The first `WEAR_LIST_MAX` entries joined by `sep`, then what `more` says of the rest, given how many are left and where they begin. +fn listed(entries: Vec, sep: &str, more: impl Fn(usize, usize) -> String) -> String { + let left = entries.len().saturating_sub(WEAR_LIST_MAX); + let mut text = entries.into_iter().take(WEAR_LIST_MAX).collect::>().join(sep); + if left > 0 { + text.push_str(sep); + text.push_str(&more(left, WEAR_LIST_MAX)); + } + text +} + +/// A spec in force with no report: its campaign and cut as one label, its repo, and the revision's admission. +struct UnreportedSpec { + label: String, + campaign: String, + repo: String, + at: u64, +} + +/// An instant of the mind's own records, in unix milliseconds; none when it is not one, so a record that cannot be dated makes its +/// read missing and never age 0. +fn instant_ms(text: &str) -> Option { + u64::try_from(DateTime::parse_from_rfc3339(text).ok()?.timestamp_millis()).ok() +} + +/// The campaign (the root of the id) a record of her mind sits under, as a plain token. +fn root_label(id: &PipelineRef) -> String { + plain(id.id.0.split(':').next().unwrap_or("")) +} + +/// Her slice: the roots whose repos in force meet hers (compared by identity, the forge's spelling being case-blind), each with its +/// newest admission. None when one cannot be dated. +fn slice_of(roots: &[RootTouched], hers: &[String]) -> Option> { + roots + .iter() + .filter(|root| root.repos.iter().any(|repo| hers.contains(&repo.identity()))) + .map(|root| Some((root.root.0.clone(), instant_ms(&root.admitted_at)?))) + .collect() +} + +/// Every spec in force whose cut has no report, from every page. +fn unreported_specs(mind: &dyn MindPort) -> Result> { + let selection = Selection { cited: Some(Incoming { role: "cut_spec".into(), exists: false }), ..in_force(of_kind(PipelineKind::CutSpec)) }; + let mut specs = Vec::new(); + for found in headers(mind, &selection)? { + let PipelineFacts::CutSpec { cut, revision, repo, .. } = &found.facts else { continue }; + let at = instant_ms(&found.admission.admitted_at).ok_or_else(|| anyhow!("a spec's admission has no instant"))?; + let campaign = root_label(&found.id); + specs.push(UnreportedSpec { label: format!("{campaign}/{}.r{revision}", plain(&cut.0)), campaign, repo: repo.identity(), at }); + } + Ok(specs) +} + +/// The Soul findings in force under the roots of her slice, counted by campaign, largest first, from every page. +fn open_soul_findings(mind: &dyn MindPort, slice: &[(String, u64)]) -> Result> { + let roots: Vec<&str> = slice.iter().map(|(root, _)| root.as_str()).collect(); + let selection = any_of(any_of(in_force(of_kind(PipelineKind::Finding)), "faculty", &["Soul"]), "root", &roots); + let mut rows: BTreeMap = BTreeMap::new(); + for found in headers(mind, &selection)? { + let PipelineFacts::Finding { severity, .. } = &found.facts else { continue }; + let at = instant_ms(&found.admission.admitted_at).ok_or_else(|| anyhow!("a finding's admission has no instant"))?; + let campaign = root_label(&found.id); + let row = rows.entry(campaign.clone()).or_insert(FindingRow { campaign, open: 0, blocking: 0, oldest: at }); + row.open += 1; + row.blocking += usize::from(BLOCKING.contains(&format!("{severity:?}").as_str())); + row.oldest = row.oldest.min(at); + } + let mut rows: Vec = rows.into_values().collect(); + rows.sort_by(|a, b| b.open.cmp(&a.open).then_with(|| a.campaign.cmp(&b.campaign))); + Ok(rows) +} + +/// The facts her own mind holds about her machine, derived at the first line and written nowhere: her unreported specs (those on +/// her repos named, the rest counted), the open Soul findings of her slice, and the campaigns of her slice that have gone quiet. +/// Each read walks every page. A read that fails says so in a fact of its own, and so does every fact that needed it; a quiet +/// campaign is listed only when all three reads answered. +fn record_wear_facts(mind: &dyn MindPort, repos: &[OrgRepo], open_roots: &[String], now_ms: u64) -> Vec { + let hers: Vec = repos.iter().map(OrgRepo::identity).collect(); + let slice = mind.roots_touched().ok().and_then(|roots| slice_of(&roots, &hers)); + let specs = unreported_specs(mind).ok(); + let findings = match &slice { + None => None, + Some(slice) if slice.is_empty() => Some(Vec::new()), + Some(slice) => open_soul_findings(mind, slice).ok(), + }; + let mut facts = Vec::new(); + let missing = |what: &str| WearFact::RecordsMissing(what.to_owned()); + if slice.is_none() { + facts.push(missing("the campaigns on your repos could not be read, so open Soul findings and quiet campaigns are absent")); + } + if specs.is_none() { + facts.push(missing("specs with no report could not be read, so they and quiet campaigns are absent")); + } + if slice.is_some() && findings.is_none() { + facts.push(missing("open Soul findings could not be read, so they and quiet campaigns are absent")); + } + if let Some(specs) = &specs { + let (mut mine, elsewhere): (Vec<&UnreportedSpec>, Vec<&UnreportedSpec>) = specs.iter().partition(|spec| hers.contains(&spec.repo)); + mine.sort_by_key(|spec| spec.at); + if !mine.is_empty() { + facts.push(WearFact::SpecsOnYours(mine.iter().map(|spec| (spec.label.clone(), spec.at)).collect())); + } + if let Some(oldest) = elsewhere.iter().map(|spec| spec.at).min() { + facts.push(WearFact::SpecsElsewhere { count: elsewhere.len(), oldest }); + } + } + if let Some(rows) = findings.as_ref().filter(|rows| !rows.is_empty()) { + facts.push(WearFact::SoulFindings(rows.clone())); + } + if let (Some(slice), Some(specs), Some(findings)) = (&slice, &specs, &findings) { + let mut quiet: Vec<(String, u64)> = slice + .iter() + .filter(|(root, newest)| { + let campaign = plain(root); + let open = specs.iter().any(|spec| spec.campaign == campaign) || findings.iter().any(|row| row.campaign == campaign) || open_roots.contains(root); + open && now_ms.saturating_sub(*newest) >= STALE_AFTER_DAYS * 86_400_000 + }) + .map(|(root, newest)| (plain(root), *newest)) + .collect(); + quiet.sort_by_key(|(_, newest)| *newest); + if !quiet.is_empty() { + facts.push(WearFact::QuietCampaigns(quiet)); + } + } + facts +} + /// `(N h)` under a day, `(N d)` from a day on. fn age_text(now_ms: u64, at_ms: u64) -> String { let hours = now_ms.saturating_sub(at_ms) / 3_600_000; @@ -2257,6 +2427,29 @@ fn render_wear(now_ms: u64, facts: &[WearFact]) -> String { WearFact::Unseen { repo, redirect: Some(to) } => format!("- a repo the forge read cannot see: {repo} (the forge redirects it to {to})\n"), WearFact::Changed { repo, words, at } => format!("- changed by others: {repo}: {words} {}\n", age_text(now_ms, *at)), WearFact::Yours { repo, number, at } => format!("- your open PR: {repo}#{number} {}\n", age_text(now_ms, *at)), + WearFact::RecordsMissing(what) => format!("- records are missing: {what}.\n"), + WearFact::SpecsOnYours(specs) => { + let named = specs.iter().map(|(label, at)| format!("{label} {}", age_text(now_ms, *at))).collect(); + format!("- specs with no report in your mind, on your repos: {}\n", listed(named, ", ", |left, _| format!("+{left} more"))) + } + WearFact::SpecsElsewhere { count, oldest } => { + format!("- specs with no report in your mind, on repos you do not steward: {count}, oldest {}\n", age_text(now_ms, *oldest)) + } + WearFact::SoulFindings(rows) => { + let named = rows.iter().map(|row| format!("{} {}, {} blocking, oldest {}", row.campaign, row.open, row.blocking, age_text(now_ms, row.oldest))).collect(); + let more = |left: usize, from: usize| { + let held: usize = rows[from..].iter().map(|row| row.open).sum(); + format!("+{held} more in {left} {}", if left == 1 { "campaign" } else { "campaigns" }) + }; + format!("- open Soul findings in your mind: {}\n", listed(named, "; ", more)) + } + WearFact::QuietCampaigns(quiet) => { + let named = quiet.iter().map(|(campaign, at)| format!("{campaign} {}", age_text(now_ms, *at))).collect(); + format!( + "- campaigns on your repos with open work and no admission in your mind for {STALE_AFTER_DAYS} d or more: {}\n", + listed(named, ", ", |left, _| format!("+{left} more")) + ) + } }) .collect(); // The tail line is "(N more facts not shown)"; keep room for it whenever a fact is dropped. diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 3d6dcb8..9b8f769 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -4553,11 +4553,17 @@ struct Forged<'a> { script: RefCell>>, asked: RefCell>>, head: Cell, + /// Every read of her mind, by the schema it names (`roots` for the roots read), in order. + reads: RefCell>, } impl<'a> Forged<'a> { fn over(mind: &'a TestMind, script: Vec>) -> Self { - Self { mind, script: RefCell::new(script.into()), asked: RefCell::default(), head: Cell::new(0) } + Self { mind, script: RefCell::new(script.into()), asked: RefCell::default(), head: Cell::new(0), reads: RefCell::default() } + } + + fn reads_of(&self, kind: K) -> usize { + self.reads.borrow().iter().filter(|read| read.as_str() == kind.type_id()).count() } } @@ -4567,9 +4573,15 @@ impl MindPort for Forged<'_> { } fn query(&self, selection: &cultnet_rs::Selection) -> Result { + self.reads.borrow_mut().extend(selection.schemas.iter().flatten().cloned()); self.mind.query(selection) } + fn roots_touched(&self) -> Result> { + self.reads.borrow_mut().push("roots".into()); + self.mind.roots_touched() + } + fn admit(&self, agent: &str, session: &str, documents: Vec) -> Result { self.mind.admit(agent, session, documents) } @@ -4959,3 +4971,410 @@ fn the_wear_bound_is_exact_to_the_byte() { assert!(over.len() <= WEAR_MAX_BYTES, "{} bytes: {over}", over.len()); assert!(over.trim_end().ends_with("more facts not shown)"), "{over}"); } + + +// ---- The wear her own mind holds: specs with no report, open Soul findings and quiet campaigns. ---- + +const DAY_MS: i64 = 86_400_000; + +fn t(days_ago: i64) -> u64 { + unix_ms(at(0)) - (days_ago * DAY_MS) as u64 +} + +fn pipeline_range() -> eureka_pipeline::CommitRange { + eureka_pipeline::CommitRange { base: eureka_pipeline::Sha("df7507b".into()), head: eureka_pipeline::Sha("df7507b".into()) } +} + +/// A spec in force whose cut has no report. +fn unreported_spec(campaign: &str, cut: &str, repo: &str) -> PipelineDocument { + let PipelineDocument::CutSpec(mut spec) = spec(cut) else { unreachable!("a spec") }; + spec.campaign = Slug(campaign.into()); + spec.repo = repo.into(); + PipelineDocument::CutSpec(spec) +} + +/// A spec with its report and a verdict that holds: the chain a Soul finding stands on. +fn chain(campaign: &str, cut: &str, repo: &str) -> Vec { + let PipelineDocument::CutReport(mut report) = report(cut) else { unreachable!("a report") }; + report.campaign = Slug(campaign.into()); + report.cut_spec = format!("{campaign}:cut_spec:cut-{cut}.r1").as_str().into(); + report.repo = repo.into(); + let verdict = eureka_pipeline::PipelineVerdict { + campaign: Slug(campaign.into()), + cut_report: format!("{campaign}:cut_report:cut-{cut}.h1").as_str().into(), + pass: 1, + range: pipeline_range(), + claims: vec![eureka_pipeline::VerdictClaim { + claim: "It holds.".into(), + outcome: eureka_pipeline::ClaimOutcome::Holds, + evidence: vec![evidence()], + findings: vec![], + promise: None, + mutations: vec![], + }], + }; + vec![unreported_spec(campaign, cut, repo), PipelineDocument::CutReport(report), PipelineDocument::Verdict(verdict)] +} + +fn evidence() -> eureka_pipeline::Evidence { + eureka_pipeline::Evidence { kind: eureka_pipeline::EvidenceKind::Test, locator: "cargo test".into(), result: "ok".into() } +} + +fn soul_finding(campaign: &str, cut: &str, label: &str, severity: eureka_pipeline::FindingSeverity) -> PipelineDocument { + PipelineDocument::Finding(eureka_pipeline::PipelineFinding { + campaign: Slug(campaign.into()), + verdict: format!("{campaign}:verdict:cut-{cut}.s1").as_str().into(), + label: label.into(), + range: pipeline_range(), + confidence: eureka_pipeline::FindingConfidence::Confirmed, + severity, + claim: "A claim.".into(), + invariants: vec![], + locations: vec![eureka_pipeline::CodeLocation { path: "a.rs".into(), line: 1, end_line: None, symbol: None }], + failure_scenario: "It fails.".into(), + evidence: vec![evidence()], + precedents: vec![], + origin: eureka_pipeline::FindingOrigin::Introduced, + }) +} + +/// Admits `documents` in batches the mind takes, the first at `first_days_ago` and the rest at `rest_days_ago`. +fn put_in_batches(mind: &TestMind, faculty: huginn_mind::Faculty, documents: Vec, first_days_ago: i64, rest_days_ago: i64) { + let mut first = true; + for batch in documents.chunks(60) { + mind.admit_at(at(-(if first { first_days_ago } else { rest_days_ago }) * DAY_MS)); + first = false; + let outcome = mind.put_as(faculty, batch.to_vec()); + assert!(matches!(outcome, AdmissionOutcome::Committed { .. }), "{outcome:?}"); + } +} + +#[derive(Clone, Copy)] +enum Fault { + Refused, + Client, +} + +/// Her mind as a daemon answers it, every read through the real adapter: the `nth` request (1-based) that names `schema` (`roots` +/// for the roots read) fails the way the daemon does, as a refusal, or the way a dead socket does, as a client error. Every read +/// is logged by the schema it names. The forge answers a quiet org. +fn through<'a>(mind: &'a TestMind, fail: Option<(String, usize, Fault)>, log: &'a RefCell>) -> crate::queue::HuginnMind<'a> { + let seen = Cell::new(0usize); + crate::queue::HuginnMind::over( + mind.instance().clone(), + Box::new(move |request| { + let which = match &request { + huginn_mind::HuginnMindRequest::RootsTouched { .. } => Some("roots".to_string()), + huginn_mind::HuginnMindRequest::Query { selection, .. } => selection.schemas.as_ref().and_then(|schemas| schemas.first().cloned()), + _ => None, + }; + if let Some(which) = &which { + log.borrow_mut().push(which.clone()); + } + if let (Some((name, nth, fault)), Some(which)) = (&fail, &which) + && which == name + { + seen.set(seen.get() + 1); + if seen.get() == *nth { + return match fault { + Fault::Refused => Ok(huginn_mind::HuginnMindResponse::Refused(huginn_mind::MindRefusal::Unavailable { detail: "canary".into() })), + Fault::Client => Err(eureka_state::ClientError::Unencodable { detail: "canary".into() }), + }; + } + } + if let huginn_mind::HuginnMindRequest::StewardedActivity { .. } = request { + return Ok(huginn_mind::HuginnMindResponse::Activity(RepoActivity { head: 1, events: vec![], capped: vec![], open_prs: vec![], unseen: vec![], moved: vec![] })); + } + Ok(mind.answer(request)) + }), + ) +} + +fn soul(mind: &TestMind, documents: Vec) { + let outcome = mind.put_as(huginn_mind::Faculty::Soul, documents); + assert!(matches!(outcome, AdmissionOutcome::Committed { .. }), "{outcome:?}"); +} + +fn hers() -> Vec { + vec![OrgRepo::from(REPO)] +} + +fn lines_of(wear: &str) -> Vec<&str> { + wear.lines().skip(1).collect() +} + +/// A spec on a repo she does not steward is only a count, a differently cased repo is hers, a finding outside her slice prints +/// nothing, and an empty slice makes no findings read. +#[test] +fn wear_lists_only_her_slice() { + let mind = TestMind::seeded(); + for (slug, repo) in [("elsewhere", "GameCult/Elsewhere"), ("cased", "gamecult/huginn")] { + mind.admit_at(at(-9 * DAY_MS)); + mind.committed(campaign_on(slug, &[repo])); + } + mind.admit_at(at(-3 * DAY_MS)); + mind.committed(vec![unreported_spec(CAMPAIGN, "mine", REPO)]); + mind.admit_at(at(-2 * DAY_MS)); + mind.committed(vec![unreported_spec("cased", "low", "gamecult/huginn")]); + mind.admit_at(at(-5 * DAY_MS)); + mind.committed(vec![unreported_spec("elsewhere", "theirs", "GameCult/Elsewhere")]); + for (campaign, repo) in [(CAMPAIGN, REPO), ("elsewhere", "GameCult/Elsewhere")] { + mind.admit_at(at(-6 * DAY_MS)); + mind.committed(chain(campaign, "f", repo)); + mind.admit_at(at(-4 * DAY_MS)); + soul(&mind, vec![soul_finding(campaign, "f", "one", eureka_pipeline::FindingSeverity::High)]); + } + let log = RefCell::default(); + let facts = record_wear_facts(&through(&mind, None, &log), &hers(), &[], t(0)); + assert_eq!( + facts, + [ + WearFact::SpecsOnYours(vec![("eureka-body/mine.r1".into(), t(3)), ("cased/low.r1".into(), t(2))]), + WearFact::SpecsElsewhere { count: 1, oldest: t(5) }, + WearFact::SoulFindings(vec![FindingRow { campaign: "eureka-body".into(), open: 1, blocking: 1, oldest: t(4) }]), + ] + ); + let wear = render_wear(t(0), &facts); + assert!(!wear.contains("elsewhere") && !wear.contains("Elsewhere"), "{wear}"); + + let none = TestMind::without_campaigns(); + let log = RefCell::default(); + assert_eq!(record_wear_facts(&through(&none, None, &log), &hers(), &[], t(0)), []); + assert_eq!(*log.borrow(), ["roots", K::CutSpec.type_id()], "an empty slice makes no findings read"); +} + +/// A mind with `specs` unreported specs in `eureka-body` and `findings` open Soul findings there, the first batch of each ten +/// days old and the rest one. +fn crowded(specs: usize, findings: usize) -> TestMind { + let mind = TestMind::seeded(); + put_in_batches(&mind, huginn_mind::Faculty::Hands, (0..specs).map(|n| unreported_spec(CAMPAIGN, &format!("s{n:03}"), REPO)).collect(), 9, 1); + mind.admit_at(at(-11 * DAY_MS)); + mind.committed(chain(CAMPAIGN, "v", REPO)); + let severity = |n: usize| if n % 4 == 0 { eureka_pipeline::FindingSeverity::Blocker } else { eureka_pipeline::FindingSeverity::Low }; + put_in_batches(&mind, huginn_mind::Faculty::Soul, (0..findings).map(|n| soul_finding(CAMPAIGN, "v", &format!("f{n:03}"), severity(n))).collect(), 10, 1); + mind +} + +/// A count crosses the page: 201 open findings print 201 with the oldest's age, and 201 unreported specs are all named by the fact +/// (the line prints four), not one page's worth. +#[test] +fn wear_counts_cross_a_page() { + let mind = crowded(201, 201); + let log = RefCell::default(); + let facts = record_wear_facts(&through(&mind, None, &log), &hers(), &[], t(0)); + let [WearFact::SpecsOnYours(specs), WearFact::SoulFindings(rows)] = facts.as_slice() else { panic!("{facts:?}") }; + assert_eq!((specs.len(), specs[0].1, specs[200].1), (201, t(9), t(1))); + assert_eq!(rows, &[FindingRow { campaign: "eureka-body".into(), open: 201, blocking: 51, oldest: t(10) }]); + assert!(log.borrow().iter().filter(|read| read.as_str() == K::Finding.type_id()).count() >= 2, "more than one page of findings was walked"); + assert!(log.borrow().iter().filter(|read| read.as_str() == K::CutSpec.type_id()).count() >= 2, "and of specs"); + let wear = render_wear(t(0), &facts); + assert!(wear.contains("- open Soul findings in your mind: eureka-body 201, 51 blocking, oldest (10 d)\n"), "{wear}"); +} + +/// A walk that fails on a later page fails that read whole, as a refusal or as a dead socket: its own missing line, no partial count, +/// the other reads' lines, and no quiet campaign from partial inputs. +#[test] +fn a_later_page_refused_fails_that_read_whole() { + let mind = crowded(201, 201); + for fault in [Fault::Refused, Fault::Client] { + let log = RefCell::default(); + let facts = record_wear_facts(&through(&mind, Some((K::Finding.type_id().into(), 2, fault)), &log), &hers(), &["eureka-body".into()], t(0) + 30 * DAY_MS as u64); + assert_eq!(facts.len(), 2, "{facts:?}"); + assert!(matches!(&facts[0], WearFact::RecordsMissing(what) if what.contains("open Soul findings could not be read")), "{facts:?}"); + assert!(matches!(&facts[1], WearFact::SpecsOnYours(specs) if specs.len() == 201), "the specs print whole: {facts:?}"); + + let log = RefCell::default(); + let facts = record_wear_facts(&through(&mind, Some((K::CutSpec.type_id().into(), 2, fault)), &log), &hers(), &[], t(0)); + assert_eq!(facts.len(), 2, "{facts:?}"); + assert!(matches!(&facts[0], WearFact::RecordsMissing(what) if what.contains("specs with no report could not be read")), "{facts:?}"); + assert!(matches!(&facts[1], WearFact::SoulFindings(rows) if rows[0].open == 201), "{facts:?}"); + } +} + +/// Each read in turn answers as the daemon does, as a refusal and as a client error: its own missing line prints, the others' +/// facts print, no quiet campaign comes of partial inputs, and a session still runs and shows it. +#[test] +fn a_failed_wear_read_says_so() { + let mind = TestMind::seeded(); + mind.admit_at(at(-30 * DAY_MS)); + mind.committed(vec![unreported_spec(CAMPAIGN, "old", REPO)]); + mind.committed(chain(CAMPAIGN, "f", REPO)); + soul(&mind, vec![soul_finding(CAMPAIGN, "f", "one", eureka_pipeline::FindingSeverity::Low)]); + let all = |facts: &[WearFact]| facts.iter().filter(|fact| matches!(fact, WearFact::QuietCampaigns(_))).count(); + let log = RefCell::default(); + let quiet_at = t(0) + DAY_MS as u64 * 60; + let whole = record_wear_facts(&through(&mind, None, &log), &hers(), &[], quiet_at); + assert_eq!(all(&whole), 1, "with every read answered the old campaign is quiet: {whole:?}"); + for fault in [Fault::Refused, Fault::Client] { + for (read, missing) in [ + ("roots", "campaigns on your repos could not be read"), + (K::CutSpec.type_id(), "specs with no report could not be read"), + (K::Finding.type_id(), "open Soul findings could not be read"), + ] { + let log = RefCell::default(); + let facts = record_wear_facts(&through(&mind, Some((read.into(), 1, fault)), &log), &hers(), &[], quiet_at); + assert!(facts.iter().any(|fact| matches!(fact, WearFact::RecordsMissing(what) if what.contains(missing))), "{read}: {facts:?}"); + assert_eq!(facts.len(), 2, "{read}: its missing line and the facts of the reads that answered: {facts:?}"); + assert_eq!(all(&facts), 0, "{read}: no quiet campaign from partial inputs"); + } + } + + let mut rig = stewarding_rig(2); + rig.mind = mind; + let log = RefCell::default(); + let broken = through(&rig.mind, Some(("roots".into(), 1, Fault::Client)), &log); + live_session(&rig, &broken, 1000, false, 1); + let input = input_of(&rig, 0); + assert!(input.contains("- records are missing: the campaigns on your repos could not be read"), "{input}"); + assert!(input.contains("- specs with no report in your mind, on your repos: eureka-body/old.r1"), "the session ran and the specs printed: {input}"); +} + +/// A campaign is quiet exactly at the window: 14 d lists, a millisecond less does not; it needs open work (an unreported spec, an +/// open Soul finding or an open follow-up) and a campaign with none is not listed. +#[test] +fn a_quiet_campaign_needs_open_work_and_the_full_window() { + let mind = TestMind::seeded(); + let opened = t(20); + for slug in ["q-spec", "q-find", "q-follow", "q-none"] { + mind.admit_at(at(-20 * DAY_MS)); + mind.committed(campaign_on(slug, &[REPO])); + } + mind.admit_at(at(-20 * DAY_MS)); + mind.committed(vec![unreported_spec("q-spec", "x", REPO)]); + mind.admit_at(at(-20 * DAY_MS)); + mind.committed(chain("q-find", "x", REPO)); + mind.admit_at(at(-20 * DAY_MS)); + soul(&mind, vec![soul_finding("q-find", "x", "one", eureka_pipeline::FindingSeverity::Low)]); + let log = RefCell::default(); + let port = through(&mind, None, &log); + let window = opened + 14 * DAY_MS as u64; + let quiet = |now: u64, open: &[String]| record_wear_facts(&port, &hers(), open, now).into_iter().find_map(|fact| if let WearFact::QuietCampaigns(q) = fact { Some(q) } else { None }); + let follow = ["q-follow".to_string(), "q-none-but-not-in-the-slice".to_string()]; + assert_eq!(quiet(window, &follow), Some(vec![("q-find".into(), opened), ("q-follow".into(), opened), ("q-spec".into(), opened)]), "exactly 14 d lists, and q-none has no open work"); + assert_eq!(quiet(window - 1, &follow), None, "a millisecond less lists none"); + assert_eq!(quiet(window + 1, &follow).map(|q| q.len()), Some(3)); + assert_eq!(quiet(window, &[]).map(|q| q.len()), Some(2), "without the follow-up q-follow has no open work"); +} + +/// A follow-up the goals bound hides still counts as open work: the roots come from the whole list. +#[test] +fn a_follow_up_the_goals_bound_hides_still_counts_as_open_work() { + let mind = TestMind::seeded(); + mind.committed(campaign_on("hidden-in", &[REPO])); + mind.committed(chain("hidden-in", "src", REPO)); + let PipelineDocument::FollowUp(mut hidden) = follow_up("old", &reference(K::CutSpec, "hidden-in:cut_spec:cut-src.r1")) else { unreachable!("a follow-up") }; + hidden.campaign = Slug("hidden-in".into()); + mind.committed(vec![PipelineDocument::FollowUp(hidden)]); + mind.committed(chain(CAMPAIGN, "src", REPO)); + for n in 0..12 { + let PipelineDocument::FollowUp(mut long) = follow_up(&format!("long{n}"), &spec_ref("src")) else { unreachable!("a follow-up") }; + long.item = Line("x".repeat(900)); + mind.committed(vec![PipelineDocument::FollowUp(long)]); + } + let goals = render_responsibilities(&mind, &Slug(INSTANCE.into()), &BTreeMap::new()).unwrap(); + assert!(goals.text.contains("older open follow-ups not shown") && !goals.text.contains("hidden-in/old"), "the bound hid it: {}", goals.text); + assert_eq!(goals.open_roots, ["eureka-body", "hidden-in"], "{:?}", goals.open_roots); +} + +/// The caps are exact: four entries print with no tail, five print four and `+1 more`, on all three listing lines, and the finding +/// line's `+N more in M campaigns` counts what it left out. +#[test] +fn wear_caps_are_exact() { + let now = t(0); + let entries = |n: usize| (0..n).map(|k| (format!("c{k}"), now - (k as u64 + 1) * DAY_MS as u64)).collect::>(); + let line = |fact: WearFact| render_wear(now, &[fact]).lines().nth(1).unwrap().to_string(); + for (make, head) in [ + (WearFact::SpecsOnYours as fn(Vec<(String, u64)>) -> WearFact, "- specs with no report in your mind, on your repos: "), + (WearFact::QuietCampaigns, "- campaigns on your repos with open work and no admission in your mind for 14 d or more: "), + ] { + assert_eq!(line(make(entries(4))), format!("{head}c0 (1 d), c1 (2 d), c2 (3 d), c3 (4 d)")); + assert_eq!(line(make(entries(5))), format!("{head}c0 (1 d), c1 (2 d), c2 (3 d), c3 (4 d), +1 more")); + assert_eq!(line(make(entries(7))), format!("{head}c0 (1 d), c1 (2 d), c2 (3 d), c3 (4 d), +3 more")); + } + let rows = |n: usize| (0..n).map(|k| FindingRow { campaign: format!("c{k}"), open: 10 - k, blocking: k, oldest: now - 3 * 3_600_000 }).collect::>(); + let named = "- open Soul findings in your mind: c0 10, 0 blocking, oldest (3 h); c1 9, 1 blocking, oldest (3 h); c2 8, 2 blocking, oldest (3 h); c3 7, 3 blocking, oldest (3 h)"; + assert_eq!(line(WearFact::SoulFindings(rows(4))), named); + assert_eq!(line(WearFact::SoulFindings(rows(5))), format!("{named}; +6 more in 1 campaign")); + assert_eq!(line(WearFact::SoulFindings(rows(7))), format!("{named}; +15 more in 3 campaigns")); + assert_eq!(line(WearFact::SpecsElsewhere { count: 9, oldest: now - 40 * DAY_MS as u64 }), "- specs with no report in your mind, on repos you do not steward: 9, oldest (40 d)"); +} + +/// Forge facts and record facts share the one bound: built to exactly the bound they all print with no tail; one byte more drops +/// from the end, the section stays inside the bound and the tail's count is exact; the forge's facts rank first. +#[test] +fn record_facts_share_the_one_bound() { + let now = t(0); + let forge = |n: usize| WearFact::Changed { repo: format!("GameCult/r{n:03}"), words: "1 commit pushed by nibu".into(), at: now - 3_600_000 }; + let record = |pad: usize| WearFact::QuietCampaigns(vec![(format!("q{}", "x".repeat(pad)), now - 15 * DAY_MS as u64)]); + let header = render_wear(now, &[forge(0)]).lines().next().unwrap().len() + 1; + let each = render_wear(now, &[forge(0)]).len() - header; + let record_len = |pad: usize| render_wear(now, &[record(pad)]).len() - header; + let count = (WEAR_MAX_BYTES - header - record_len(0)) / each; + let spare = WEAR_MAX_BYTES - header - record_len(0) - count * each; + let mut facts: Vec = vec![record(spare)]; + facts.extend((0..count).map(forge)); + let exact = render_wear(now, &facts); + assert_eq!(exact.len(), WEAR_MAX_BYTES, "built to the byte"); + assert!(!exact.contains("not shown") && exact.contains("campaigns on your repos with open work"), "all print: {exact}"); + assert!(exact.lines().nth(1).unwrap().starts_with("- changed by others"), "the forge's facts rank first: {exact}"); + + facts[0] = record(spare + 1); + let over = render_wear(now, &facts); + assert!(over.len() <= WEAR_MAX_BYTES, "{} bytes", over.len()); + assert!(!over.contains("campaigns on your repos with open work"), "the record fact drops from the end: {over}"); + let shown = over.lines().filter(|line| line.starts_with("- ")).count(); + assert!(over.trim_end().ends_with(&format!("({} more facts not shown)", facts.len() - shown)) && shown < facts.len(), "{over}"); + assert!(shown + 1 >= count, "forge facts are kept before it: {shown} of {count}"); +} + +/// A settled record leaves the next first line: resolving a finding and reporting a spec removes them, and the reads wrote nothing. +#[test] +fn a_settled_record_leaves_the_next_first_line() { + let mind = TestMind::seeded(); + mind.admit_at(at(-3 * DAY_MS)); + mind.committed(vec![unreported_spec(CAMPAIGN, "open", REPO)]); + mind.committed(chain(CAMPAIGN, "f", REPO)); + soul(&mind, vec![soul_finding(CAMPAIGN, "f", "one", eureka_pipeline::FindingSeverity::High)]); + let held = || [K::CutSpec, K::CutReport, K::Verdict, K::Finding, K::Resolution, K::Campaign].iter().map(|kind| mind.ids(*kind).len()).sum::(); + let log = RefCell::default(); + let before = held(); + let facts = record_wear_facts(&through(&mind, None, &log), &hers(), &[], t(0)); + assert_eq!(facts.len(), 2, "{facts:?}"); + assert_eq!(held(), before, "the reads wrote nothing"); + + let finding = mind.ids(K::Finding).remove(0); + mind.committed(vec![close(reference(K::Finding, &finding), recorded())]); + let PipelineDocument::CutReport(mut report) = report("open") else { unreachable!("a report") }; + report.cut_spec = id("cut_spec", "cut-open.r1").as_str().into(); + mind.committed(vec![PipelineDocument::CutReport(report)]); + assert_eq!(record_wear_facts(&through(&mind, None, &log), &hers(), &[], t(0)), [], "both are gone from the next first line"); +} + +/// One session over a mind that holds an unreported spec: how many roots, spec and Soul-finding reads it made, and its first line. +fn wear_reads(steward: bool, polls: u64, messages: bool) -> (usize, usize, usize, String) { + let rig = stewarding_rig(polls as usize + 5); + rig.mind.committed(vec![unreported_spec(CAMPAIGN, "open", REPO)]); + if !steward { + for stewardship in rig.mind.ids(K::Stewardship) { + rig.mind.committed(vec![close(reference(K::Stewardship, &stewardship), withdrawn())]); + } + } + let forged = Forged::over(&rig.mind, vec![activity(100, vec![])]); + live_session(&rig, &forged, 5_000, messages, polls); + let roots = forged.reads.borrow().iter().filter(|read| read.as_str() == "roots").count(); + (roots, forged.reads_of(K::CutSpec), forged.reads_of(K::Finding), input_of(&rig, 0)) +} + +/// Wear reads her mind on a first line and nowhere else: a session's start adds one roots read, one specs read and one findings +/// read to a Mind that stewards nothing, and a session of many polls and tells adds none beyond the first line's. +#[test] +fn wear_reads_only_on_a_first_line() { + let (roots, specs, findings, input) = wear_reads(true, 1, false); + let (none_roots, none_specs, none_findings, bare) = wear_reads(false, 1, false); + assert_eq!((roots, specs - none_specs, findings - none_findings), (1, 1, 1), "three reads on a start"); + assert_eq!(none_roots, 0, "a Mind with no stewardship makes none of them"); + assert!(input.contains("eureka-body/open.r1") && !bare.contains("Where your machine shows wear"), "{input} +{bare}"); + let long = wear_reads(true, 25, true); + assert_eq!((long.0, long.1, long.2), (roots, specs, findings), "polls and tells read nothing more"); +} diff --git a/crates/mind-body/src/testkit.rs b/crates/mind-body/src/testkit.rs index 6d39aa6..24f6c8f 100644 --- a/crates/mind-body/src/testkit.rs +++ b/crates/mind-body/src/testkit.rs @@ -135,10 +135,15 @@ impl TestMind { /// Admits as Hands, one second after the last admission. pub(crate) fn put(&self, documents: Vec) -> PipelineAdmissionOutcome { + self.put_as(Faculty::Hands, documents) + } + + /// Admits as `faculty`, one second after the last admission. + pub(crate) fn put_as(&self, faculty: Faculty, documents: Vec) -> PipelineAdmissionOutcome { self.now.set(self.now.get() + Duration::seconds(1)); let batch = PipelineAdmissionBatch { instance: self.instance.clone(), - provenance: PipelineProvenance { faculty: Faculty::Hands, agent: "test".into(), session: "test".into(), tool: "test".into() }, + provenance: PipelineProvenance { faculty, agent: "test".into(), session: "test".into(), tool: "test".into() }, documents, }; self.mind.borrow_mut().admit(batch, self.now.get()) @@ -304,6 +309,15 @@ pub(crate) fn campaign_named(slug: &str, title: &str) -> PipelineDocument { }) } +/// A campaign opened on `repos`, with its first target and the one invariant. +pub(crate) fn campaign_on(slug: &str, repos: &[&str]) -> Vec { + let PipelineDocument::Campaign(mut opened) = campaign_named(slug, slug) else { unreachable!("a campaign") }; + opened.repos = repos.iter().map(|repo| (*repo).into()).collect(); + let PipelineDocument::Target(mut first) = target() else { unreachable!("a target") }; + first.campaign = Slug(slug.into()); + vec![PipelineDocument::Campaign(opened), PipelineDocument::Target(first)] +} + fn target() -> PipelineDocument { PipelineDocument::Target(PipelineTarget { campaign: Slug(CAMPAIGN.into()), From ea70d3a05b15520e90dcdcc00ed17af9412d827b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 3 Nov 2026 05:22:46 +0100 Subject: [PATCH 4/9] wear-facts: the hidden follow-up in the goals-bound test is as long as the ones that fill the bound Co-Authored-By: Claude Opus 5.5 --- crates/mind-body/src/persona/tests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 9b8f769..20e4be7 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -5264,6 +5264,7 @@ fn a_follow_up_the_goals_bound_hides_still_counts_as_open_work() { mind.committed(chain("hidden-in", "src", REPO)); let PipelineDocument::FollowUp(mut hidden) = follow_up("old", &reference(K::CutSpec, "hidden-in:cut_spec:cut-src.r1")) else { unreachable!("a follow-up") }; hidden.campaign = Slug("hidden-in".into()); + hidden.item = Line("x".repeat(900)); mind.committed(vec![PipelineDocument::FollowUp(hidden)]); mind.committed(chain(CAMPAIGN, "src", REPO)); for n in 0..12 { @@ -5272,7 +5273,7 @@ fn a_follow_up_the_goals_bound_hides_still_counts_as_open_work() { mind.committed(vec![PipelineDocument::FollowUp(long)]); } let goals = render_responsibilities(&mind, &Slug(INSTANCE.into()), &BTreeMap::new()).unwrap(); - assert!(goals.text.contains("older open follow-ups not shown") && !goals.text.contains("hidden-in/old"), "the bound hid it: {}", goals.text); + assert!(goals.text.contains("older open follow-ups not shown") && !goals.text.contains("hidden-in/old"), "the bound hid it: {} bytes", goals.text.len()); assert_eq!(goals.open_roots, ["eureka-body", "hidden-in"], "{:?}", goals.open_roots); } From 0669ec6d96726e4b93b4860d41c5594209c0383b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 3 Nov 2026 05:23:07 +0100 Subject: [PATCH 5/9] wear-facts: a record with no instant makes its read missing, pinned for each of the three reads Co-Authored-By: Claude Opus 5.5 --- crates/mind-body/src/persona/tests.rs | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 20e4be7..50b6bb7 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -5379,3 +5379,43 @@ fn wear_reads_only_on_a_first_line() { let long = wear_reads(true, 25, true); assert_eq!((long.0, long.1, long.2), (roots, specs, findings), "polls and tells read nothing more"); } + +/// A record whose admission is no instant makes its read missing, never age 0: the roots read, the specs read and the findings read +/// each, with the others' facts still printed. +#[test] +fn an_undated_record_makes_its_read_missing() { + let mind = TestMind::seeded(); + mind.committed(vec![unreported_spec(CAMPAIGN, "open", REPO)]); + mind.committed(chain(CAMPAIGN, "f", REPO)); + soul(&mind, vec![soul_finding(CAMPAIGN, "f", "one", eureka_pipeline::FindingSeverity::Low)]); + for (spoiled, missing) in [ + ("roots", "campaigns on your repos could not be read"), + (K::CutSpec.type_id(), "specs with no report could not be read"), + (K::Finding.type_id(), "open Soul findings could not be read"), + ] { + let port = crate::queue::HuginnMind::over( + mind.instance().clone(), + Box::new(|request| { + let asked = match &request { + huginn_mind::HuginnMindRequest::Query { selection, .. } => selection.schemas.as_ref().and_then(|schemas| schemas.first().cloned()), + _ => None, + }; + let mut answer = mind.answer(request); + match &mut answer { + huginn_mind::HuginnMindResponse::RootsTouched(roots) if spoiled == "roots" => roots.iter_mut().for_each(|root| root.admitted_at = "yesterday".into()), + huginn_mind::HuginnMindResponse::Query(page) if asked.as_deref() == Some(spoiled) => { + if let huginn_mind::PipelinePageItems::Headers(found) = &mut page.items { + found.iter_mut().for_each(|header| header.admission.admitted_at = "yesterday".into()); + } + } + _ => {} + } + Ok(answer) + }), + ); + let facts = record_wear_facts(&port, &hers(), &[], t(0)); + assert!(facts.iter().any(|fact| matches!(fact, WearFact::RecordsMissing(what) if what.contains(missing))), "{spoiled}: {facts:?}"); + assert!(!facts.iter().any(|fact| matches!(fact, WearFact::QuietCampaigns(_))), "{spoiled}: {facts:?}"); + assert_eq!(facts.len(), 2, "{spoiled}: the missing line and the other read's facts: {facts:?}"); + } +} From cbb17b01a599526ad2a10a1d14ce32de20175fc2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 3 Nov 2026 06:18:05 +0100 Subject: [PATCH 6/9] wear-facts: every wear line leads to a record she can open; blocking campaigns sort first and the tail keeps their count Specs print by record id, the specs of other repos by campaign with the oldest one's id, a campaign's findings by the oldest blocking finding's id. Findings sort blocking, then open; the tail reads +N more, B blocking, in M campaigns. The quiet line says campaigns that touch your repos. Ids go through the same plain-token rule as forge names. Co-Authored-By: Claude Opus 5.5 --- crates/mind-body/src/persona.rs | 78 +++++++++---- crates/mind-body/src/persona/tests.rs | 162 +++++++++++++++++++++++--- 2 files changed, 204 insertions(+), 36 deletions(-) diff --git a/crates/mind-body/src/persona.rs b/crates/mind-body/src/persona.rs index 515452a..d4bc90d 100644 --- a/crates/mind-body/src/persona.rs +++ b/crates/mind-body/src/persona.rs @@ -2201,23 +2201,25 @@ enum WearFact { Unseen { repo: String, redirect: Option }, Changed { repo: String, words: String, at: u64 }, Yours { repo: String, number: u32, at: u64 }, - /// Specs in force with no report, on repos she stewards: `/.r` and the revision's admission, oldest first. + /// Specs in force with no report, on repos she stewards: each spec's record id and the revision's admission, oldest first. SpecsOnYours(Vec<(String, u64)>), - /// The same on repos she does not steward: how many, and the oldest admission. - SpecsElsewhere { count: usize, oldest: u64 }, - /// Open Soul findings in her mind, by campaign of her slice, largest first. + /// The same on repos she does not steward: how many in each campaign (largest first), and the oldest spec's record id and admission. + SpecsElsewhere { by_campaign: Vec<(String, usize)>, oldest: (String, u64) }, + /// Open Soul findings in her mind, by campaign of her slice, blocking first, then largest. SoulFindings(Vec), /// Campaigns of her slice with open work and no admission in her mind for `STALE_AFTER_DAYS`, by newest admission, oldest first. QuietCampaigns(Vec<(String, u64)>), } -/// One campaign's open Soul findings in her mind: how many, how many block, and the oldest admission. +/// One campaign's open Soul findings in her mind: how many, how many block, the oldest admission, and the oldest blocking finding's +/// record id and admission when one blocks. #[derive(Clone, Debug, PartialEq, Eq)] struct FindingRow { campaign: String, open: usize, blocking: usize, oldest: u64, + oldest_blocking: Option<(String, u64)>, } impl WearFact { @@ -2243,6 +2245,12 @@ fn plain(name: &str) -> String { name.chars().map(|c| if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | '/') { c } else { '?' }).collect() } +/// A record id as plain tokens: each `:`-separated part goes through `plain`, so an id read from a record cannot carry a line break +/// or a field into the section. +fn plain_id(id: &str) -> String { + id.split(':').map(plain).collect::>().join(":") +} + /// The facts a first read gives about her repos, in no order (the renderer orders them): PRs waiting on her, repos the forge cannot /// show, others' changes in the last three days, and PRs she has open. fn wear_facts(activity: &RepoActivity, repos: &[OrgRepo], instance: &Slug, now_ms: u64) -> Vec { @@ -2286,9 +2294,9 @@ fn listed(entries: Vec, sep: &str, more: impl Fn(usize, usize) -> String text } -/// A spec in force with no report: its campaign and cut as one label, its repo, and the revision's admission. +/// A spec in force with no report: its record id, its campaign, its repo, and the revision's admission. struct UnreportedSpec { - label: String, + id: String, campaign: String, repo: String, at: u64, @@ -2320,10 +2328,10 @@ fn unreported_specs(mind: &dyn MindPort) -> Result> { let selection = Selection { cited: Some(Incoming { role: "cut_spec".into(), exists: false }), ..in_force(of_kind(PipelineKind::CutSpec)) }; let mut specs = Vec::new(); for found in headers(mind, &selection)? { - let PipelineFacts::CutSpec { cut, revision, repo, .. } = &found.facts else { continue }; + let PipelineFacts::CutSpec { repo, .. } = &found.facts else { continue }; let at = instant_ms(&found.admission.admitted_at).ok_or_else(|| anyhow!("a spec's admission has no instant"))?; let campaign = root_label(&found.id); - specs.push(UnreportedSpec { label: format!("{campaign}/{}.r{revision}", plain(&cut.0)), campaign, repo: repo.identity(), at }); + specs.push(UnreportedSpec { id: plain_id(&found.id.id.0), campaign, repo: repo.identity(), at }); } Ok(specs) } @@ -2337,13 +2345,19 @@ fn open_soul_findings(mind: &dyn MindPort, slice: &[(String, u64)]) -> Result = rows.into_values().collect(); - rows.sort_by(|a, b| b.open.cmp(&a.open).then_with(|| a.campaign.cmp(&b.campaign))); + rows.sort_by(|a, b| b.blocking.cmp(&a.blocking).then_with(|| b.open.cmp(&a.open)).then_with(|| a.campaign.cmp(&b.campaign))); Ok(rows) } @@ -2375,10 +2389,16 @@ fn record_wear_facts(mind: &dyn MindPort, repos: &[OrgRepo], open_roots: &[Strin let (mut mine, elsewhere): (Vec<&UnreportedSpec>, Vec<&UnreportedSpec>) = specs.iter().partition(|spec| hers.contains(&spec.repo)); mine.sort_by_key(|spec| spec.at); if !mine.is_empty() { - facts.push(WearFact::SpecsOnYours(mine.iter().map(|spec| (spec.label.clone(), spec.at)).collect())); + facts.push(WearFact::SpecsOnYours(mine.iter().map(|spec| (spec.id.clone(), spec.at)).collect())); } - if let Some(oldest) = elsewhere.iter().map(|spec| spec.at).min() { - facts.push(WearFact::SpecsElsewhere { count: elsewhere.len(), oldest }); + if let Some(oldest) = elsewhere.iter().min_by(|a, b| (a.at, &a.id).cmp(&(b.at, &b.id))) { + let mut counts: BTreeMap<&str, usize> = BTreeMap::new(); + for spec in &elsewhere { + *counts.entry(spec.campaign.as_str()).or_default() += 1; + } + let mut by_campaign: Vec<(String, usize)> = counts.into_iter().map(|(campaign, count)| (campaign.to_owned(), count)).collect(); + by_campaign.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0))); + facts.push(WearFact::SpecsElsewhere { by_campaign, oldest: (oldest.id.clone(), oldest.at) }); } } if let Some(rows) = findings.as_ref().filter(|rows| !rows.is_empty()) { @@ -2432,21 +2452,37 @@ fn render_wear(now_ms: u64, facts: &[WearFact]) -> String { let named = specs.iter().map(|(label, at)| format!("{label} {}", age_text(now_ms, *at))).collect(); format!("- specs with no report in your mind, on your repos: {}\n", listed(named, ", ", |left, _| format!("+{left} more"))) } - WearFact::SpecsElsewhere { count, oldest } => { - format!("- specs with no report in your mind, on repos you do not steward: {count}, oldest {}\n", age_text(now_ms, *oldest)) + WearFact::SpecsElsewhere { by_campaign, oldest: (oldest_id, oldest_at) } => { + let count: usize = by_campaign.iter().map(|(_, count)| count).sum(); + let named = by_campaign.iter().map(|(campaign, count)| format!("{campaign} {count}")).collect(); + let more = |left: usize, from: usize| { + let held: usize = by_campaign[from..].iter().map(|(_, count)| count).sum(); + format!("+{held} more in {left} {}", if left == 1 { "campaign" } else { "campaigns" }) + }; + format!( + "- specs with no report in your mind, on repos you do not steward: {count} ({}), oldest {oldest_id} {}\n", + listed(named, ", ", more), + age_text(now_ms, *oldest_at) + ) } WearFact::SoulFindings(rows) => { - let named = rows.iter().map(|row| format!("{} {}, {} blocking, oldest {}", row.campaign, row.open, row.blocking, age_text(now_ms, row.oldest))).collect(); + let named = rows + .iter() + .map(|row| match &row.oldest_blocking { + Some((id, at)) => format!("{} {}, {} blocking, oldest blocking {id} {}", row.campaign, row.open, row.blocking, age_text(now_ms, *at)), + None => format!("{} {}, {} blocking, oldest {}", row.campaign, row.open, row.blocking, age_text(now_ms, row.oldest)), + }) + .collect(); let more = |left: usize, from: usize| { - let held: usize = rows[from..].iter().map(|row| row.open).sum(); - format!("+{held} more in {left} {}", if left == 1 { "campaign" } else { "campaigns" }) + let (held, blocking) = rows[from..].iter().fold((0, 0), |(held, blocking), row| (held + row.open, blocking + row.blocking)); + format!("+{held} more, {blocking} blocking, in {left} {}", if left == 1 { "campaign" } else { "campaigns" }) }; format!("- open Soul findings in your mind: {}\n", listed(named, "; ", more)) } WearFact::QuietCampaigns(quiet) => { let named = quiet.iter().map(|(campaign, at)| format!("{campaign} {}", age_text(now_ms, *at))).collect(); format!( - "- campaigns on your repos with open work and no admission in your mind for {STALE_AFTER_DAYS} d or more: {}\n", + "- campaigns that touch your repos with open work and no admission in your mind for {STALE_AFTER_DAYS} d or more: {}\n", listed(named, ", ", |left, _| format!("+{left} more")) ) } diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 50b6bb7..039d76a 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -5129,9 +5129,15 @@ fn wear_lists_only_her_slice() { assert_eq!( facts, [ - WearFact::SpecsOnYours(vec![("eureka-body/mine.r1".into(), t(3)), ("cased/low.r1".into(), t(2))]), - WearFact::SpecsElsewhere { count: 1, oldest: t(5) }, - WearFact::SoulFindings(vec![FindingRow { campaign: "eureka-body".into(), open: 1, blocking: 1, oldest: t(4) }]), + WearFact::SpecsOnYours(vec![("eureka-body:cut_spec:cut-mine.r1".into(), t(3)), ("cased:cut_spec:cut-low.r1".into(), t(2))]), + WearFact::SpecsElsewhere { by_campaign: vec![("elsewhere".into(), 1)], oldest: ("elsewhere:cut_spec:cut-theirs.r1".into(), t(5)) }, + WearFact::SoulFindings(vec![FindingRow { + campaign: "eureka-body".into(), + open: 1, + blocking: 1, + oldest: t(4), + oldest_blocking: Some(("eureka-body:finding:cut-f.s1.one".into(), t(4))), + }]), ] ); let wear = render_wear(t(0), &facts); @@ -5164,11 +5170,14 @@ fn wear_counts_cross_a_page() { let facts = record_wear_facts(&through(&mind, None, &log), &hers(), &[], t(0)); let [WearFact::SpecsOnYours(specs), WearFact::SoulFindings(rows)] = facts.as_slice() else { panic!("{facts:?}") }; assert_eq!((specs.len(), specs[0].1, specs[200].1), (201, t(9), t(1))); - assert_eq!(rows, &[FindingRow { campaign: "eureka-body".into(), open: 201, blocking: 51, oldest: t(10) }]); + assert_eq!( + rows, + &[FindingRow { campaign: "eureka-body".into(), open: 201, blocking: 51, oldest: t(10), oldest_blocking: Some(("eureka-body:finding:cut-v.s1.f000".into(), t(10))) }] + ); assert!(log.borrow().iter().filter(|read| read.as_str() == K::Finding.type_id()).count() >= 2, "more than one page of findings was walked"); assert!(log.borrow().iter().filter(|read| read.as_str() == K::CutSpec.type_id()).count() >= 2, "and of specs"); let wear = render_wear(t(0), &facts); - assert!(wear.contains("- open Soul findings in your mind: eureka-body 201, 51 blocking, oldest (10 d)\n"), "{wear}"); + assert!(wear.contains("- open Soul findings in your mind: eureka-body 201, 51 blocking, oldest blocking eureka-body:finding:cut-v.s1.f000 (10 d)\n"), "{wear}"); } /// A walk that fails on a later page fails that read whole, as a refusal or as a dead socket: its own missing line, no partial count, @@ -5226,7 +5235,7 @@ fn a_failed_wear_read_says_so() { live_session(&rig, &broken, 1000, false, 1); let input = input_of(&rig, 0); assert!(input.contains("- records are missing: the campaigns on your repos could not be read"), "{input}"); - assert!(input.contains("- specs with no report in your mind, on your repos: eureka-body/old.r1"), "the session ran and the specs printed: {input}"); + assert!(input.contains("- specs with no report in your mind, on your repos: eureka-body:cut_spec:cut-old.r1"), "the session ran and the specs printed: {input}"); } /// A campaign is quiet exactly at the window: 14 d lists, a millisecond less does not; it needs open work (an unreported spec, an @@ -5286,18 +5295,40 @@ fn wear_caps_are_exact() { let line = |fact: WearFact| render_wear(now, &[fact]).lines().nth(1).unwrap().to_string(); for (make, head) in [ (WearFact::SpecsOnYours as fn(Vec<(String, u64)>) -> WearFact, "- specs with no report in your mind, on your repos: "), - (WearFact::QuietCampaigns, "- campaigns on your repos with open work and no admission in your mind for 14 d or more: "), + (WearFact::QuietCampaigns, "- campaigns that touch your repos with open work and no admission in your mind for 14 d or more: "), ] { assert_eq!(line(make(entries(4))), format!("{head}c0 (1 d), c1 (2 d), c2 (3 d), c3 (4 d)")); assert_eq!(line(make(entries(5))), format!("{head}c0 (1 d), c1 (2 d), c2 (3 d), c3 (4 d), +1 more")); assert_eq!(line(make(entries(7))), format!("{head}c0 (1 d), c1 (2 d), c2 (3 d), c3 (4 d), +3 more")); } - let rows = |n: usize| (0..n).map(|k| FindingRow { campaign: format!("c{k}"), open: 10 - k, blocking: k, oldest: now - 3 * 3_600_000 }).collect::>(); - let named = "- open Soul findings in your mind: c0 10, 0 blocking, oldest (3 h); c1 9, 1 blocking, oldest (3 h); c2 8, 2 blocking, oldest (3 h); c3 7, 3 blocking, oldest (3 h)"; + // Rows arrive blocking first: c0 blocks most. Each blocking row names its oldest blocking finding. + let rows = |n: usize| { + (0..n) + .map(|k| FindingRow { + campaign: format!("c{k}"), + open: 10 - k, + blocking: 3 - k.min(3), + oldest: now - 9 * 3_600_000, + oldest_blocking: (k < 3).then(|| (format!("c{k}:finding:cut-v.s1.b"), now - 3 * 3_600_000)), + }) + .collect::>() + }; + let named = "- open Soul findings in your mind: c0 10, 3 blocking, oldest blocking c0:finding:cut-v.s1.b (3 h); c1 9, 2 blocking, oldest blocking c1:finding:cut-v.s1.b (3 h); c2 8, 1 blocking, oldest blocking c2:finding:cut-v.s1.b (3 h); c3 7, 0 blocking, oldest (9 h)"; assert_eq!(line(WearFact::SoulFindings(rows(4))), named); - assert_eq!(line(WearFact::SoulFindings(rows(5))), format!("{named}; +6 more in 1 campaign")); - assert_eq!(line(WearFact::SoulFindings(rows(7))), format!("{named}; +15 more in 3 campaigns")); - assert_eq!(line(WearFact::SpecsElsewhere { count: 9, oldest: now - 40 * DAY_MS as u64 }), "- specs with no report in your mind, on repos you do not steward: 9, oldest (40 d)"); + assert_eq!(line(WearFact::SoulFindings(rows(5))), format!("{named}; +6 more, 0 blocking, in 1 campaign")); + assert_eq!(line(WearFact::SoulFindings(rows(7))), format!("{named}; +15 more, 0 blocking, in 3 campaigns")); + // The tail counts the blocking it hides. + let mut crowded_out = rows(7); + (crowded_out[4].blocking, crowded_out[5].blocking, crowded_out[6].blocking) = (1, 2, 3); + assert_eq!(line(WearFact::SoulFindings(crowded_out)).rsplit_once("; ").unwrap().1, "+15 more, 6 blocking, in 3 campaigns"); + let oldest = ("elsewhere:cut_spec:cut-theirs.r1".to_string(), now - 40 * DAY_MS as u64); + let elsewhere = |counts: &[(&str, usize)]| WearFact::SpecsElsewhere { by_campaign: counts.iter().map(|(c, n)| (c.to_string(), *n)).collect(), oldest: oldest.clone() }; + let head = "- specs with no report in your mind, on repos you do not steward: "; + assert_eq!(line(elsewhere(&[("a", 6), ("b", 3)])), format!("{head}9 (a 6, b 3), oldest elsewhere:cut_spec:cut-theirs.r1 (40 d)")); + assert_eq!( + line(elsewhere(&[("a", 6), ("b", 5), ("c", 4), ("d", 3), ("e", 2), ("f", 1)])), + format!("{head}21 (a 6, b 5, c 4, d 3, +3 more in 2 campaigns), oldest elsewhere:cut_spec:cut-theirs.r1 (40 d)") + ); } /// Forge facts and record facts share the one bound: built to exactly the bound they all print with no tail; one byte more drops @@ -5316,18 +5347,119 @@ fn record_facts_share_the_one_bound() { facts.extend((0..count).map(forge)); let exact = render_wear(now, &facts); assert_eq!(exact.len(), WEAR_MAX_BYTES, "built to the byte"); - assert!(!exact.contains("not shown") && exact.contains("campaigns on your repos with open work"), "all print: {exact}"); + assert!(!exact.contains("not shown") && exact.contains("campaigns that touch your repos with open work"), "all print: {exact}"); assert!(exact.lines().nth(1).unwrap().starts_with("- changed by others"), "the forge's facts rank first: {exact}"); facts[0] = record(spare + 1); let over = render_wear(now, &facts); assert!(over.len() <= WEAR_MAX_BYTES, "{} bytes", over.len()); - assert!(!over.contains("campaigns on your repos with open work"), "the record fact drops from the end: {over}"); + assert!(!over.contains("campaigns that touch your repos with open work"), "the record fact drops from the end: {over}"); let shown = over.lines().filter(|line| line.starts_with("- ")).count(); assert!(over.trim_end().ends_with(&format!("({} more facts not shown)", facts.len() - shown)) && shown < facts.len(), "{over}"); assert!(shown + 1 >= count, "forge facts are kept before it: {shown} of {count}"); } +/// Campaigns that block come first whatever their size, and the line's tail counts the blocking it leaves out: a small campaign with +/// a blocker is listed before a large one with none, and the campaigns past the cap are named by `+N more, B blocking, in M campaigns`. +#[test] +fn blocking_findings_sort_first_and_the_tail_counts_what_it_hides() { + use eureka_pipeline::FindingSeverity::{Blocker, Low}; + let mind = TestMind::seeded(); + // (campaign, blocking findings, low findings): `a` is the largest and blocks nothing. + for (campaign, blockers, lows) in [("a", 0, 5), ("b", 1, 0), ("c", 1, 1), ("d", 1, 0), ("e", 1, 0), ("f", 1, 3)] { + mind.admit_at(at(-9 * DAY_MS)); + mind.committed(campaign_on(campaign, &[REPO])); + mind.committed(chain(campaign, "v", REPO)); + let found = (0..blockers).map(|n| soul_finding(campaign, "v", &format!("b{n}"), Blocker)).chain((0..lows).map(|n| soul_finding(campaign, "v", &format!("l{n}"), Low))); + soul(&mind, found.collect()); + } + let log = RefCell::default(); + let facts = record_wear_facts(&through(&mind, None, &log), &hers(), &[], t(0)); + let rows = facts.iter().find_map(|fact| if let WearFact::SoulFindings(rows) = fact { Some(rows) } else { None }).expect("findings"); + let order: Vec<(&str, usize, usize)> = rows.iter().map(|row| (row.campaign.as_str(), row.open, row.blocking)).collect(); + assert_eq!(order, [("f", 4, 1), ("c", 2, 1), ("b", 1, 1), ("d", 1, 1), ("e", 1, 1), ("a", 5, 0)], "blocking first, then open, then name"); + let wear = render_wear(t(0), &facts); + let line = wear.lines().find(|line| line.starts_with("- open Soul findings")).unwrap(); + assert!(line.starts_with("- open Soul findings in your mind: f 4, 1 blocking, oldest blocking f:finding:cut-v.s1.b0 (9 d); c 2, 1 blocking,"), "{line}"); + assert!(line.ends_with("; +6 more, 1 blocking, in 2 campaigns"), "the tail keeps the blocking it hides: {line}"); +} + +/// Every record fact leads to a record she can open: a spec by its id, the specs of other repos by campaign with the oldest one's id, +/// and a campaign's blocking by its oldest blocking finding's id (the oldest by admission, not by name). +#[test] +fn wear_lines_name_the_records_to_open() { + use eureka_pipeline::FindingSeverity::{Blocker, Low}; + let mind = TestMind::seeded(); + mind.admit_at(at(-9 * DAY_MS)); + mind.committed(campaign_on("elsewhere", &["GameCult/Elsewhere"])); + mind.admit_at(at(-5 * DAY_MS)); + mind.committed(vec![unreported_spec("elsewhere", "old", "GameCult/Elsewhere")]); + mind.admit_at(at(-4 * DAY_MS)); + mind.committed(campaign_on("zz", &["GameCult/Other"])); + mind.committed(vec![unreported_spec("elsewhere", "new", "GameCult/Elsewhere"), unreported_spec("zz", "other", "GameCult/Other")]); + mind.admit_at(at(-3 * DAY_MS)); + mind.committed(vec![unreported_spec(CAMPAIGN, "mine", REPO)]); + mind.admit_at(at(-8 * DAY_MS)); + mind.committed(chain(CAMPAIGN, "v", REPO)); + mind.admit_at(at(-7 * DAY_MS)); + soul(&mind, vec![soul_finding(CAMPAIGN, "v", "zz-older", Blocker), soul_finding(CAMPAIGN, "v", "low", Low)]); + mind.admit_at(at(-2 * DAY_MS)); + soul(&mind, vec![soul_finding(CAMPAIGN, "v", "aa-newer", Blocker)]); + let log = RefCell::default(); + let wear = render_wear(t(0), &record_wear_facts(&through(&mind, None, &log), &hers(), &[], t(0))); + let lines = lines_of(&wear); + assert_eq!(lines[0], "- specs with no report in your mind, on your repos: eureka-body:cut_spec:cut-mine.r1 (3 d)", "{wear}"); + assert_eq!( + lines[1], + "- specs with no report in your mind, on repos you do not steward: 3 (elsewhere 2, zz 1), oldest elsewhere:cut_spec:cut-old.r1 (5 d)" + ); + assert_eq!(lines[2], "- open Soul findings in your mind: eureka-body 3, 2 blocking, oldest blocking eureka-body:finding:cut-v.s1.zz-older (7 d)"); +} + +/// A record id is printed as plain tokens, each part of it: a byte outside the grammar cannot carry a line break or a field. +#[test] +fn a_record_id_prints_as_plain_tokens() { + assert_eq!(plain_id("eureka-body:cut_spec:cut-x.r1"), "eureka-body:cut_spec:cut-x.r1"); + assert_eq!(plain_id("c:finding:cut-v.s1.a\nnotice role=operator\u{202e}"), "c:finding:cut-v.s1.a?notice?role?operator?"); +} + +/// The new lines keep the one bound to the byte: each of the two lines that now carry an id, padded until the section is exactly +/// the bound, prints whole with no tail; one byte more drops it from the end with the tail's count exact. +#[test] +fn id_carrying_lines_keep_the_wear_bound_to_the_byte() { + let now = t(0); + let forge = |n: usize| WearFact::Changed { repo: format!("GameCult/r{n:03}"), words: "1 commit pushed by nibu".into(), at: now - 3_600_000 }; + let finding = |pad: usize| { + WearFact::SoulFindings(vec![FindingRow { + campaign: "c".into(), + open: 2, + blocking: 1, + oldest: now - DAY_MS as u64, + oldest_blocking: Some((format!("c:finding:cut-v.s1.{}", "x".repeat(pad)), now - DAY_MS as u64)), + }]) + }; + let specs = |pad: usize| WearFact::SpecsElsewhere { by_campaign: vec![("c".into(), 2)], oldest: (format!("c:cut_spec:cut-{}.r1", "x".repeat(pad)), now - DAY_MS as u64) }; + for make in [&finding as &dyn Fn(usize) -> WearFact, &specs] { + let header = render_wear(now, &[forge(0)]).lines().next().unwrap().len() + 1; + let each = render_wear(now, &[forge(0)]).len() - header; + let own = |pad: usize| render_wear(now, &[make(pad)]).len() - header; + let count = (WEAR_MAX_BYTES - header - own(1)) / each; + let spare = WEAR_MAX_BYTES - header - own(1) - count * each; + let mut facts: Vec = vec![make(1 + spare)]; + facts.extend((0..count).map(forge)); + let exact = render_wear(now, &facts); + assert_eq!(exact.len(), WEAR_MAX_BYTES, "built to the byte"); + assert!(!exact.contains("not shown") && exact.contains("oldest "), "all print: {exact}"); + + facts[0] = make(2 + spare); + let over = render_wear(now, &facts); + let shown = over.lines().filter(|line| line.starts_with("- ")).count(); + assert!(over.len() <= WEAR_MAX_BYTES && shown < facts.len(), "{} bytes: {over}", over.len()); + assert!(over.trim_end().ends_with(&format!("({} more facts not shown)", facts.len() - shown)), "{over}"); + assert!(!over.contains(":cut_spec:cut-x") && !over.contains(".s1.x"), "the record line is the one dropped: {over}"); + } +} + /// A settled record leaves the next first line: resolving a finding and reporting a spec removes them, and the reads wrote nothing. #[test] fn a_settled_record_leaves_the_next_first_line() { @@ -5374,7 +5506,7 @@ fn wear_reads_only_on_a_first_line() { let (none_roots, none_specs, none_findings, bare) = wear_reads(false, 1, false); assert_eq!((roots, specs - none_specs, findings - none_findings), (1, 1, 1), "three reads on a start"); assert_eq!(none_roots, 0, "a Mind with no stewardship makes none of them"); - assert!(input.contains("eureka-body/open.r1") && !bare.contains("Where your machine shows wear"), "{input} + assert!(input.contains("eureka-body:cut_spec:cut-open.r1") && !bare.contains("Where your machine shows wear"), "{input} {bare}"); let long = wear_reads(true, 25, true); assert_eq!((long.0, long.1, long.2), (roots, specs, findings), "polls and tells read nothing more"); From 1d956b4ae443f9b7432509af3f9ff2046b417add Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 3 Nov 2026 06:29:24 +0100 Subject: [PATCH 7/9] wear-facts: two tests state what the lines now say (the campaign breakdown names a campaign; ages floor to 8 d) Co-Authored-By: Claude Opus 5.5 --- crates/mind-body/src/persona/tests.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 039d76a..0b6cce7 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -5141,7 +5141,8 @@ fn wear_lists_only_her_slice() { ] ); let wear = render_wear(t(0), &facts); - assert!(!wear.contains("elsewhere") && !wear.contains("Elsewhere"), "{wear}"); + assert!(!wear.contains("Elsewhere"), "the repo she does not steward is not named: {wear}"); + assert!(wear.lines().filter(|line| line.contains("Soul findings")).all(|line| !line.contains("elsewhere")), "no finding outside her slice: {wear}"); let none = TestMind::without_campaigns(); let log = RefCell::default(); @@ -5380,7 +5381,7 @@ fn blocking_findings_sort_first_and_the_tail_counts_what_it_hides() { assert_eq!(order, [("f", 4, 1), ("c", 2, 1), ("b", 1, 1), ("d", 1, 1), ("e", 1, 1), ("a", 5, 0)], "blocking first, then open, then name"); let wear = render_wear(t(0), &facts); let line = wear.lines().find(|line| line.starts_with("- open Soul findings")).unwrap(); - assert!(line.starts_with("- open Soul findings in your mind: f 4, 1 blocking, oldest blocking f:finding:cut-v.s1.b0 (9 d); c 2, 1 blocking,"), "{line}"); + assert!(line.starts_with("- open Soul findings in your mind: f 4, 1 blocking, oldest blocking f:finding:cut-v.s1.b0 (8 d); c 2, 1 blocking,"), "{line}"); assert!(line.ends_with("; +6 more, 1 blocking, in 2 campaigns"), "the tail keeps the blocking it hides: {line}"); } From 7abf621e3c52cdb4624fd3c63b0cf2881a4828d1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 3 Nov 2026 06:59:34 +0100 Subject: [PATCH 8/9] wear-facts: a campaign's blocking handle names the most severe blocker; sort pinned; old words gone The blocking handle is the worst blocking finding (Blocker before High, oldest among equals), and the redundant admission-time compare is removed: the page is in admission order, so the first read at the top severity is the oldest. The line says "worst". A test fails when the by-campaign breakdown's sort is deleted. "campaigns on your repos" and "largest first" no longer misdescribe the code. Co-Authored-By: Claude Opus 5.5 --- crates/mind-body/src/persona.rs | 29 +++++++++------- crates/mind-body/src/persona/tests.rs | 50 +++++++++++++++++++-------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/crates/mind-body/src/persona.rs b/crates/mind-body/src/persona.rs index d4bc90d..0a60c48 100644 --- a/crates/mind-body/src/persona.rs +++ b/crates/mind-body/src/persona.rs @@ -2203,7 +2203,7 @@ enum WearFact { Yours { repo: String, number: u32, at: u64 }, /// Specs in force with no report, on repos she stewards: each spec's record id and the revision's admission, oldest first. SpecsOnYours(Vec<(String, u64)>), - /// The same on repos she does not steward: how many in each campaign (largest first), and the oldest spec's record id and admission. + /// The same on repos she does not steward: how many in each campaign (by count, largest first), and the oldest spec's record id and admission. SpecsElsewhere { by_campaign: Vec<(String, usize)>, oldest: (String, u64) }, /// Open Soul findings in her mind, by campaign of her slice, blocking first, then largest. SoulFindings(Vec), @@ -2211,15 +2211,15 @@ enum WearFact { QuietCampaigns(Vec<(String, u64)>), } -/// One campaign's open Soul findings in her mind: how many, how many block, the oldest admission, and the oldest blocking finding's -/// record id and admission when one blocks. +/// One campaign's open Soul findings in her mind: how many, how many block, the oldest admission, and the most severe blocking +/// finding's record id and admission (the oldest among equals) when one blocks. #[derive(Clone, Debug, PartialEq, Eq)] struct FindingRow { campaign: String, open: usize, blocking: usize, oldest: u64, - oldest_blocking: Option<(String, u64)>, + worst_blocking: Option<(String, u64)>, } impl WearFact { @@ -2336,23 +2336,26 @@ fn unreported_specs(mind: &dyn MindPort) -> Result> { Ok(specs) } -/// The Soul findings in force under the roots of her slice, counted by campaign, largest first, from every page. +/// The Soul findings in force under the roots of her slice, counted by campaign, from every page, blocking campaigns first and then the largest. fn open_soul_findings(mind: &dyn MindPort, slice: &[(String, u64)]) -> Result> { let roots: Vec<&str> = slice.iter().map(|(root, _)| root.as_str()).collect(); let selection = any_of(any_of(in_force(of_kind(PipelineKind::Finding)), "faculty", &["Soul"]), "root", &roots); let mut rows: BTreeMap = BTreeMap::new(); + // Each row's held worst blocking finding's severity, as an index into BLOCKING (0 the worst). + let mut held_rank: BTreeMap = BTreeMap::new(); for found in headers(mind, &selection)? { let PipelineFacts::Finding { severity, .. } = &found.facts else { continue }; let at = instant_ms(&found.admission.admitted_at).ok_or_else(|| anyhow!("a finding's admission has no instant"))?; let campaign = root_label(&found.id); - let row = rows.entry(campaign.clone()).or_insert(FindingRow { campaign, open: 0, blocking: 0, oldest: at, oldest_blocking: None }); + let row = rows.entry(campaign.clone()).or_insert(FindingRow { campaign, open: 0, blocking: 0, oldest: at, worst_blocking: None }); row.open += 1; row.oldest = row.oldest.min(at); - if BLOCKING.contains(&format!("{severity:?}").as_str()) { + if let Some(rank) = BLOCKING.iter().position(|name| *name == format!("{severity:?}")) { row.blocking += 1; - let id = plain_id(&found.id.id.0); - if row.oldest_blocking.as_ref().is_none_or(|(held, held_at)| (at, &id) < (*held_at, held)) { - row.oldest_blocking = Some((id, at)); + // The page is in admission order, so the first read at the top severity is the oldest at it. + if held_rank.get(&row.campaign).is_none_or(|held| rank < *held) { + held_rank.insert(row.campaign.clone(), rank); + row.worst_blocking = Some((plain_id(&found.id.id.0), at)); } } } @@ -2377,7 +2380,7 @@ fn record_wear_facts(mind: &dyn MindPort, repos: &[OrgRepo], open_roots: &[Strin let mut facts = Vec::new(); let missing = |what: &str| WearFact::RecordsMissing(what.to_owned()); if slice.is_none() { - facts.push(missing("the campaigns on your repos could not be read, so open Soul findings and quiet campaigns are absent")); + facts.push(missing("the campaigns of your repos could not be read, so open Soul findings and quiet campaigns are absent")); } if specs.is_none() { facts.push(missing("specs with no report could not be read, so they and quiet campaigns are absent")); @@ -2468,8 +2471,8 @@ fn render_wear(now_ms: u64, facts: &[WearFact]) -> String { WearFact::SoulFindings(rows) => { let named = rows .iter() - .map(|row| match &row.oldest_blocking { - Some((id, at)) => format!("{} {}, {} blocking, oldest blocking {id} {}", row.campaign, row.open, row.blocking, age_text(now_ms, *at)), + .map(|row| match &row.worst_blocking { + Some((id, at)) => format!("{} {}, {} blocking, worst {id} {}", row.campaign, row.open, row.blocking, age_text(now_ms, *at)), None => format!("{} {}, {} blocking, oldest {}", row.campaign, row.open, row.blocking, age_text(now_ms, row.oldest)), }) .collect(); diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 0b6cce7..66dd9ef 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -5136,7 +5136,7 @@ fn wear_lists_only_her_slice() { open: 1, blocking: 1, oldest: t(4), - oldest_blocking: Some(("eureka-body:finding:cut-f.s1.one".into(), t(4))), + worst_blocking: Some(("eureka-body:finding:cut-f.s1.one".into(), t(4))), }]), ] ); @@ -5173,12 +5173,12 @@ fn wear_counts_cross_a_page() { assert_eq!((specs.len(), specs[0].1, specs[200].1), (201, t(9), t(1))); assert_eq!( rows, - &[FindingRow { campaign: "eureka-body".into(), open: 201, blocking: 51, oldest: t(10), oldest_blocking: Some(("eureka-body:finding:cut-v.s1.f000".into(), t(10))) }] + &[FindingRow { campaign: "eureka-body".into(), open: 201, blocking: 51, oldest: t(10), worst_blocking: Some(("eureka-body:finding:cut-v.s1.f000".into(), t(10))) }] ); assert!(log.borrow().iter().filter(|read| read.as_str() == K::Finding.type_id()).count() >= 2, "more than one page of findings was walked"); assert!(log.borrow().iter().filter(|read| read.as_str() == K::CutSpec.type_id()).count() >= 2, "and of specs"); let wear = render_wear(t(0), &facts); - assert!(wear.contains("- open Soul findings in your mind: eureka-body 201, 51 blocking, oldest blocking eureka-body:finding:cut-v.s1.f000 (10 d)\n"), "{wear}"); + assert!(wear.contains("- open Soul findings in your mind: eureka-body 201, 51 blocking, worst eureka-body:finding:cut-v.s1.f000 (10 d)\n"), "{wear}"); } /// A walk that fails on a later page fails that read whole, as a refusal or as a dead socket: its own missing line, no partial count, @@ -5217,7 +5217,7 @@ fn a_failed_wear_read_says_so() { assert_eq!(all(&whole), 1, "with every read answered the old campaign is quiet: {whole:?}"); for fault in [Fault::Refused, Fault::Client] { for (read, missing) in [ - ("roots", "campaigns on your repos could not be read"), + ("roots", "campaigns of your repos could not be read"), (K::CutSpec.type_id(), "specs with no report could not be read"), (K::Finding.type_id(), "open Soul findings could not be read"), ] { @@ -5235,7 +5235,7 @@ fn a_failed_wear_read_says_so() { let broken = through(&rig.mind, Some(("roots".into(), 1, Fault::Client)), &log); live_session(&rig, &broken, 1000, false, 1); let input = input_of(&rig, 0); - assert!(input.contains("- records are missing: the campaigns on your repos could not be read"), "{input}"); + assert!(input.contains("- records are missing: the campaigns of your repos could not be read"), "{input}"); assert!(input.contains("- specs with no report in your mind, on your repos: eureka-body:cut_spec:cut-old.r1"), "the session ran and the specs printed: {input}"); } @@ -5302,7 +5302,7 @@ fn wear_caps_are_exact() { assert_eq!(line(make(entries(5))), format!("{head}c0 (1 d), c1 (2 d), c2 (3 d), c3 (4 d), +1 more")); assert_eq!(line(make(entries(7))), format!("{head}c0 (1 d), c1 (2 d), c2 (3 d), c3 (4 d), +3 more")); } - // Rows arrive blocking first: c0 blocks most. Each blocking row names its oldest blocking finding. + // Rows arrive blocking first: c0 blocks most. Each blocking row names its worst finding. let rows = |n: usize| { (0..n) .map(|k| FindingRow { @@ -5310,11 +5310,11 @@ fn wear_caps_are_exact() { open: 10 - k, blocking: 3 - k.min(3), oldest: now - 9 * 3_600_000, - oldest_blocking: (k < 3).then(|| (format!("c{k}:finding:cut-v.s1.b"), now - 3 * 3_600_000)), + worst_blocking: (k < 3).then(|| (format!("c{k}:finding:cut-v.s1.b"), now - 3 * 3_600_000)), }) .collect::>() }; - let named = "- open Soul findings in your mind: c0 10, 3 blocking, oldest blocking c0:finding:cut-v.s1.b (3 h); c1 9, 2 blocking, oldest blocking c1:finding:cut-v.s1.b (3 h); c2 8, 1 blocking, oldest blocking c2:finding:cut-v.s1.b (3 h); c3 7, 0 blocking, oldest (9 h)"; + let named = "- open Soul findings in your mind: c0 10, 3 blocking, worst c0:finding:cut-v.s1.b (3 h); c1 9, 2 blocking, worst c1:finding:cut-v.s1.b (3 h); c2 8, 1 blocking, worst c2:finding:cut-v.s1.b (3 h); c3 7, 0 blocking, oldest (9 h)"; assert_eq!(line(WearFact::SoulFindings(rows(4))), named); assert_eq!(line(WearFact::SoulFindings(rows(5))), format!("{named}; +6 more, 0 blocking, in 1 campaign")); assert_eq!(line(WearFact::SoulFindings(rows(7))), format!("{named}; +15 more, 0 blocking, in 3 campaigns")); @@ -5381,15 +5381,15 @@ fn blocking_findings_sort_first_and_the_tail_counts_what_it_hides() { assert_eq!(order, [("f", 4, 1), ("c", 2, 1), ("b", 1, 1), ("d", 1, 1), ("e", 1, 1), ("a", 5, 0)], "blocking first, then open, then name"); let wear = render_wear(t(0), &facts); let line = wear.lines().find(|line| line.starts_with("- open Soul findings")).unwrap(); - assert!(line.starts_with("- open Soul findings in your mind: f 4, 1 blocking, oldest blocking f:finding:cut-v.s1.b0 (8 d); c 2, 1 blocking,"), "{line}"); + assert!(line.starts_with("- open Soul findings in your mind: f 4, 1 blocking, worst f:finding:cut-v.s1.b0 (8 d); c 2, 1 blocking,"), "{line}"); assert!(line.ends_with("; +6 more, 1 blocking, in 2 campaigns"), "the tail keeps the blocking it hides: {line}"); } /// Every record fact leads to a record she can open: a spec by its id, the specs of other repos by campaign with the oldest one's id, -/// and a campaign's blocking by its oldest blocking finding's id (the oldest by admission, not by name). +/// and a campaign's blocking by its worst finding's id (the oldest by admission, not by name). #[test] fn wear_lines_name_the_records_to_open() { - use eureka_pipeline::FindingSeverity::{Blocker, Low}; + use eureka_pipeline::FindingSeverity::{Blocker, High, Low}; let mind = TestMind::seeded(); mind.admit_at(at(-9 * DAY_MS)); mind.committed(campaign_on("elsewhere", &["GameCult/Elsewhere"])); @@ -5402,6 +5402,8 @@ fn wear_lines_name_the_records_to_open() { mind.committed(vec![unreported_spec(CAMPAIGN, "mine", REPO)]); mind.admit_at(at(-8 * DAY_MS)); mind.committed(chain(CAMPAIGN, "v", REPO)); + mind.admit_at(at(-7 * DAY_MS - DAY_MS / 2)); + soul(&mind, vec![soul_finding(CAMPAIGN, "v", "high", High)]); mind.admit_at(at(-7 * DAY_MS)); soul(&mind, vec![soul_finding(CAMPAIGN, "v", "zz-older", Blocker), soul_finding(CAMPAIGN, "v", "low", Low)]); mind.admit_at(at(-2 * DAY_MS)); @@ -5414,7 +5416,27 @@ fn wear_lines_name_the_records_to_open() { lines[1], "- specs with no report in your mind, on repos you do not steward: 3 (elsewhere 2, zz 1), oldest elsewhere:cut_spec:cut-old.r1 (5 d)" ); - assert_eq!(lines[2], "- open Soul findings in your mind: eureka-body 3, 2 blocking, oldest blocking eureka-body:finding:cut-v.s1.zz-older (7 d)"); + assert_eq!(lines[2], "- open Soul findings in your mind: eureka-body 4, 3 blocking, worst eureka-body:finding:cut-v.s1.zz-older (7 d)"); +} + +/// The specs of other repos are counted by campaign, largest first and then by name: the breakdown is sorted, not left in key order. +#[test] +fn specs_of_other_repos_are_counted_by_campaign_largest_first() { + let mind = TestMind::seeded(); + mind.admit_at(at(-5 * DAY_MS)); + mind.committed(campaign_on("a", &["GameCult/Other"])); + mind.committed(campaign_on("b", &["GameCult/Other"])); + mind.committed(campaign_on("c", &["GameCult/Other"])); + mind.committed(vec![ + unreported_spec("a", "one", "GameCult/Other"), + unreported_spec("b", "one", "GameCult/Other"), + unreported_spec("b", "two", "GameCult/Other"), + unreported_spec("c", "one", "GameCult/Other"), + ]); + let log = RefCell::default(); + let facts = record_wear_facts(&through(&mind, None, &log), &hers(), &[], t(0)); + let by_campaign = facts.iter().find_map(|fact| if let WearFact::SpecsElsewhere { by_campaign, .. } = fact { Some(by_campaign.clone()) } else { None }).expect("specs elsewhere"); + assert_eq!(by_campaign, [("b".to_string(), 2), ("a".to_string(), 1), ("c".to_string(), 1)]); } /// A record id is printed as plain tokens, each part of it: a byte outside the grammar cannot carry a line break or a field. @@ -5436,7 +5458,7 @@ fn id_carrying_lines_keep_the_wear_bound_to_the_byte() { open: 2, blocking: 1, oldest: now - DAY_MS as u64, - oldest_blocking: Some((format!("c:finding:cut-v.s1.{}", "x".repeat(pad)), now - DAY_MS as u64)), + worst_blocking: Some((format!("c:finding:cut-v.s1.{}", "x".repeat(pad)), now - DAY_MS as u64)), }]) }; let specs = |pad: usize| WearFact::SpecsElsewhere { by_campaign: vec![("c".into(), 2)], oldest: (format!("c:cut_spec:cut-{}.r1", "x".repeat(pad)), now - DAY_MS as u64) }; @@ -5522,7 +5544,7 @@ fn an_undated_record_makes_its_read_missing() { mind.committed(chain(CAMPAIGN, "f", REPO)); soul(&mind, vec![soul_finding(CAMPAIGN, "f", "one", eureka_pipeline::FindingSeverity::Low)]); for (spoiled, missing) in [ - ("roots", "campaigns on your repos could not be read"), + ("roots", "campaigns of your repos could not be read"), (K::CutSpec.type_id(), "specs with no report could not be read"), (K::Finding.type_id(), "open Soul findings could not be read"), ] { From 60145540e94cb666c6940423c2ecec5bb2f9872a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 3 Nov 2026 07:02:46 +0100 Subject: [PATCH 9/9] wear-facts: the byte-bound test checks the padded id prints, not the word 'oldest' Co-Authored-By: Claude Opus 5.5 --- crates/mind-body/src/persona/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 66dd9ef..681546a 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -5472,7 +5472,7 @@ fn id_carrying_lines_keep_the_wear_bound_to_the_byte() { facts.extend((0..count).map(forge)); let exact = render_wear(now, &facts); assert_eq!(exact.len(), WEAR_MAX_BYTES, "built to the byte"); - assert!(!exact.contains("not shown") && exact.contains("oldest "), "all print: {exact}"); + assert!(!exact.contains("not shown") && exact.contains("xx"), "all print: {exact}"); facts[0] = make(2 + spare); let over = render_wear(now, &facts);