From 333e1c4314eec616fa852ef0674fce1b2e2a169c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 16 Sep 2026 22:19:42 +0200 Subject: [PATCH] Give the two failable loosenings entries of their own Cut 9's entries file recorded three weakenings as unfailable. Two of them are reachable in this Body, so they are entries now: - The `semantic` check below `Reader::new` changes the answer for a store the reader refuses -- a doubled receipt, a row that does not decode -- from the semantic refusal to the integrity one. `V8L` is that move, and the semantic test asks a semantic query of a mind whose receipts name one write twice. - `resolutions_of` comparing a subject by `id` alone is reachable because the read side validates no `PipelineRef` it is handed. `V10L` is that comparison, and the subject-history test asks for a `Ruling` ref carrying a question's id after that question has ten resolutions. `assignments_of` ignoring the instance stays recorded as unfailable, with A5 and the derivations scoped to this mind as the reason. `specs_without_report` compares the citation by exact id, but every fixture report cited `.r1`, so the test could not tell an exact id from a cut prefix. cut-9 now carries a report of the revision it superseded and cut-12 is a superseded revision no report names, so standing and citation are each still visible on their own, and `V13L` compares by cut prefix. Fork, not closed here: Soul asked that `view` and `history` validate the ref through the leaf and refuse a malformed one typed rather than answering empty. The leaf's `Bounded` is `pub(crate)` at the pinned rev d5a36c2, so `PipelineRef::validate` cannot be called from this crate and the fix is an Epiphany change. The history test pins today's empty answer and names the reason. Co-Authored-By: Claude Fable 5.1 --- crates/huginn-mind/src/query.rs | 70 +++++++++++++++++++++++++++----- tools/eureka-cut9-mutations.psd1 | 62 +++++++++++++++++++++++----- 2 files changed, 112 insertions(+), 20 deletions(-) diff --git a/crates/huginn-mind/src/query.rs b/crates/huginn-mind/src/query.rs index 99eb1a6..8cf21fb 100644 --- a/crates/huginn-mind/src/query.rs +++ b/crates/huginn-mind/src/query.rs @@ -552,6 +552,14 @@ mod tests { id("resolution", "resolution.question.Q1.n1.n1") ]); + // The scope is the whole ref, kind included: a subject whose kind + // disagrees with its id is a subject this mind has no resolution of, + // even though the id names one with ten. The read side does not + // validate the ref it is handed -- the leaf's validator is not public + // at the pinned rev -- so a malformed scope is an empty history rather + // than a typed refusal. + assert!(mind.history(&HistoryScope::Subject(r(K::Ruling, &q1))).unwrap().is_empty()); + // History lands one record per batch (Cut 12's import constraint), so // ten records carry ten receipts. let receipts = history.iter().map(|entry| entry.admission.receipt_id.clone()).collect::>(); @@ -882,12 +890,22 @@ mod tests { committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A"), question("Q2", &["A", "B"], "A")])); committed(admit(&mut mind, vec![answering("R1", &q2)])); - // cut-9: a superseded r1 and an r2 with no report. + // cut-9: a superseded r1 with a report of its own, and an r2 with none. + // The citation is the exact spec and not its cut, so r2 is open work + // while the cut it belongs to has been reported. committed(admit(&mut mind, vec![D::CutSpec(cut_spec("9", 1))])); + committed(admit(&mut mind, vec![D::CutReport(cut_report("9", 1))])); committed(admit(&mut mind, vec![ D::CutSpec(cut_spec("9", 2)), resolution(r(K::CutSpec, &id("cut_spec", "cut-9.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-9.r2"))])), ])); + // cut-12: a superseded r1 and an r2, neither reported, so standing is + // still what decides which of the two is open work. + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("12", 1))])); + committed(admit(&mut mind, vec![ + D::CutSpec(cut_spec("12", 2)), + resolution(r(K::CutSpec, &id("cut_spec", "cut-12.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-12.r2"))])), + ])); // cut-10: a spec with a report, and that report with no verdict. committed(admit(&mut mind, vec![D::CutSpec(cut_spec("10", 1))])); committed(admit(&mut mind, vec![D::CutReport(cut_report("10", 1))])); @@ -916,21 +934,29 @@ mod tests { assert_eq!(ids(&open.follow_ups), vec![id("follow_up", "FU-1")]); assert_eq!( ids(&open.specs_without_report), - vec![id("cut_spec", "cut-9.r2")], - "a superseded revision is not open, and a spec with a report is not either" + vec![id("cut_spec", "cut-12.r2"), id("cut_spec", "cut-9.r2")], + "a superseded revision is not open, a spec with a report is not either, and the report of the \ + revision it superseded is not this revision's" ); assert_eq!( ids(&open.reports_without_verdict), - vec![id("cut_report", "cut-10.h1"), id("cut_report", "cut-11.h2")], + vec![id("cut_report", "cut-10.h1"), id("cut_report", "cut-11.h2"), id("cut_report", "cut-9.h1")], "a report is not resolvable, so only the verdict that names it, exactly, closes it" ); } - /// D5: `semantic` is refused typed, before any filter runs, and never - /// answered with an empty page that reads like "nothing matched". + /// D5: `semantic` is refused typed, before the image or the receipts are + /// read, and never answered with an empty page that reads like "nothing + /// matched". Where the check sits is the rule: below the reader, a store + /// the reader refuses would answer the integrity fault instead, which is + /// not what the caller asked about. #[test] fn semantic_query_refuses_typed_until_wired() { - let mind = seeded(); + let store = MemoryStore::new(); + let mut mind = opened(store.clone(), INSTANCE); + seed(&mut mind); + let q1 = id("question", "Q1"); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); let query = PipelineQuery { campaign: Some(slug(CAMPAIGN)), repo: Some(repo(REPO)), @@ -943,11 +969,35 @@ mod tests { limit: Some(10), semantic: Some(SemanticQuery { text: "what did we rule about keys".into(), top_k: 5 }), }; + let unwired = MindRefusal::Unavailable { detail: "semantic query: the index is not wired (Cut 11)".into() }; + assert_eq!(mind.query(&query).err(), Some(unwired.clone())); + assert!(mind.query(&PipelineQuery { semantic: None, ..query.clone() }).is_ok()); + + // Two receipts naming one write is a store the reader refuses to build + // over at all. The semantic refusal still lands, because it is decided + // before the image and the receipts are read. + let doubled = MemoryStore::new(); + for row in store.rows() { + doubled.plant(row); + } + let forged = crate::receipt::candidate( + &slug(INSTANCE), + provenance(Faculty::Soul), + &[], + &[prepare(&question("Q1", &["A", "B"], "A")), prepare(&question("Q8", &["A", "B"], "A"))], + now(), + ) + .unwrap(); + doubled.plant(schema_cache().unwrap().prepare_entry_named(&forged.receipt_id, &forged).unwrap().0); + let unreadable = opened(doubled, INSTANCE); assert_eq!( - mind.query(&query).err(), - Some(MindRefusal::Unavailable { detail: "semantic query: the index is not wired (Cut 11)".into() }) + unreadable.query(&PipelineQuery::default()).err(), + Some(MindRefusal::Unavailable { + detail: format!("document {}/{q1} is written by two receipts", K::Question.type_id()), + }), + "the reader refuses this store" ); - assert!(mind.query(&PipelineQuery { semantic: None, ..query }).is_ok()); + assert_eq!(unreadable.query(&query).err(), Some(unwired), "and the semantic refusal precedes reading it"); } /// D4 and D8: `view` is the typed one-document read with its facts, and an diff --git a/tools/eureka-cut9-mutations.psd1 b/tools/eureka-cut9-mutations.psd1 index d64a3bf..54c7b37 100644 --- a/tools/eureka-cut9-mutations.psd1 +++ b/tools/eureka-cut9-mutations.psd1 @@ -15,19 +15,23 @@ # breaks it for the views. V1L is H41's own edit, listed here as well as in # `eureka-cut8-mutations.psd1` -- one mutant, two suites, one owner. # -# Three weakenings the spec names have no entry because they change no -# behaviour this Body can reach, and a mutation that cannot fail is not a -# proof: +# One weakening the spec names has no entry because it changes no behaviour +# this Body can reach, and a mutation that cannot fail is not a proof: # -# - The `semantic` check moved below the filters. It still refuses every query -# that carries one; only the work done before the refusal changes. -# - `resolutions_of` comparing a subject by `id` and ignoring `kind`. A -# pipeline id carries its own kind segment and the leaf refuses a -# `PipelineRef` whose kind and id disagree, so no two subjects of different -# kinds can share an id. # - `assignments_of` comparing the repo and ignoring the instance. A5 refuses # a stewardship naming another instance, so every stewardship a mind holds -# already names that mind. +# already names that mind, and every other caller is a sequence this mind +# derives over its own scope. +# +# The two listed beside it in the spec are failable and have entries. The +# `semantic` check below `Reader::new` changes the answer for a store the +# reader refuses -- a doubled receipt, a row that does not decode -- from the +# semantic refusal to the integrity one, and `V8L` is that move. And +# `resolutions_of` comparing a subject by `id` alone is reachable because the +# read side never validates the `PipelineRef` it is handed: the leaf's +# validator is not public at the pinned rev, so `history(Subject(ref))` and +# `view(ref)` take a ref whose kind and id disagree and answer over it. `V10L` +# is that comparison. @{ Mutations = @( @{ @@ -179,6 +183,28 @@ '@ New = @' return Ok(PipelineQueryPage { items: vec![], matched: 0 }); +'@ + } + @{ + Id = 'V8L' + Rule = 'The semantic refusal is decided before the image and the receipts are read, so a store the reader refuses still answers the question the caller asked.' + Test = 'query::tests::semantic_query_refuses_typed_until_wired' + File = 'crates/huginn-mind/src/query.rs' + Old = @' + if query.semantic.is_some() { + return Err(MindRefusal::Unavailable { + detail: "semantic query: the index is not wired (Cut 11)".into(), + }); + } + let reader = Reader::new(self)?; +'@ + New = @' + let reader = Reader::new(self)?; + if query.semantic.is_some() { + return Err(MindRefusal::Unavailable { + detail: "semantic query: the index is not wired (Cut 11)".into(), + }); + } '@ } @{ @@ -203,6 +229,14 @@ .map(|(key, resolution)| { let _ = subject; (resolution.sequence, key.to_string()) }) '@ } + @{ + Id = 'V10L' + Rule = 'A subject is selected by the whole ref, kind included: the read side validates no ref it is handed, so a kind that disagrees with its id selects nothing rather than that id''s records.' + Test = 'query::tests::a_subjects_history_lists_every_resolution_with_its_status_and_receipt' + File = 'crates/huginn-mind/src/docs.rs' + Old = ' self.resolutions().filter(move |(_, resolution)| resolution.subject == *subject)' + New = ' self.resolutions().filter(move |(_, resolution)| resolution.subject.id == subject.id)' + } @{ Id = 'V11' Rule = 'A repo''s history is selected by (this mind, this repo), so another repo''s assignments are another history.' @@ -233,6 +267,14 @@ Old = ' specs_without_report: in_force(PipelineKind::CutSpec)' New = ' specs_without_report: of_kind(PipelineKind::CutSpec).cloned().collect::>()' } + @{ + Id = 'V13L' + Rule = 'A spec is reported by a report naming its exact id, not by any report of its cut: a report of the revision this one superseded is not this revision''s report.' + Test = 'query::tests::open_items_are_derived_from_in_force_and_citation' + File = 'crates/huginn-mind/src/query.rs' + Old = ' .filter(|view| !reported.contains(&view.id.id.0.as_str()))' + New = ' .filter(|view| !reported.iter().any(|cited| root_and_local(cited).1.split(''.'').next() == root_and_local(&view.id.id.0).1.split(''.'').next()))' + } @{ Id = 'V14' Rule = 'A report is closed by the verdict that names it; a report is not resolvable, so nothing else closes it.'