From abd28908347683e8c86bef568ede68a2aed92008 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 25 Oct 2026 21:05:02 +0100 Subject: [PATCH] Mind::members: the walk's page size is the mind's by construction The walk selection is built field by field from the caller's filters, so a caller's limit, projection, order or cursor cannot reach it (one select per member at limit 1 measured 300 ms against 14 ms). A test pins it. Unverified at this commit: built and tested once at the end of the batch. Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/query.rs | 47 +++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/crates/huginn-mind/src/query.rs b/crates/huginn-mind/src/query.rs index 6c8d816..3ba5946 100644 --- a/crates/huginn-mind/src/query.rs +++ b/crates/huginn-mind/src/query.rs @@ -23,7 +23,7 @@ use std::collections::{BTreeMap, BTreeSet}; use cultnet_rs::{ - Cursor, EdgeAnchor, EdgeMatch, Evaluation, LIMIT_MAX, LIMIT_MIN, PROJECTION_DOCUMENT, Row, Selection, select, validate, + Cursor, EdgeAnchor, EdgeMatch, Evaluation, LIMIT_MAX, LIMIT_MIN, PROJECTION_DOCUMENT, PROJECTION_HEADER, Row, Selection, select, validate, }; use eureka_pipeline::{ ClaimOutcome, CommitRange, Date, FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, OrgRepo, @@ -517,6 +517,26 @@ impl Mind { refuse_values(selection) } + /// The selection `members` walks: the caller's filters and nothing else. + /// Built field by field, never spread from the caller's selection, so the + /// page size is ours (the caller's `limit` shapes an answer, not the + /// walk's cost: one select per member at limit 1) and the walk is headers + /// only, from the first page, in the one order. + fn walk_selection(selection: Selection) -> Selection { + let Selection { schemas, keys, fields, cites, cited, .. } = selection; + Selection { + schemas, + keys, + fields, + cites, + cited, + projection: PROJECTION_HEADER.into(), + descending: false, + limit: Some(LIMIT_MAX), + cursor: None, + } + } + /// The members of a semantic query: every document the selection matches /// at the head, decided before the index is asked anything. /// @@ -529,7 +549,7 @@ impl Mind { let selection = Self::semantic_selection(selection, semantic)?; let head = receipt::head(self)?; let rows = Reader::at(self, head)?.rows()?; - let mut walk = Selection { limit: Some(LIMIT_MAX), ..selection }; + let mut walk = Self::walk_selection(selection); let mut ids = Vec::new(); loop { let evaluation = select(&Vocabulary, &rows, &walk, head, self.cursor_key())?; @@ -1887,6 +1907,29 @@ mod tests { assert_eq!(mind.members(&limited, &asking(5)).unwrap().ids.len(), 250); } + /// The walk costs one select per 200 members whatever the caller asked + /// for: a caller's `limit`, projection, order or cursor never reach it, + /// and its filters all do. + #[test] + fn the_members_walk_takes_its_page_size_from_the_mind_not_the_caller() { + let questions = of_kinds(&[K::Question]); + let caller = Selection { + limit: Some(1), + projection: PROJECTION_DOCUMENT.into(), + descending: true, + cursor: Some("caller-cursor".into()), + keys: Some(vec!["Q1".into()]), + cited: Some(Incoming { role: "subject".into(), exists: false }), + ..questions + }; + let walk = Mind::::walk_selection(caller.clone()); + assert_eq!(walk.limit, Some(LIMIT_MAX)); + assert_eq!(walk.projection, PROJECTION_HEADER); + assert!(!walk.descending); + assert_eq!(walk.cursor, None); + assert_eq!((walk.schemas, walk.keys, walk.cited), (caller.schemas, caller.keys, caller.cited)); + } + /// `matched` is what the selection admits, not what the index returned: /// two hits among five members still say five. #[test]