wear-facts: a Mind's first line shows the wear her own mind holds, specs with no report, open Soul findings and quiet campaigns on what she stewards, each leading to a record she can open (squash of hands/eureka-body-wear-facts ab61a53..6014554; Soul s1-s2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-11-03 07:06:16 +01:00
commit cb86f58a7d
14 changed files with 1203 additions and 24 deletions

View file

@ -509,6 +509,7 @@ fn unexpected(answer: &HuginnMindResponse) -> Trouble {
HuginnMindResponse::PersonaPut(_) => "a persona put", HuginnMindResponse::PersonaPut(_) => "a persona put",
HuginnMindResponse::Sources(_) => "source hits", HuginnMindResponse::Sources(_) => "source hits",
HuginnMindResponse::Activity(_) => "repo activity", HuginnMindResponse::Activity(_) => "repo activity",
HuginnMindResponse::RootsTouched(_) => "roots touched",
HuginnMindResponse::Refused(_) => "a refusal", HuginnMindResponse::Refused(_) => "a refusal",
HuginnMindResponse::Deferred(_) => "a deferral", HuginnMindResponse::Deferred(_) => "a deferral",
}; };

View file

@ -5,6 +5,7 @@
//! reads through `eureka_state::hold`, the decider the heartbeat's gate reads. //! reads through `eureka_state::hold`, the decider the heartbeat's gate reads.
use clap::{Args, Subcommand}; use clap::{Args, Subcommand};
use eureka_state::hold::BLOCKING;
use huginn_mind::SemanticQuery; use huginn_mind::SemanticQuery;
use huginn_mind::eureka_pipeline::{Line, PipelineKind}; use huginn_mind::eureka_pipeline::{Line, PipelineKind};
use serde_json::{Value, json}; use serde_json::{Value, json};
@ -16,8 +17,6 @@ use crate::trouble::Trouble;
const GAP_HOME: &str = "eureka-substrate"; const GAP_HOME: &str = "eureka-substrate";
/// How many neighbours `precedent` asks the index for. /// How many neighbours `precedent` asks the index for.
const PRECEDENT_TOP_K: u32 = 10; const PRECEDENT_TOP_K: u32 = 10;
/// Severities that block a cut.
const BLOCKING: [&str; 2] = ["Blocker", "High"];
#[derive(Args, Clone, Debug, Default)] #[derive(Args, Clone, Debug, Default)]
pub struct Paging { pub struct Paging {

View file

@ -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 { pub fn of_kind(kind: PipelineKind) -> Selection {
Selection { Selection {
schemas: Some(vec![kind.type_id().to_string()]), schemas: Some(vec![kind.type_id().to_string()]),

View file

@ -200,6 +200,10 @@ impl<S: MindStore, I: IndexSink<S>> Daemon<S, I> {
HuginnMindRequest::Query { selection, semantic: Some(semantic), .. } => { HuginnMindRequest::Query { selection, semantic: Some(semantic), .. } => {
return self.begin_search(selection, 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 } => { HuginnMindRequest::StewardedActivity { instance, repos, after } => {
let refused = |message: &str| MindRefusal::SelectionInvalid { field: "repos".into(), value: None, message: message.into() }; let refused = |message: &str| MindRefusal::SelectionInvalid { field: "repos".into(), value: None, message: message.into() };
if repos.len() > ACTIVITY_REPOS_MAX { 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::<HuginnMindResponse>(&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 { fn activity_request(repos: &[&str]) -> HuginnMindRequest {
HuginnMindRequest::StewardedActivity { instance: slug(INSTANCE), repos: repos.iter().map(|repo| OrgRepo::from(*repo)).collect(), after: Some(1) } HuginnMindRequest::StewardedActivity { instance: slug(INSTANCE), repos: repos.iter().map(|repo| OrgRepo::from(*repo)).collect(), after: Some(1) }
} }

View file

@ -48,7 +48,7 @@ pub use mind::{HuginnMindEpoch, Mind};
pub use persona::{PersonaPutOutcome, PersonaStateView}; pub use persona::{PersonaPutOutcome, PersonaStateView};
pub use query::{ pub use query::{
AdmissionFacts, PipelineDocumentSummary, PipelineDocumentView, PipelineEdge, PipelineFacts, PipelinePageItems, 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 receipt::{DocumentVersion, Faculty, HuginnCommitReceipt, PipelineProvenance, RECEIPT_SCHEMA_VERSION};
pub use refusal::{MindRefusal, RunId}; pub use refusal::{MindRefusal, RunId};

View file

@ -28,7 +28,7 @@ use cultnet_rs::{
}; };
use eureka_pipeline::{ use eureka_pipeline::{
ClaimOutcome, CommitRange, Date, FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MemoryOrigin, MemorySort, OrgRepo, 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, Short, Slug, Title, pipeline_key,
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
@ -39,7 +39,7 @@ use crate::mind::{Mind, schema_cache};
use crate::persona::HuginnPersonaEntry; use crate::persona::HuginnPersonaEntry;
use crate::receipt::{self, PipelineProvenance}; use crate::receipt::{self, PipelineProvenance};
use crate::refusal::MindRefusal; 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; use crate::store::MindStore;
/// What admission recorded about a document when it landed: the receipt that /// 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}") } 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<OrgRepo>,
}
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 /// 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 /// was built from. Built once per call and thrown away with it; nothing here
/// outlives the answer. /// outlives the answer.
@ -455,6 +472,31 @@ impl Reader {
current current
} }
fn roots_touched(&self) -> Vec<RootTouched> {
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<Vec<SelectionRow>, MindRefusal> { fn rows(&self) -> Result<Vec<SelectionRow>, MindRefusal> {
self.docs self.docs
.image .image
@ -563,6 +605,14 @@ impl<S: MindStore> Mind<S> {
Self::page(&selection, evaluation, as_of, None) 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<Vec<RootTouched>, MindRefusal> {
let head = receipt::head(self)?;
Ok(Reader::at(self, head)?.roots_touched())
}
/// What a semantic query must satisfy before anything is embedded: the /// What a semantic query must satisfy before anything is embedded: the
/// selection passes the same two doors `query` does, carries no cursor and /// selection passes the same two doors `query` does, carries no cursor and
/// no `descending` (a ranked answer is neither pageable nor reversible), /// no `descending` (a ranked answer is neither pageable nor reversible),
@ -2521,4 +2571,110 @@ mod tests {
let count = mind.read_back().unwrap(); let count = mind.read_back().unwrap();
println!("read back {count} stored documents in {:?}", started.elapsed()); println!("read back {count} stored documents in {:?}", started.elapsed());
} }
fn at_hour(hour: u32) -> chrono::DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 9, 16, 12 + hour, 0, 0).unwrap()
}
fn touched(mind: &Mind<MemoryStore>) -> BTreeMap<String, RootTouched> {
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<D> {
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::<Vec<_>>(), [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::<Vec<_>>(), [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");
}
} }

View file

@ -14,7 +14,7 @@ use serde_json::Value;
use crate::admission::{PipelineAdmissionBatch, PipelineAdmissionOutcome}; use crate::admission::{PipelineAdmissionBatch, PipelineAdmissionOutcome};
use crate::mind::Mind; use crate::mind::Mind;
use crate::persona::{PersonaPutOutcome, PersonaStateView}; 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::receipt::{HuginnCommitReceipt, PipelineProvenance};
use crate::refusal::MindRefusal; use crate::refusal::MindRefusal;
use crate::store::MindStore; use crate::store::MindStore;
@ -98,6 +98,9 @@ pub enum HuginnMindRequest {
/// in time. The repos are the asker's to name (her stewardships); the /// in time. The repos are the asker's to name (her stewardships); the
/// instance's own login is dropped from the answer. /// instance's own login is dropped from the answer.
StewardedActivity { instance: Slug, repos: Vec<OrgRepo>, after: Option<u64> }, StewardedActivity { instance: Slug, repos: Vec<OrgRepo>, after: Option<u64> },
/// 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. /// The most repos one activity read names.
@ -250,6 +253,7 @@ impl HuginnMindRequest {
Self::PersonaPut { .. } => "persona_put", Self::PersonaPut { .. } => "persona_put",
Self::SearchSources { .. } => "search_sources", Self::SearchSources { .. } => "search_sources",
Self::StewardedActivity { .. } => "stewarded_activity", Self::StewardedActivity { .. } => "stewarded_activity",
Self::RootsTouched { .. } => "roots_touched",
} }
} }
@ -263,7 +267,8 @@ impl HuginnMindRequest {
| Self::PersonaGet { instance } | Self::PersonaGet { instance }
| Self::PersonaPut { instance, .. } | Self::PersonaPut { instance, .. }
| Self::SearchSources { 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), PersonaPut(PersonaPutOutcome),
Sources(SourceHits), Sources(SourceHits),
Activity(RepoActivity), Activity(RepoActivity),
RootsTouched(Vec<RootTouched>),
/// The answer did not fit one send and is being held for the client to /// 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, /// 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 /// in place of an answer, and the answer it stands for is the named
@ -417,6 +423,7 @@ mod tests {
instance: slug(INSTANCE), instance: slug(INSTANCE),
query: SourceQuery { text: Line("reconcile".into()), top_k: 5, repo: Some(Short("Huginn".into())) }, 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(), text: "x".into(),
}], }],
}), }),
HuginnMindResponse::RootsTouched(seeded().roots_touched().unwrap()),
HuginnMindResponse::Deferred(DeferredAnswer { HuginnMindResponse::Deferred(DeferredAnswer {
manifest: cultnet_rs::pack_content("huginn.mind_response", "package", "", "", "", b"body", 4) manifest: cultnet_rs::pack_content("huginn.mind_response", "package", "", "", "", b"body", 4)
.unwrap() .unwrap()
@ -480,7 +488,7 @@ mod tests {
#[test] #[test]
fn the_wire_vocabulary_is_the_minds_methods_and_status_is_derived_from_the_response() { 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(); 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() { for request in requests() {
let declared = request.instance().is_none(); let declared = request.instance().is_none();
assert_eq!(declared, matches!(request, HuginnMindRequest::Whoami), "{request:?}"); assert_eq!(declared, matches!(request, HuginnMindRequest::Whoami), "{request:?}");

View file

@ -54,13 +54,14 @@ use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow, bail}; use anyhow::{Result, anyhow, bail};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use cultcache_rs::{CultCache, DatabaseEntry, SingleFileMessagePackBackingStore}; use cultcache_rs::{CultCache, DatabaseEntry, SingleFileMessagePackBackingStore};
use cultnet_rs::Selection; use cultnet_rs::{Incoming, Selection};
use eureka_pipeline::{ use eureka_pipeline::{
Date, Label, Line, MemoryOrigin, MemorySort, OrgRepo, PipelineDocument, PipelineFollowUp, PipelineKind, PipelineMemory, PipelinePulse, PipelineRef, PipelineResolution, ResolutionOutcome, RunOperator, RunTurn, SALIENCE_MAX_PERMILLE, Short, Slug, 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, TEMPO_VOTE_MAX_S, TEMPO_VOTE_MIN_S, URGENCY_MAX_PERMILLE, pipeline_key,
}; };
use eureka_state::HuginnClient; 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::{Deserialize, Serialize};
use serde_json::{Value, json}; use serde_json::{Value, json};
use uuid::Uuid; use uuid::Uuid;
@ -1059,7 +1060,8 @@ impl<C: ControlSource, S: ControlSource> PersonaInbox<'_, C, S> {
let names = sender_names(deliveries); let names = sender_names(deliveries);
let goals = render_responsibilities(organs.mind, &instance, &names)?; let goals = render_responsibilities(organs.mind, &instance, &names)?;
self.repos = goals.repos.clone(); 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 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 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)?; let recalled = recall::select(organs.mind, &cue)?;
@ -1114,17 +1116,19 @@ impl<C: ControlSource, S: ControlSource> 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 /// 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,
/// last three days, as the wear section, and the cursor the later reads continue from. A read that fails leaves the cursor unset, /// what the forge cannot show, what others changed in the last three days, the cursor the later reads continue from, then what
/// puts one line in the section saying repo facts are missing, and never fails the session; a Mind with no repos asks nothing. /// her own mind holds of her machine (`record_wear_facts`). A read that fails leaves the cursor unset, puts one line in the
fn first_repo_read(&mut self) -> String { /// 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<WearFact>) {
if self.repos.is_empty() { if self.repos.is_empty() {
return String::new(); return (0, Vec::new());
} }
let organs = self.organs; let organs = self.organs;
let now = unix_ms(organs.clock.now()); let now = unix_ms(organs.clock.now());
self.repo_read_due_ms = now + REPO_READ_EVERY_S * 1000; 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) => { Ok(activity) => {
self.repo_head = Some(activity.head); self.repo_head = Some(activity.head);
wear_facts(&activity, &self.repos, organs.mind.instance(), now) wear_facts(&activity, &self.repos, organs.mind.instance(), now)
@ -1134,7 +1138,8 @@ impl<C: ControlSource, S: ControlSource> PersonaInbox<'_, C, S> {
vec![WearFact::Missing] 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 /// 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 open = Selection { descending: true, ..in_force(of_kind(PipelineKind::FollowUp)) };
let follow_ups = headers(mind, &open)?; let follow_ups = headers(mind, &open)?;
let mut dropped = 0; 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<String> = 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() { if !follow_ups.is_empty() {
out.push_str("Open follow-ups, newest first:\n"); 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 { if dropped_rulings > 0 {
out.push_str(&format!("({dropped_rulings} more cited rulings not shown.)\n")); 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 /// The responsibilities section, and the ids of the follow-ups and rulings it lists, which recall
@ -1915,6 +1928,8 @@ struct Responsibilities {
people: Vec<String>, people: Vec<String>,
/// Her stewardships in force, by repo: the only map from her mind to the repos the forge is asked about. /// Her stewardships in force, by repo: the only map from her mind to the repos the forge is asked about.
repos: Vec<OrgRepo>, repos: Vec<OrgRepo>,
/// The campaigns that hold a follow-up in force in her mind, whether or not the bound showed its line.
open_roots: Vec<String>,
} }
/// Her latest runs with how each ended, one entry each, newest first. A Self /// 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<String>
/// The most the wear section may take of a first line. /// The most the wear section may take of a first line.
const WEAR_MAX_BYTES: usize = 1536; 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. /// A fact about her machine from the forge.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -2174,22 +2194,47 @@ enum WearFact {
Missing, Missing,
/// The read stopped at a repo's page limit: the older entries there were not read. /// The read stopped at a repo's page limit: the older entries there were not read.
Capped(String), 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 }, 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. /// 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<String> }, Unseen { repo: String, redirect: Option<String> },
Changed { repo: String, words: String, at: u64 }, Changed { repo: String, words: String, at: u64 },
Yours { repo: String, number: u32, at: u64 }, 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 (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<FindingRow>),
/// 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, 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,
worst_blocking: Option<(String, u64)>,
} }
impl WearFact { impl WearFact {
/// The priority order a full section drops from the end of. /// The priority order a full section drops from the end of.
fn rank(&self) -> u8 { fn rank(&self) -> u8 {
match self { match self {
Self::Missing | Self::Capped(_) => 0, Self::Missing | Self::RecordsMissing(_) | Self::Capped(_) => 0,
Self::Awaiting { .. } => 1, Self::Awaiting { .. } => 1,
Self::Unseen { .. } => 2, Self::Unseen { .. } => 2,
Self::Changed { .. } => 3, Self::Changed { .. } => 3,
Self::Yours { .. } => 4, Self::Yours { .. } => 4,
Self::SpecsOnYours(_) => 5,
Self::SpecsElsewhere { .. } => 6,
Self::SoulFindings(_) => 7,
Self::QuietCampaigns(_) => 8,
} }
} }
} }
@ -2200,6 +2245,12 @@ fn plain(name: &str) -> String {
name.chars().map(|c| if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | '/') { c } else { '?' }).collect() 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::<Vec<_>>().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 /// 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. /// 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<WearFact> { fn wear_facts(activity: &RepoActivity, repos: &[OrgRepo], instance: &Slug, now_ms: u64) -> Vec<WearFact> {
@ -2232,6 +2283,148 @@ fn wear_facts(activity: &RepoActivity, repos: &[OrgRepo], instance: &Slug, now_m
facts 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<String>, 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::<Vec<_>>().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 record id, its campaign, its repo, and the revision's admission.
struct UnreportedSpec {
id: 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> {
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<Vec<(String, u64)>> {
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<Vec<UnreportedSpec>> {
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 { 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 { id: plain_id(&found.id.id.0), campaign, repo: repo.identity(), at });
}
Ok(specs)
}
/// 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<Vec<FindingRow>> {
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<String, FindingRow> = BTreeMap::new();
// Each row's held worst blocking finding's severity, as an index into BLOCKING (0 the worst).
let mut held_rank: BTreeMap<String, usize> = 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, worst_blocking: None });
row.open += 1;
row.oldest = row.oldest.min(at);
if let Some(rank) = BLOCKING.iter().position(|name| *name == format!("{severity:?}")) {
row.blocking += 1;
// 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));
}
}
}
let mut rows: Vec<FindingRow> = rows.into_values().collect();
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)
}
/// 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<WearFact> {
let hers: Vec<String> = 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 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"));
}
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.id.clone(), spec.at)).collect()));
}
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()) {
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. /// `(N h)` under a day, `(N d)` from a day on.
fn age_text(now_ms: u64, at_ms: u64) -> String { fn age_text(now_ms: u64, at_ms: u64) -> String {
let hours = now_ms.saturating_sub(at_ms) / 3_600_000; let hours = now_ms.saturating_sub(at_ms) / 3_600_000;
@ -2257,6 +2450,45 @@ 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::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::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::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 { 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| 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();
let more = |left: usize, from: usize| {
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 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"))
)
}
}) })
.collect(); .collect();
// The tail line is "(N more facts not shown)"; keep room for it whenever a fact is dropped. // The tail line is "(N more facts not shown)"; keep room for it whenever a fact is dropped.

View file

@ -4553,11 +4553,17 @@ struct Forged<'a> {
script: RefCell<VecDeque<Option<RepoActivity>>>, script: RefCell<VecDeque<Option<RepoActivity>>>,
asked: RefCell<Vec<Option<u64>>>, asked: RefCell<Vec<Option<u64>>>,
head: Cell<u64>, head: Cell<u64>,
/// Every read of her mind, by the schema it names (`roots` for the roots read), in order.
reads: RefCell<Vec<String>>,
} }
impl<'a> Forged<'a> { impl<'a> Forged<'a> {
fn over(mind: &'a TestMind, script: Vec<Option<RepoActivity>>) -> Self { fn over(mind: &'a TestMind, script: Vec<Option<RepoActivity>>) -> 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<huginn_mind::PipelineSelectionPage> { fn query(&self, selection: &cultnet_rs::Selection) -> Result<huginn_mind::PipelineSelectionPage> {
self.reads.borrow_mut().extend(selection.schemas.iter().flatten().cloned());
self.mind.query(selection) self.mind.query(selection)
} }
fn roots_touched(&self) -> Result<Vec<RootTouched>> {
self.reads.borrow_mut().push("roots".into());
self.mind.roots_touched()
}
fn admit(&self, agent: &str, session: &str, documents: Vec<PipelineDocument>) -> Result<AdmissionOutcome> { fn admit(&self, agent: &str, session: &str, documents: Vec<PipelineDocument>) -> Result<AdmissionOutcome> {
self.mind.admit(agent, session, documents) self.mind.admit(agent, session, documents)
} }
@ -4959,3 +4971,606 @@ fn the_wear_bound_is_exact_to_the_byte() {
assert!(over.len() <= WEAR_MAX_BYTES, "{} bytes: {over}", over.len()); assert!(over.len() <= WEAR_MAX_BYTES, "{} bytes: {over}", over.len());
assert!(over.trim_end().ends_with("more facts not shown)"), "{over}"); 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<PipelineDocument> {
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<PipelineDocument>, 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<Vec<String>>) -> 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<PipelineDocument>) {
let outcome = mind.put_as(huginn_mind::Faculty::Soul, documents);
assert!(matches!(outcome, AdmissionOutcome::Committed { .. }), "{outcome:?}");
}
fn hers() -> Vec<OrgRepo> {
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: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),
worst_blocking: Some(("eureka-body:finding:cut-f.s1.one".into(), t(4))),
}]),
]
);
let wear = render_wear(t(0), &facts);
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();
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), 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, 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,
/// 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 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"),
] {
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 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}");
}
/// 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());
hidden.item = Line("x".repeat(900));
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: {} bytes", goals.text.len());
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::<Vec<_>>();
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 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"));
}
// Rows arrive blocking first: c0 blocks most. Each blocking row names its worst 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,
worst_blocking: (k < 3).then(|| (format!("c{k}:finding:cut-v.s1.b"), now - 3 * 3_600_000)),
})
.collect::<Vec<_>>()
};
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"));
// 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
/// 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<WearFact> = 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 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 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, 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 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, High, 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 - 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));
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 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.
#[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,
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) };
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<WearFact> = 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("xx"), "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() {
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::<usize>();
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: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");
}
/// 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 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"),
] {
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:?}");
}
}

View file

@ -17,7 +17,7 @@ use eureka_state::{ClientError, HuginnClient};
use huginn_mind::{ use huginn_mind::{
Faculty, HuginnMindRequest, HuginnMindResponse, PipelineAdmissionBatch, Faculty, HuginnMindRequest, HuginnMindResponse, PipelineAdmissionBatch,
PipelineAdmissionOutcome, PipelineDocumentSummary, PipelineFacts, PipelineProvenance, 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. /// 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<u64>) -> Result<RepoActivity> { fn stewarded_activity(&self, _repos: &[OrgRepo], _after: Option<u64>) -> Result<RepoActivity> {
bail!("this mind port has no forge to read") 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<Vec<RootTouched>> {
bail!("this mind port has no roots to read")
}
/// Admits `documents` as one batch, attributed to `agent` in `session`. /// Admits `documents` as one batch, attributed to `agent` in `session`.
fn admit(&self, agent: &str, session: &str, documents: Vec<PipelineDocument>) -> Result<PipelineAdmissionOutcome>; fn admit(&self, agent: &str, session: &str, documents: Vec<PipelineDocument>) -> Result<PipelineAdmissionOutcome>;
} }
@ -104,6 +109,15 @@ impl MindPort for HuginnMind<'_> {
} }
} }
fn roots_touched(&self) -> Result<Vec<RootTouched>> {
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<PipelineDocument>) -> Result<PipelineAdmissionOutcome> { fn admit(&self, agent: &str, session: &str, documents: Vec<PipelineDocument>) -> Result<PipelineAdmissionOutcome> {
let batch = PipelineAdmissionBatch { let batch = PipelineAdmissionBatch {
instance: self.instance().clone(), instance: self.instance().clone(),
@ -575,6 +589,38 @@ mod tests {
assert!(format!("{:#}", port.stewarded_activity(&repos, None).unwrap_err()).contains("something else")); 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] #[test]
fn runs_of_another_item_are_not_this_items_runs() { fn runs_of_another_item_are_not_this_items_runs() {

View file

@ -135,10 +135,15 @@ impl TestMind {
/// Admits as Hands, one second after the last admission. /// Admits as Hands, one second after the last admission.
pub(crate) fn put(&self, documents: Vec<PipelineDocument>) -> PipelineAdmissionOutcome { pub(crate) fn put(&self, documents: Vec<PipelineDocument>) -> 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<PipelineDocument>) -> PipelineAdmissionOutcome {
self.now.set(self.now.get() + Duration::seconds(1)); self.now.set(self.now.get() + Duration::seconds(1));
let batch = PipelineAdmissionBatch { let batch = PipelineAdmissionBatch {
instance: self.instance.clone(), 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, documents,
}; };
self.mind.borrow_mut().admit(batch, self.now.get()) self.mind.borrow_mut().admit(batch, self.now.get())
@ -157,6 +162,10 @@ impl TestMind {
Err(refusal) => HuginnMindResponse::Refused(refusal), Err(refusal) => HuginnMindResponse::Refused(refusal),
}, },
HuginnMindRequest::Admit(batch) => HuginnMindResponse::Admit(self.mind.borrow_mut().admit(batch, self.now.get())), 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:?}"), other => panic!("the tests ask nothing else: {other:?}"),
} }
} }
@ -207,6 +216,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![] }) 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<Vec<huginn_mind::RootTouched>> {
self.mind.borrow().roots_touched().map_err(|_| anyhow!("refused"))
}
fn admit(&self, agent: &str, session: &str, documents: Vec<PipelineDocument>) -> Result<PipelineAdmissionOutcome> { fn admit(&self, agent: &str, session: &str, documents: Vec<PipelineDocument>) -> Result<PipelineAdmissionOutcome> {
self.now.set(self.now.get() + Duration::seconds(1)); self.now.set(self.now.get() + Duration::seconds(1));
let batch = PipelineAdmissionBatch { let batch = PipelineAdmissionBatch {
@ -295,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<PipelineDocument> {
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 { fn target() -> PipelineDocument {
PipelineDocument::Target(PipelineTarget { PipelineDocument::Target(PipelineTarget {
campaign: Slug(CAMPAIGN.into()), campaign: Slug(CAMPAIGN.into()),

View file

@ -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 HuginnMind < 'static >: fn new (client : HuginnClient) -> Self
mind_body::queue: impl MindPort for HuginnMind < '_ > mind_body::queue: impl MindPort for HuginnMind < '_ >
mind_body::queue: impl< 'a > HuginnMind < 'a > 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 Held # [derive (Clone , Debug , PartialEq , Eq)] pub-fields[]
mind_body::queue: struct HuginnMind< 'a > pub-fields[] mind_body::queue: struct HuginnMind< 'a > pub-fields[]
mind_body::recall: impl Lane mind_body::recall: impl Lane

View file

@ -193,6 +193,27 @@
"required": [ "required": [
"StewardedActivity" "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": { "$defs": {

View file

@ -125,6 +125,21 @@
"Activity" "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.", "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", "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": { "RulingAuthority": {
"type": "string", "type": "string",
"enum": [ "enum": [