From 8c98b7b02877ed20933fd83d2ead7ed54dcbb4d1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 25 Oct 2026 22:16:07 +0100 Subject: [PATCH] Members walk: drop the dead projection assignment; pin the cites role through members The walk never reads projection (select reads it only in validate and the cursor digest), so the assignment and its "headers only" comment decided nothing. A new test cites Q1 through two roles by rows of one schema and asserts exact members and matched for subject, superseded_by and no role. Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/query.rs | 38 +++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/crates/huginn-mind/src/query.rs b/crates/huginn-mind/src/query.rs index f76b5aa..f39df86 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, PROJECTION_HEADER, Row, Selection, select, validate, + Cursor, EdgeAnchor, EdgeMatch, Evaluation, LIMIT_MAX, LIMIT_MIN, PROJECTION_DOCUMENT, Row, Selection, select, validate, }; use eureka_pipeline::{ ClaimOutcome, CommitRange, Date, FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, OrgRepo, @@ -534,8 +534,8 @@ impl Mind { // The walk is built here, field by field, from the caller's filters // alone and never spread from the caller's selection: 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. + // one select per member at limit 1), from the first page, in the + // one order. let Selection { schemas, keys, fields, cites, cited, .. } = selection; let mut walk = Selection { schemas, @@ -543,10 +543,10 @@ impl Mind { fields, cites, cited, - projection: PROJECTION_HEADER.into(), descending: false, limit: Some(LIMIT_MAX), cursor: None, + ..Selection::default() }; let mut ids = Vec::new(); let mut pages = 0; @@ -1951,6 +1951,36 @@ mod tests { assert_eq!(ids(&ranked.page), vec![fu("FU-1")]); } + /// The walk keeps the role of the caller's `cites` hop: Q1 is cited by two + /// resolutions, one as its subject and one as what supersedes another + /// question, and a hop through `subject` admits only the first. + #[test] + fn a_cites_hop_keeps_its_role_in_the_members() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A"), question("Q2", &["A", "B"], "A")])); + let (q1, q2) = (id("question", "Q1"), id("question", "Q2")); + committed(admit(&mut mind, vec![answering("R1", &q1)])); + committed(admit(&mut mind, vec![resolution(r(K::Question, &q2), superseded(&[r(K::Question, &q1)]))])); + let (as_subject, as_superseder) = (id("resolution", "question.Q1.n1"), id("resolution", "question.Q2.n1")); + let through = |role: Option<&str>| Selection { + cites: Some(cites(K::Question, &q1, role)), + ..of_kinds(&[K::Resolution]) + }; + let hits = [hit(K::Resolution, &as_superseder, 0.9), hit(K::Resolution, &as_subject, 0.8)]; + + let subject = through(Some("subject")); + assert_eq!(mind.members(&subject, &asking(5)).unwrap().ids, vec![r(K::Resolution, &as_subject)]); + let ranked = rank_all(&mind, &subject, &asking(5), &hits); + assert_eq!(ranked.page.matched, 1); + assert_eq!(ids(&ranked.page), vec![as_subject.clone()]); + + let superseded_by = through(Some("superseded_by")); + assert_eq!(mind.members(&superseded_by, &asking(5)).unwrap().ids, vec![r(K::Resolution, &as_superseder)]); + + let any = mind.members(&through(None), &asking(5)).unwrap(); + assert_eq!(any.ids, vec![r(K::Resolution, &as_subject), r(K::Resolution, &as_superseder)]); + } + /// `matched` is what the selection admits, not what the index returned: /// two hits among five members still say five. #[test]