diff --git a/crates/huginn-mind/tests/desk_feed_fixtures.rs b/crates/huginn-mind/tests/desk_feed_fixtures.rs index 399c82b..15e050f 100644 --- a/crates/huginn-mind/tests/desk_feed_fixtures.rs +++ b/crates/huginn-mind/tests/desk_feed_fixtures.rs @@ -28,6 +28,8 @@ const RUNTIME: &str = "huginn-eureka"; /// other decider). Synthetic values. const READER: &str = "5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6f"; const OTHER_DECIDER: &str = "0b9d7e3a-6c21-4f58-a0d4-9e8f7a6b5c4d"; +/// A member who answered but is not a decider: their answer is stored `Refused(not_a_decider)`. +const OUTSIDER: &str = "c3a1f2e4-7b6d-4e8f-9a0b-1c2d3e4f5a6b"; /// A question Eureka's mind holds (asked 2026-11-11). The answers, notes and hash in `record()` are /// synthetic: the hash is CultLib's sample digest, not this question's. const FORK: &str = "eureka-substrate:question:desk-listing-authority"; @@ -102,33 +104,40 @@ fn record() -> ForkRecord { answers: vec![ answer("n1", "Counted", None, READER, "The asker lists her own forks.", "2026-11-11T17:00:00Z"), answer("n2", "Revision", Some("n1"), OTHER_DECIDER, "Agreed.", "2026-11-11T17:05:00Z"), + ForkDeskAnswer { code: Some(huginn_mind::desk_wire::ForkAnswerCode::NotADecider), ..answer("n3", "Refused", None, OUTSIDER, "Me too.", "2026-11-11T17:06:00Z") }, ], } } -// ---- PROPOSED reader views (ruling: the door withholds account ids from readers who are not -// deciders, and returns counts). CultLib's card and desk_answer cannot carry this yet (card -// `deciders` has minItems 1 and no count key; `answerer` is a required non-null id), so these are -// fragments in the shape proposed to Libby (follow-up `cultlib-fork-card-reader-counts`), built from -// mirror types that exist only here. They are not Huginn's output until that contract lands and -// `cut-desk-feed-listing` builds it. ---- +// ---- Reader views, as MIRROR TYPES (ruling desk-read-visibility-withheld-ids; shape ruling +// desk-reader-counts-shape, Libby's CultLib reader-counts cut). These types exist only here: Huginn +// does not project reader views yet (cut-desk-feed-listing), and CultLib's re-pinned vectors are not +// posted yet. They pin the rules, not CultLib's bytes; when Libby posts the sha, her vectors are +// decoded against Huginn's real types instead. ---- -/// The two card keys the proposal changes, as a fragment: a decider gets the ids, anyone else `[]` -/// and the count. Not a card. +/// The card's `deciders` and `decider_count` (right after `deciders`), as a fragment. Not a card. #[derive(Serialize)] struct CardViewers { deciders: Vec, decider_count: u64, } +/// What the call's deciders look like to one reader: the ids for a decider, `[]` (withheld) for +/// anyone else; the count either way. fn card_viewers(decider: bool) -> CardViewers { let ids = vec![READER.to_string(), OTHER_DECIDER.to_string()]; CardViewers { decider_count: ids.len() as u64, deciders: if decider { ids } else { Vec::new() } } } -/// `gamecult.desk_answer.v0` as proposed for a reader who is not a decider: `answerer` nil, key kept. +/// The card rule: `decider_count` is 1..64, and `deciders` is either empty (withheld) or holds +/// exactly `decider_count` ids. +fn card_viewers_hold(view: &CardViewers) -> bool { + (1..=64).contains(&view.decider_count) && (view.deciders.is_empty() || view.deciders.len() as u64 == view.decider_count) +} + +/// `gamecult.desk_answer.v0` with `answerer` nullable (all four variants). #[derive(Serialize)] -struct WithheldAnswer { +struct ViewAnswer { #[serde(rename = "schemaVersion")] schema_version: AnswerSchema, answer_id: String, @@ -148,25 +157,27 @@ struct WithheldAnswer { request_id: String, } -/// `gamecult.fork.record.v0` as proposed for a reader who is not a decider: every `answerer` nil and -/// `answerer_count`, the number of distinct answerers, in their place. +/// `gamecult.fork.record.v0` with `answerer_count`: six keys (header 0x86). The count's position +/// among the six is CultLib's; this mirror puts it before `answers` until the vectors say. #[derive(Serialize)] -struct WithheldRecord { +struct ViewRecord { #[serde(rename = "schemaVersion")] schema_version: RecordSchema, fork_id: String, options_hash: String, counted_answer_id: Option, answerer_count: u64, - answers: Vec, + answers: Vec, } -fn withheld(record: ForkRecord) -> WithheldRecord { +/// One record as one reader sees it. `answerer_count` is the distinct answerers over ALL stored +/// answers, refusals included, for both readers; only a non-decider's view nils `answerer`. +fn view(record: ForkRecord, decider: bool) -> ViewRecord { let mut answerers: Vec<&str> = record.answers.iter().map(|answer| answer.answerer.as_str()).collect(); answerers.sort_unstable(); answerers.dedup(); let answerer_count = answerers.len() as u64; - WithheldRecord { + ViewRecord { schema_version: record.schema_version, fork_id: record.fork_id, options_hash: record.options_hash, @@ -175,7 +186,7 @@ fn withheld(record: ForkRecord) -> WithheldRecord { answers: record .answers .into_iter() - .map(|answer| WithheldAnswer { + .map(|answer| ViewAnswer { schema_version: answer.schema_version, answer_id: answer.answer_id, fork_id: answer.fork_id, @@ -189,7 +200,7 @@ fn withheld(record: ForkRecord) -> WithheldRecord { answered_at: answer.answered_at, received_at: answer.received_at, claim_in_future: answer.claim_in_future, - answerer: None, + answerer: decider.then_some(answer.answerer), authority: answer.authority, request_id: answer.request_id, }) @@ -197,12 +208,18 @@ fn withheld(record: ForkRecord) -> WithheldRecord { } } -/// The proposed fragments by reader kind: (file stem, bytes). +/// The record rule: `answerer` is null exactly in a non-decider's view. +fn record_view_holds(view: &ViewRecord, decider: bool) -> bool { + view.answers.iter().all(|answer| answer.answerer.is_some() == decider) +} + +/// The mirror fragments by reader kind: (file stem, bytes). fn reader_views() -> Vec<(&'static str, Vec)> { vec![ ("card-viewers-decider.synthetic", rmp_serde::to_vec_named(&card_viewers(true)).unwrap()), ("card-viewers-non-decider.synthetic", rmp_serde::to_vec_named(&card_viewers(false)).unwrap()), - ("record-non-decider.proposed", rmp_serde::to_vec_named(&withheld(record())).unwrap()), + ("record-decider.mirror", rmp_serde::to_vec_named(&view(record(), true)).unwrap()), + ("record-non-decider.mirror", rmp_serde::to_vec_named(&view(record(), false)).unwrap()), ] } @@ -349,26 +366,56 @@ fn holds(bytes: &[u8], text: &str) -> bool { bytes.windows(text.len()).any(|window| window == text.as_bytes()) } -/// This test pins MIRROR TYPES ONLY (`CardViewers`, `WithheldRecord`), the shapes proposed to CultLib. -/// It does not exercise Huginn's projection, which does not withhold ids yet. The tests on real list and -/// read responses land with `cut-desk-feed-listing` (r6). +/// MIRROR TYPES ONLY: these tests pin the reader-view rules on the types above, not Huginn's +/// projection (which does not withhold ids yet) and not CultLib's bytes (pending Libby's sha). The +/// tests on real list and read responses land with `cut-desk-feed-listing`. /// -/// A reader who is not a decider gets counts and no account id at all: neither UUID appears -/// anywhere in its fragments' bytes. A decider gets every id. (Proposed shapes; the build cut pins -/// the same on Huginn's real list and read responses.) +/// A non-decider's fragments hold no account id anywhere in their bytes, only counts; a decider's +/// hold every id and the same counts. #[test] fn a_non_decider_gets_counts_and_no_ids_a_decider_gets_ids() { let views = reader_views(); let bytes = |name: &str| views.iter().find(|(stem, _)| *stem == name).unwrap().1.clone(); - for name in ["card-viewers-non-decider.synthetic", "record-non-decider.proposed"] { - let view = bytes(name); - assert!(!holds(&view, READER) && !holds(&view, OTHER_DECIDER), "{name} carries an account id"); + for name in ["card-viewers-non-decider.synthetic", "record-non-decider.mirror"] { + let held = bytes(name); + assert!([READER, OTHER_DECIDER, OUTSIDER].iter().all(|id| !holds(&held, id)), "{name} carries an account id"); } - let non_decider = card_viewers(false); - assert_eq!((non_decider.deciders.len(), non_decider.decider_count), (0, 2)); - assert_eq!(withheld(record()).answerer_count, 2); - let decider = bytes("card-viewers-decider.synthetic"); - assert!(holds(&decider, READER) && holds(&decider, OTHER_DECIDER)); - let whole = rmp_serde::to_vec_named(&record()).unwrap(); - assert!(holds(&whole, READER) && holds(&whole, OTHER_DECIDER), "a decider's record keeps the answerer ids"); + let held = bytes("card-viewers-decider.synthetic"); + assert!(holds(&held, READER) && holds(&held, OTHER_DECIDER)); + let held = bytes("record-decider.mirror"); + assert!([READER, OTHER_DECIDER, OUTSIDER].iter().all(|id| holds(&held, id)), "a decider's record keeps every answerer id"); + assert_eq!(card_viewers(false).decider_count, card_viewers(true).decider_count); +} + +/// `answerer_count` counts distinct answerers over every stored answer, the refused one included, +/// and is the same for both readers; a record view has six keys (header 0x86). +#[test] +fn answerer_count_covers_refusals_and_the_record_has_six_keys() { + assert_eq!(record().answers.iter().filter(|answer| answer.outcome == "Refused").count(), 1); + assert_eq!((view(record(), false).answerer_count, view(record(), true).answerer_count), (3, 3)); + for name in ["record-decider.mirror", "record-non-decider.mirror"] { + let held = reader_views().into_iter().find(|(stem, _)| *stem == name).unwrap().1; + assert_eq!(held[0], 0x86, "{name}"); + } +} + +/// `decider_count` is 1..64; `deciders` is empty (withheld) or holds exactly `decider_count` ids; +/// `answerer` is null exactly in a non-decider's view. +#[test] +fn a_non_empty_decider_list_matches_its_count_and_answerer_is_null_only_for_non_deciders() { + assert!(card_viewers_hold(&card_viewers(true)) && card_viewers_hold(&card_viewers(false))); + let ids = |n: usize| (0..n).map(|at| format!("00000000-0000-4000-8000-{at:012x}")).collect::>(); + for (deciders, decider_count, holds) in [ + (ids(1), 2, false), + (ids(3), 2, false), + (Vec::new(), 0, false), + (Vec::new(), 65, false), + (ids(64), 64, true), + (Vec::new(), 64, true), + (ids(1), 1, true), + ] { + assert_eq!(card_viewers_hold(&CardViewers { deciders, decider_count }), holds); + } + assert!(record_view_holds(&view(record(), true), true) && record_view_holds(&view(record(), false), false)); + assert!(!record_view_holds(&view(record(), true), false) && !record_view_holds(&view(record(), false), true)); } diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/README.md b/crates/huginn-mind/tests/fixtures/desk-feed/README.md index 0525883..de9b8cf 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/README.md +++ b/crates/huginn-mind/tests/fixtures/desk-feed/README.md @@ -35,10 +35,13 @@ lands (see `docs/desk-feed-door.md`). | `unavailable.response` | Huginn to Bifrost | `Refused` `Unavailable` (Bifrost: 503 for this Mind) | | `failure.response` | Huginn to Bifrost | operation failure, `payloadSchema` `gamecult.cultnet.operation_failure.v1` (Bifrost: 503) | | `error.response` | Huginn to Bifrost | CultNet `Error` message, no payload (Bifrost: 503) | -| `record-whole` | fragment | `gamecult.fork.record.v0` with a counted answer and a revision, answerer ids and notes whole: what the door sends today, and a decider's view | -| `record-non-decider.proposed` | fragment | PROPOSED non-decider record: every `answerer` nil, `answerer_count` 2. Needs CultLib change `cultlib-fork-card-reader-counts`; not Huginn output yet | +| `record-whole` | fragment | `gamecult.fork.record.v0` with a counted answer, a revision and a refused answer, answerer ids and notes whole: what the door sends today (5 keys, no `answerer_count`) | +| `record-decider.mirror` | fragment | MIRROR (ruling `desk-reader-counts-shape`): a record with three answers by three accounts (one refused), as a decider sees it: every `answerer` id, `answerer_count` 3, header `86`. Not Huginn output yet | +| `record-non-decider.mirror` | fragment | MIRROR: the same record for a non-decider, with every `answerer` nil and `answerer_count` 3. Not Huginn output yet | | `card-viewers-decider.synthetic` | fragment | SYNTHETIC: only `{deciders: [two ids], decider_count: 2}`. Not a card | -| `card-viewers-non-decider.synthetic` | fragment | SYNTHETIC: only `{deciders: [], decider_count: 2}`. Not a card | +| `card-viewers-non-decider.synthetic` | fragment | SYNTHETIC: only `{deciders: [], decider_count: 2}` (withheld). Not a card | + +The mirror and synthetic fragments wait on Libby's CultLib vectors (sha pending). Once she posts it, Huginn's real types are decoded against those vectors instead. No case carries a card (`gamecult.fork.card.v0`). The card's bytes wait on CultLib's re-pinned card vectors for `raised_at`, so no entry-bearing list page or read is pinned yet. diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.hex new file mode 100644 index 0000000..c027b34 --- /dev/null +++ b/crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.hex @@ -0,0 +1 @@ +86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7403a7616e737765727393de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e33a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a752656675736564a26f66c0a87265747261637473c0a4636f6465ad6e6f745f615f64656369646572ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74d6520746f6f2eab616e7377657265645f6174b4323032362d31312d31315431373a30363a30305aab72656365697665645f6174b4323032362d31312d31315431373a30363a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92463336131663265342d376236642d346538662d396130622d316332643365346635613662a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303033 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.msgpack new file mode 100644 index 0000000..c2d48d4 Binary files /dev/null and b/crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.msgpack differ diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.hex similarity index 67% rename from crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.hex rename to crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.hex index 4053399..26c5008 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.hex +++ b/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.hex @@ -1 +1 @@ -86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7402a7616e737765727392de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032 +86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7403a7616e737765727393de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e33a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a752656675736564a26f66c0a87265747261637473c0a4636f6465ad6e6f745f615f64656369646572ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74d6520746f6f2eab616e7377657265645f6174b4323032362d31312d31315431373a30363a30305aab72656365697665645f6174b4323032362d31312d31315431373a30363a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303033 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.msgpack similarity index 63% rename from crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.msgpack rename to crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.msgpack index d184479..6c85aa4 100644 Binary files a/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.proposed.payload.msgpack and b/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.msgpack differ diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.hex index aa5f793..29eea7b 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.hex +++ b/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.hex @@ -1 +1 @@ -85ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31a7616e737765727392de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032 +85ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31a7616e737765727393de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e33a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a752656675736564a26f66c0a87265747261637473c0a4636f6465ad6e6f745f615f64656369646572ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74d6520746f6f2eab616e7377657265645f6174b4323032362d31312d31315431373a30363a30305aab72656365697665645f6174b4323032362d31312d31315431373a30363a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92463336131663265342d376236642d346538662d396130622d316332643365346635613662a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303033 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.msgpack index 1c8b4ed..356290b 100644 Binary files a/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.msgpack and b/crates/huginn-mind/tests/fixtures/desk-feed/record-whole.payload.msgpack differ diff --git a/docs/desk-feed-door.md b/docs/desk-feed-door.md index 87e6432..87399fa 100644 --- a/docs/desk-feed-door.md +++ b/docs/desk-feed-door.md @@ -118,20 +118,37 @@ byte; both values are in the account-id format above. Any other reader is a non- Bifrost, withholds account ids from a non-decider and returns a count instead. Bifrost cannot do it, because the card is opaque to it. A decider sees the ids. The superseding ruling is in force: `eureka-substrate:ruling:desk-read-visibility-withheld-ids`. -- **CultLib cannot carry this yet.** The card has 16 required keys in closed map-mode (17 once `raised_at` lands), and - `deciders` must hold at least one item. `answerer` is a required, non-null id. The proposal to - Libby (follow-up `cultlib-fork-card-reader-counts`): - - the card gains a required `decider_count` (an integer). A non-decider gets `deciders: []`; a - decider gets the ids. The count is the same for both. - - `answerer` becomes nullable: nil for a non-decider, with the key kept, as `note` is stripped. - - the record gains a required `answerer_count`, the number of distinct answerers. -- **Proposed fixtures**, built from mirror types that exist only in the test: - - `card-viewers-decider.synthetic` and `card-viewers-non-decider.synthetic` hold only the two card - keys the proposal changes. They are not cards. - - `record-non-decider.proposed` is a whole record as the proposal shapes it. +- **The shape** (ruling `eureka-substrate:ruling:desk-reader-counts-shape`, from Libby's CultLib + reader-counts cut). CultLib main does not have it yet: on main the card has 16 required keys and + `deciders` needs at least one item, and `answerer` is a required, non-null id. + - **Card:** `decider_count` is an integer from 1 to 64, placed right after `deciders`. An empty + `deciders` means the ids are withheld from this reader; a non-empty list holds exactly + `decider_count` ids. With `raised_at` the card has 18 keys (MessagePack header `DE 00 12`). + - **Record:** `answerer_count` is the number of distinct answerers over ALL the fork's stored + answers, refusals included. It is the same for both readers. The record has 6 keys (header `86`). + - **Answers:** `answerer` is nullable on all four variants (`Counted`, `Revision`, `Retraction`, + `Refused`). It is null only in a non-decider's view, with the key kept. Stored answers always + carry it. + - `options_hash` does not change (still five members). +- **Mirror fixtures.** These come from types that exist only in the test. They are neither CultLib's + bytes nor Huginn's output: + - `card-viewers-decider.synthetic` and `card-viewers-non-decider.synthetic` hold only `deciders` + and `decider_count`. They are not cards. + - `record-decider.mirror` and `record-non-decider.mirror` are a record with three answers by three + accounts (one of them refused) as each reader sees it. - The test `a_non_decider_gets_counts_and_no_ids_a_decider_gets_ids` checks that neither account id - appears anywhere in a non-decider's bytes. `record-whole` is the decider's view. + The tests check: + - a non-decider's bytes hold no account id anywhere, and a decider's hold every id; + - `answerer_count` is 3, counting the refusal, for both readers, and each record view starts with + `86`; + - a non-empty `deciders` must match `decider_count`, and the count stays within 1 to 64; + - `answerer` is null exactly in a non-decider's view. + + Where `answerer_count` sits among the record's six keys is CultLib's to fix. The mirror puts it + before `answers`. +- **Pending:** Libby's re-pinned vectors. When she posts the CultLib sha, Huginn's real types are + decoded against those vectors and these mirrors are retired (`cut-desk-feed-listing`). No byte + beyond Huginn's own envelope is pinned until then. Response (`huginn.desk_response.v1`): @@ -182,7 +199,7 @@ An empty list is never served in place of a failure. `open_count: 0` means zero ## Card fields (pending CultLib's re-pinned card vectors) `raised_at` is ruled by CultLib's steward (2026-11-11). It is a required card key whose value is an -RFC3339 string or nil, which takes the card from 16 to 17 keys in closed map-mode. Nil means unknown. +RFC3339 string or nil, which takes the card from 16 to 17 keys in closed map-mode (18 with `decider_count`). Nil means unknown. A string is the question's first-admission time, which never moves. It is outside `options_hash`, which keeps its five members, so the hash vectors do not change. Huginn derives it from the question's admission: the `committed_at` of the receipt that first admitted the question, or for an