diff --git a/Cargo.lock b/Cargo.lock index 45d9d79..685b066 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,6 +1160,7 @@ source = "git+https://eureka.gamecult.org/forge/GameCult/ModelConnector.git?rev= dependencies = [ "aes-gcm", "base64", + "cultcache-rs 0.3.1", "cultnet-rs 0.2.0", "rand_core 0.6.4", "rmp-serde", diff --git a/crates/eureka-state/src/bin/huginn/main.rs b/crates/eureka-state/src/bin/huginn/main.rs index 635d76c..4a5d159 100644 --- a/crates/eureka-state/src/bin/huginn/main.rs +++ b/crates/eureka-state/src/bin/huginn/main.rs @@ -22,17 +22,13 @@ mod render; mod schema; mod trouble; -use std::cell::{Cell, RefCell}; use std::io::Write; use std::process::ExitCode; use std::sync::OnceLock; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; use cultnet_rs::{PROJECTION_DOCUMENT, Selection}; -use eureka_state::hold; -use huginn_mind::eureka_pipeline::{ - Line, PipelineDocument, PipelineRef, Short, Slug, pipeline_key, -}; +use huginn_mind::eureka_pipeline::{Line, PipelineDocument, PipelineRef, Short, Slug, pipeline_key}; use huginn_mind::{ BATCH_MAX, Faculty, HuginnMindRequest, HuginnMindResponse, MindRefusal, PipelineAdmissionBatch, PipelineAdmissionOutcome, PipelinePageItems, PipelineProvenance, PipelineSelectionPage, SemanticQuery, SourceQuery, @@ -186,15 +182,7 @@ fn run(settings: &Settings, command: Command) -> Result { let semantic = near.map(|text| SemanticQuery { text: Line(text), top_k: top_k.unwrap_or(DEFAULT_TOP_K) }); query(settings, selection, semantic, None, documents) } - Command::Sources { - text, - repo, - top_k, - json, - } => sources(settings, text, repo, top_k, json), - Command::Recipe(Recipe::Blocked { spec_id, page }) => { - blocked(settings, &spec_id, page.cursor, page.documents) - } + Command::Sources { text, repo, top_k, json } => sources(settings, text, repo, top_k, json), Command::Recipe(recipe) => { let ask = recipe.ask()?; query(settings, door::selection_value(&ask.selection)?, ask.semantic, ask.page.cursor, ask.page.documents) @@ -276,81 +264,9 @@ fn query( } } -/// The open questions that hold a spec, and the answered-`retire` ones whose -/// claims are not yet resolved: `eureka_state::hold`'s answer, the one the -/// heartbeat's gate reads, printed as a selection of those questions. -fn blocked( - settings: &Settings, - spec_id: &str, - cursor: Option, - documents: bool, -) -> Result { - let spec = PipelineRef { - kind: door::kind_of(spec_id)?, - id: Short(spec_id.to_string()), - }; - let failure: RefCell> = RefCell::new(None); - let as_of = Cell::new(0); - let ask = |selection: &Selection| -> anyhow::Result { - let selection = selection.clone(); - match settings.ask(|instance| HuginnMindRequest::Query { - instance: instance.clone(), - selection, - semantic: None, - }) { - Ok(HuginnMindResponse::Query(page)) => { - as_of.set(page.as_of); - Ok(page) - } - Ok(_) => Err(anyhow::anyhow!( - "the mind answered a query with something else" - )), - Err(trouble) => { - *failure.borrow_mut() = Some(trouble); - Err(anyhow::anyhow!("the mind could not be asked")) - } - } - }; - let holding = match hold::holds(&ask) { - Ok(holds) => holds, - Err(error) => { - return Err(failure - .take() - .unwrap_or_else(|| Trouble::Internal(error.to_string()))); - } - }; - let keys: Vec = holding - .iter() - .filter(|held| held.items.contains(&spec)) - .map(|held| held.question.id.id.0.clone()) - .collect(); - if keys.is_empty() { - say(format_args!("matched 0 as_of {} next -", as_of.get())); - return Ok(0); - } - let selection = Selection { - keys: Some(keys), - ..Selection::default() - }; - query(settings, selection, None, cursor, documents) -} - -fn sources( - settings: &Settings, - text: String, - repo: Option, - top_k: Option, - json: bool, -) -> Result { - let query = SourceQuery { - text: Line(text), - top_k: top_k.unwrap_or(DEFAULT_TOP_K), - repo: repo.map(Short), - }; - match settings.ask(|instance| HuginnMindRequest::SearchSources { - instance: instance.clone(), - query, - })? { +fn sources(settings: &Settings, text: String, repo: Option, top_k: Option, json: bool) -> Result { + let query = SourceQuery { text: Line(text), top_k: top_k.unwrap_or(DEFAULT_TOP_K), repo: repo.map(Short) }; + match settings.ask(|instance| HuginnMindRequest::SearchSources { instance: instance.clone(), query })? { HuginnMindResponse::Sources(found) => { if json { say(serde_json::to_string_pretty(&found).map_err(|error| Trouble::Internal(error.to_string()))?); @@ -508,8 +424,6 @@ fn unexpected(answer: &HuginnMindResponse) -> Trouble { HuginnMindResponse::Persona(_) => "a persona", 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/eureka-state/src/bin/huginn/recipes.rs b/crates/eureka-state/src/bin/huginn/recipes.rs index 7d1c3b2..78bf092 100644 --- a/crates/eureka-state/src/bin/huginn/recipes.rs +++ b/crates/eureka-state/src/bin/huginn/recipes.rs @@ -1,11 +1,8 @@ //! The campaign progress view as named subcommands. Each recipe is exactly the //! selection `references/campaign-state.md` gives (Eureka 278b881, "Recipes"), //! built per call and never cached. Each one's `--help` prints its selection. -//! The one exception is `blocked`, which asks what a question holds and so -//! 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}; @@ -17,6 +14,8 @@ 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 { @@ -67,7 +66,7 @@ pub enum Recipe { #[command(flatten)] page: Paging, }, - /// Questions that hold a spec, answered-retire ones included: the spec waits on the operator. + /// Open questions raised in a spec: the spec waits on the operator. Blocked { spec_id: String, #[command(flatten)] @@ -126,12 +125,7 @@ impl Recipe { Self::Open { campaigns, page } => (open(&campaigns), None, page), Self::Findings { campaign, blocking, page } => (findings(&campaign, blocking), None, page), Self::Unreported { campaign, page } => (unreported(&campaign), None, page), - // Several selections, not one: `main` answers it through `eureka_state::hold`. - Self::Blocked { .. } => { - return Err(Trouble::Internal( - "blocked is answered by the hold, not by one selection".into(), - )); - } + Self::Blocked { spec_id, page } => (blocked(&spec_id), None, page), Self::Unjudged { campaign, page } => (unjudged(&campaign), None, page), Self::History { id, page } => (history(door::kind_of(&id)?.type_id(), &id), None, page), Self::Cut { campaign, label, page } => (cut(&campaign, &label), None, page), @@ -204,6 +198,14 @@ fn unreported(campaign: &str) -> Value { }) } +fn blocked(spec_id: &str) -> Value { + json!({ + "schemas": [schema(PipelineKind::Question)], + "fields": [in_force()], + "cites": { "target": { "schemaId": schema(PipelineKind::CutSpec), "recordKey": spec_id }, "role": "raised_in" }, + }) +} + fn unjudged(campaign: &str) -> Value { json!({ "schemas": [schema(PipelineKind::CutReport)], @@ -255,18 +257,6 @@ fn gaps() -> Value { }) } -/// `blocked` is not one selection: it reads the in-force questions and, for each, -/// what it holds, by `eureka_state::hold`, the decider the heartbeat's gate reads. -fn blocked_help() -> String { - let questions = json!({ "schemas": [schema(PipelineKind::Question)], "fields": [in_force()] }); - format!( - "selection (first of several): -{} -A question holds the item it is raised in, or every claim of the run it is raised in; one answered `retire` keeps holding the claims still in force. The recipe prints the questions that hold .", - serde_json::to_string_pretty(&questions).unwrap_or_default() - ) -} - /// Each recipe's `--help` addendum: its selection with placeholders in the /// arguments' places. The same builders as execution, so help cannot drift /// from what runs. @@ -286,7 +276,7 @@ pub fn help() -> Vec<(&'static str, String)> { ("open", selection(open(&[c()]))), ("findings", findings_help), ("unreported", selection(unreported(&c()))), - ("blocked", blocked_help()), + ("blocked", selection(blocked(""))), ("unjudged", selection(unjudged(&c()))), ("history", selection(history("epiphany.pipeline..v2", ""))), ("cut", selection(cut(&c(), "