From 30a05309c2fde0caff0e64ad7dda6be1b5c8b17f Mon Sep 17 00:00:00 2001 From: Eureka Date: Wed, 11 Nov 2026 17:53:32 +0000 Subject: [PATCH 1/4] Desk feed door: reader-count shapes as ruled (decider_count, nullable answerer, answerer_count) Ruling desk-reader-counts-shape (Libby's CultLib reader-counts cut): decider_count 1..64 right after deciders, deciders empty when withheld or exactly decider_count ids; answerer nullable on all four variants and null only in a non-decider's view; answerer_count over all stored answers, refusals included; card 18 keys with raised_at, record 6. Mirror fragments for both reader kinds and tests pinning those rules; no CultLib bytes pinned until Libby posts her sha. Co-Authored-By: Claude Sonnet 5.5 --- .../huginn-mind/tests/desk_feed_fixtures.rs | 119 ++++++++++++------ .../tests/fixtures/desk-feed/README.md | 9 +- .../record-decider.mirror.payload.hex | 1 + .../record-decider.mirror.payload.msgpack | Bin 0 -> 1626 bytes ... => record-non-decider.mirror.payload.hex} | 2 +- ...record-non-decider.mirror.payload.msgpack} | Bin 1084 -> 1515 bytes .../desk-feed/record-whole.payload.hex | 2 +- .../desk-feed/record-whole.payload.msgpack | Bin 1142 -> 1610 bytes docs/desk-feed-door.md | 45 ++++--- 9 files changed, 123 insertions(+), 55 deletions(-) create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.hex create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.msgpack rename crates/huginn-mind/tests/fixtures/desk-feed/{record-non-decider.proposed.payload.hex => record-non-decider.mirror.payload.hex} (67%) rename crates/huginn-mind/tests/fixtures/desk-feed/{record-non-decider.proposed.payload.msgpack => record-non-decider.mirror.payload.msgpack} (63%) 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 0000000000000000000000000000000000000000..c2d48d406e6ccbe4a95f12bd11bf2550c1448dab GIT binary patch literal 1626 zcmc(eO>Pr06vvUc07uB8TSxwyk7Na6-Az@oU^DXA&zVs&W3WA`xBwU6236Hb38g|p zVuvDbkqu|TP6A?4QYk@TS)Tci-&oJ@_x^8hovfe{FTlFmv|lEo0ol#`P#ZfPT9C%7 z;T-o-7PVSq=qyYHa$V`X6&}W~J8(YyI#%GOXjTW&PLSw)Wo+%=d@!%#t}80xs`b-M zjS?;MOeKtRic>-~aFR%(K+{Yx&a;$cWtrxbC7P$egeD9N5G*B<=bxnMS`Vrat-Aml zQo3x($E^r*v-`N;YWME@qbGm5U(9xQNVwn4@5dc}+1_+sng;sk+bl1QUM(zyLP_sd zSJEh0{pEsO%gWRemcmVA-J7~qSFQ2z^}K>X_y~3onYcj}-poZih$P)`xhq*Hr2kAY zO%Wkso|9~hahNZcfj!joSn2x%?&M6=O;NXn?tEy9Z;PK4)+dR^l9mKeF1SKmvJ#CT z(})OKGRai}slNHuBG;$TlwjXm*z{6tvlF}6mmW`daR&1`x+UNZ z-S?jW_s=F4K@M5&|%ah>EyV8sK@TxTLJlR)!mlJp>w?0Ay> z8R*V|y2q!t!9;~96v$eE{YR$lPhsGV*~>J2i0LL^A5+N$(G)n!$~3q$kVGSaOGG49 M3k3K&Zq;C&NTruktaHnJ^=|RQe|d!b7f?rZ)U)#a%FUKVPkZ2 zz@%euWM!>xZ**T_Uu0!tX=G(`kzr4hk^>nAO=Tc-Z*P;a104Z2lg|Sn0XCEH10W=0 sGhs1iGG#O^H)1wqEi`2~W-U2kFk&q+V=`njWi)0rVK!ouY63w4Gqdn98~^|S delta 12 UcmX@b^NnM|S;k2l&#hnv04Hb#?*IS* 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 -- 2.47.3 From 7ca49f13d22b291db777880c09884744bb09ac8a Mon Sep 17 00:00:00 2001 From: Eureka Date: Wed, 11 Nov 2026 18:00:56 +0000 Subject: [PATCH 2/4] Desk feed fixtures: four answer variants, one account answering twice Soul on 30a0530: deleting dedup() in view() survived because three answers came from three accounts, and Retraction was never in the mirror record. The record now holds Counted, Revision, Refused and Retraction by three accounts (four answers, answerer_count 3), and the test asserts both. Co-Authored-By: Claude Sonnet 5.5 --- .../huginn-mind/tests/desk_feed_fixtures.rs | 14 ++++++++++++-- .../tests/fixtures/desk-feed/README.md | 4 ++-- .../record-decider.mirror.payload.hex | 2 +- .../record-decider.mirror.payload.msgpack | Bin 1626 -> 2068 bytes .../record-non-decider.mirror.payload.hex | 2 +- .../record-non-decider.mirror.payload.msgpack | Bin 1515 -> 1920 bytes .../desk-feed/record-whole.payload.hex | 2 +- .../desk-feed/record-whole.payload.msgpack | Bin 1610 -> 2052 bytes docs/desk-feed-door.md | 6 +++--- 9 files changed, 20 insertions(+), 10 deletions(-) diff --git a/crates/huginn-mind/tests/desk_feed_fixtures.rs b/crates/huginn-mind/tests/desk_feed_fixtures.rs index 15e050f..45cdf49 100644 --- a/crates/huginn-mind/tests/desk_feed_fixtures.rs +++ b/crates/huginn-mind/tests/desk_feed_fixtures.rs @@ -105,6 +105,13 @@ fn record() -> ForkRecord { 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") }, + // A second answer by an account already counted: four answers, three answerers. + ForkDeskAnswer { + retracts: Some("n2".into()), + choice: None, + note: None, + ..answer("n4", "Retraction", None, OTHER_DECIDER, "", "2026-11-11T17:07:00Z") + }, ], } } @@ -387,11 +394,14 @@ fn a_non_decider_gets_counts_and_no_ids_a_decider_gets_ids() { 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, +/// `answerer_count` counts DISTINCT answerers over every stored answer, the refused one included +/// (four answers, one account answering twice, count 3), /// 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); + let outcomes: Vec = record().answers.iter().map(|answer| answer.outcome.clone()).collect(); + assert_eq!(outcomes, ["Counted", "Revision", "Refused", "Retraction"], "all four variants"); + assert_eq!(record().answers.len(), 4, "four answers by three accounts"); 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; diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/README.md b/crates/huginn-mind/tests/fixtures/desk-feed/README.md index de9b8cf..0c68d7b 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/README.md +++ b/crates/huginn-mind/tests/fixtures/desk-feed/README.md @@ -35,8 +35,8 @@ 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, 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-whole` | fragment | `gamecult.fork.record.v0` with all four answer variants (Counted, Revision, Refused, Retraction) by three accounts, 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 four answers (Counted, Revision, Refused, Retraction) by three accounts, 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}` (withheld). Not a card | 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 index c027b34..28d9000 100644 --- 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 @@ -1 +1 @@ -86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7403a7616e737765727393de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e33a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a752656675736564a26f66c0a87265747261637473c0a4636f6465ad6e6f745f615f64656369646572ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74d6520746f6f2eab616e7377657265645f6174b4323032362d31312d31315431373a30363a30305aab72656365697665645f6174b4323032362d31312d31315431373a30363a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92463336131663265342d376236642d346538662d396130622d316332643365346635613662a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303033 +86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7403a7616e737765727394de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e33a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a752656675736564a26f66c0a87265747261637473c0a4636f6465ad6e6f745f615f64656369646572ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74d6520746f6f2eab616e7377657265645f6174b4323032362d31312d31315431373a30363a30305aab72656365697665645f6174b4323032362d31312d31315431373a30363a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92463336131663265342d376236642d346538662d396130622d316332643365346635613662a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303033de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e34a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65aa52657472616374696f6ea26f66c0a87265747261637473a26e32a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365c0a46e6f7465c0ab616e7377657265645f6174b4323032362d31312d31315431373a30373a30305aab72656365697665645f6174b4323032362d31312d31315431373a30373a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303034 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 index c2d48d406e6ccbe4a95f12bd11bf2550c1448dab..6db3bbb76cd7fe8157068fbe807bad2903044f6b 100644 GIT binary patch delta 85 zcmcb`Geuy+UB)RJ@71tQyl2X2GIf)RLma8UU7M9(PRZCQAYF0_n8$K%_p(6GMWGYAxGxoB#j- diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.hex index 26c5008..dafd063 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.hex +++ b/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.hex @@ -1 +1 @@ -86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7403a7616e737765727393de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e33a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a752656675736564a26f66c0a87265747261637473c0a4636f6465ad6e6f745f615f64656369646572ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74d6520746f6f2eab616e7377657265645f6174b4323032362d31312d31315431373a30363a30305aab72656365697665645f6174b4323032362d31312d31315431373a30363a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303033 +86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7403a7616e737765727394de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e33a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a752656675736564a26f66c0a87265747261637473c0a4636f6465ad6e6f745f615f64656369646572ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74d6520746f6f2eab616e7377657265645f6174b4323032362d31312d31315431373a30363a30305aab72656365697665645f6174b4323032362d31312d31315431373a30363a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303033de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e34a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65aa52657472616374696f6ea26f66c0a87265747261637473a26e32a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365c0a46e6f7465c0ab616e7377657265645f6174b4323032362d31312d31315431373a30373a30305aab72656365697665645f6174b4323032362d31312d31315431373a30373a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303034 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.msgpack index 6c85aa4b573ee0aad87252203d810b388ccb90b7..d00019fd42dd7526ea4435980a3d14c671859ecf 100644 GIT binary patch delta 79 zcmaFO-M~NLF5{Gq_jp++-ZN!1nS7SXNqSXKYDrOIa!F=>-lF`p11pLk+~P%fMw4?G fW7rQY$;&TEJurDLvkas8-lF`p11pLk+~P%fMw4xs mWB3j%$;&TEJ+L}4uedz5XtDs4D5Lr0^UMlB@&j`#qX_^d=OR@A delta 12 TcmZn>IK?yJEaT*j=W191B4-6$ diff --git a/docs/desk-feed-door.md b/docs/desk-feed-door.md index 87399fa..fd538dd 100644 --- a/docs/desk-feed-door.md +++ b/docs/desk-feed-door.md @@ -134,12 +134,12 @@ byte; both values are in the account-id format above. Any other reader is a non- 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. + - `record-decider.mirror` and `record-non-decider.mirror` are a record with four answers by three accounts, one of + each variant (`Counted`, `Revision`, `Refused`, `Retraction`), with one account answering twice, as each reader sees it. 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 + - `answerer_count` is 3 over four answers, 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. -- 2.47.3 From 18867ceea81c213c153e0dc361ddaa84f65d8f00 Mon Sep 17 00:00:00 2001 From: Eureka Date: Wed, 11 Nov 2026 18:19:08 +0000 Subject: [PATCH 3/4] Desk feed fixtures: vendor CultLib's reader-count vectors at 3a51bbab, retire the mirror and synthetic fragments Ten vectors from fork-card-vectors.json at CultLib 3a51bbabad244a56ec4521080530d88744001ce4 (#117 on #115), unedited: three cards, three answers, four entries (decider and non-decider twins). Tests decode them with a decoder of their own and pin the header DE 00 12 (18 keys), record 86 with answerer_count fifth, decider_count after deciders, nullable answerer; Huginn's real types are pinned as not yet carrying the shapes. docs/desk-feed-door.md states the pinned shapes. Co-Authored-By: Claude Sonnet 5.5 --- .../huginn-mind/tests/desk_feed_fixtures.rs | 424 +++++++++++------- .../tests/fixtures/desk-feed/README.md | 13 +- ...card-viewers-decider.synthetic.payload.hex | 1 - ...-viewers-decider.synthetic.payload.msgpack | 1 - ...-viewers-non-decider.synthetic.payload.hex | 1 - ...wers-non-decider.synthetic.payload.msgpack | 1 - .../desk-feed/cultlib-3a51bbab/README.md | 24 + .../cultlib-3a51bbab/answer-counted.msgpack | Bin 0 -> 516 bytes .../answer-refused-stale.msgpack | Bin 0 -> 526 bytes .../answer-withheld-answerer.msgpack | Bin 0 -> 479 bytes .../card-operator-fork.msgpack | Bin 0 -> 577 bytes .../card-raised-at-nil.msgpack | Bin 0 -> 557 bytes .../card-withheld-deciders.msgpack | Bin 0 -> 539 bytes ...y-closed-with-ruling-and-a-refusal.msgpack | Bin 0 -> 1853 bytes .../entry-open-counted.msgpack | Bin 0 -> 1298 bytes ...d-closed-with-ruling-and-a-refusal.msgpack | Bin 0 -> 1741 bytes .../entry-withheld-open-counted.msgpack | Bin 0 -> 1223 bytes .../record-decider.mirror.payload.hex | 1 - .../record-decider.mirror.payload.msgpack | Bin 2068 -> 0 bytes .../record-non-decider.mirror.payload.hex | 1 - .../record-non-decider.mirror.payload.msgpack | Bin 1920 -> 0 bytes docs/desk-feed-door.md | 91 ++-- 22 files changed, 326 insertions(+), 232 deletions(-) delete mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.hex delete mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.msgpack delete mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.hex delete mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.msgpack create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/README.md create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/answer-counted.msgpack create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/answer-refused-stale.msgpack create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/answer-withheld-answerer.msgpack create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/card-operator-fork.msgpack create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/card-raised-at-nil.msgpack create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/card-withheld-deciders.msgpack create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/entry-closed-with-ruling-and-a-refusal.msgpack create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/entry-open-counted.msgpack create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/entry-withheld-closed-with-ruling-and-a-refusal.msgpack create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/entry-withheld-open-counted.msgpack delete mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.hex delete mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.msgpack delete mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.hex delete mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.msgpack diff --git a/crates/huginn-mind/tests/desk_feed_fixtures.rs b/crates/huginn-mind/tests/desk_feed_fixtures.rs index 45cdf49..f8e95fc 100644 --- a/crates/huginn-mind/tests/desk_feed_fixtures.rs +++ b/crates/huginn-mind/tests/desk_feed_fixtures.rs @@ -3,15 +3,16 @@ //! are what the door speaks. `docs/desk-feed-door.md` is the contract they pin; the README in the //! fixtures directory lists them. //! -//! The card (`gamecult.fork.card.v0`) is in no fixture: it waits on CultLib's re-pinned card -//! vectors for `raised_at`. Every response pinned here carries no entry. +//! No fixture written here carries a card: Huginn's types do not carry `raised_at`, `decider_count`, +//! `answerer_count` or a nullable `answerer` yet. Those shapes are CultLib's own bytes, vendored at +//! one commit in `fixtures/desk-feed/cultlib-3a51bbab/` and checked below. //! //! `cargo test -p huginn-mind --test desk_feed_fixtures` compares every file with what the //! encoders produce now. `HUGINN_WRITE_DESK_FIXTURES=1` rewrites them instead. use std::path::PathBuf; -use serde::Serialize; +use sha2::{Digest, Sha256}; use cultnet_rs::{CultNetMessage, CultNetWireContract, encode_cultnet_message_to_vec, encode_frame}; use huginn_mind::MindRefusal; @@ -116,120 +117,6 @@ fn record() -> ForkRecord { } } -// ---- 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 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() } } -} - -/// 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 ViewAnswer { - #[serde(rename = "schemaVersion")] - schema_version: AnswerSchema, - answer_id: String, - fork_id: String, - outcome: String, - of: Option, - retracts: Option, - code: Option, - options_hash: String, - choice: Option, - note: Option, - answered_at: String, - received_at: String, - claim_in_future: bool, - answerer: Option, - authority: DeskAuthority, - request_id: String, -} - -/// `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 ViewRecord { - #[serde(rename = "schemaVersion")] - schema_version: RecordSchema, - fork_id: String, - options_hash: String, - counted_answer_id: Option, - answerer_count: u64, - answers: Vec, -} - -/// 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; - ViewRecord { - schema_version: record.schema_version, - fork_id: record.fork_id, - options_hash: record.options_hash, - counted_answer_id: record.counted_answer_id, - answerer_count, - answers: record - .answers - .into_iter() - .map(|answer| ViewAnswer { - schema_version: answer.schema_version, - answer_id: answer.answer_id, - fork_id: answer.fork_id, - outcome: answer.outcome, - of: answer.of, - retracts: answer.retracts, - code: answer.code, - options_hash: answer.options_hash, - choice: answer.choice, - note: answer.note, - answered_at: answer.answered_at, - received_at: answer.received_at, - claim_in_future: answer.claim_in_future, - answerer: decider.then_some(answer.answerer), - authority: answer.authority, - request_id: answer.request_id, - }) - .collect(), - } -} - -/// 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-decider.mirror", rmp_serde::to_vec_named(&view(record(), true)).unwrap()), - ("record-non-decider.mirror", rmp_serde::to_vec_named(&view(record(), false)).unwrap()), - ] -} - /// The named MessagePack payload a CultNet operation message carries (base64 in the envelope). fn payload(message: &CultNetMessage) -> Vec { use base64::Engine; @@ -301,10 +188,6 @@ fn fixtures() -> Vec<(String, Vec)> { let whole = rmp_serde::to_vec_named(&record()).unwrap(); files.push(("record-whole.payload.msgpack".into(), whole.clone())); files.push(("record-whole.payload.hex".into(), hex(&whole).into_bytes())); - for (name, bytes) in reader_views() { - files.push((format!("{name}.payload.msgpack"), bytes.clone())); - files.push((format!("{name}.payload.hex"), hex(&bytes).into_bytes())); - } files } @@ -369,63 +252,262 @@ fn every_account_id_in_the_fixtures_is_a_lowercase_d_format_uuid() { } } + +// ---- CultLib's bytes: the reader-count shapes, pinned at one commit. ---- + +/// The CultLib commit the vendored vectors come from (#117 on #115). Re-pin: see the README beside them. +const CULTLIB_SHA: &str = "3a51bbabad244a56ec4521080530d88744001ce4"; + +/// Each vendored vector and the SHA-256 of its bytes. +const VENDORED: &[(&str, &str)] = &[ + ("card-operator-fork", "56bfdb65ded60b562001d09d0b8da6e092900f404bf5f68f7084dff29bcc34f5"), + ("card-raised-at-nil", "b81bb825790ffe03fabe63c3f8a61c36bd50ca268aa6dff67b5bcfad3957e58c"), + ("card-withheld-deciders", "d13b9b77461cfdf78f943ef56c6aeab852ab6b6f0b355e26c5b361f99c79c7f7"), + ("answer-counted", "55d69ae3cc7ebbeeae7d501e8641640e115c3fb5aac399ea760588bca343b9e6"), + ("answer-withheld-answerer", "b66b4f921cdd374be04af3de34c59e254a5e542027ae8c57f196a3ea3406e427"), + ("answer-refused-stale", "3183bc9c483f4a625332cda11ef22a9c8560868a7728b77daa63a1960a3cd3d6"), + ("entry-open-counted", "b51f499e8dd0465cf5df9d68d8fccbe94dd1339fd3157969693dd0d4ecb2549b"), + ("entry-withheld-open-counted", "62ccad7a9f41262d6e35d0247315d0bb75d1313b3c9dabe8a99d1aa1b844607f"), + ("entry-closed-with-ruling-and-a-refusal", "f7859c7d04d338d6b1c634308b08ae58ae77b0ff459767859a5e40f0af2f16e5"), + ("entry-withheld-closed-with-ruling-and-a-refusal", "daaa819bfa2612616826ef3217dfcf8d8d8cb71d86547d7106426c779134f827"), +]; + +fn vendored(name: &str) -> Vec { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/desk-feed/cultlib-3a51bbab").join(format!("{name}.msgpack")); + std::fs::read(&path).unwrap_or_else(|error| panic!("{}: {error}", path.display())) +} + +/// A decoded MessagePack value that keeps map order. A small decoder of its own, so the key counts, +/// headers and positions below are read off the bytes and not off `rmp_serde`'s idea of the types. +#[derive(Debug, PartialEq)] +enum Value { + Nil, + Bool(bool), + Int(i64), + Str(String), + List(Vec), + Map(Vec<(String, Value)>), +} + +impl Value { + fn get(&self, key: &str) -> &Value { + let Value::Map(pairs) = self else { panic!("not a map") }; + &pairs.iter().find(|(name, _)| name == key).unwrap_or_else(|| panic!("no key {key}")).1 + } + fn keys(&self) -> Vec<&str> { + let Value::Map(pairs) = self else { panic!("not a map") }; + pairs.iter().map(|(name, _)| name.as_str()).collect() + } + fn list(&self) -> &[Value] { + let Value::List(items) = self else { panic!("not a list") }; + items + } + fn str(&self) -> &str { + let Value::Str(text) = self else { panic!("not a string: {self:?}") }; + text + } + fn int(&self) -> i64 { + let Value::Int(number) = self else { panic!("not an int: {self:?}") }; + *number + } +} + +fn decode(bytes: &[u8]) -> Value { + fn take<'a>(bytes: &'a [u8], at: &mut usize, n: usize) -> &'a [u8] { + let slice = &bytes[*at..*at + n]; + *at += n; + slice + } + fn be(slice: &[u8]) -> u64 { + slice.iter().fold(0, |acc, byte| (acc << 8) | u64::from(*byte)) + } + fn text(bytes: &[u8], at: &mut usize, n: usize) -> Value { + Value::Str(String::from_utf8(take(bytes, at, n).to_vec()).unwrap()) + } + fn items(bytes: &[u8], at: &mut usize, n: usize) -> Vec { + (0..n).map(|_| value(bytes, at)).collect() + } + fn pairs(bytes: &[u8], at: &mut usize, n: usize) -> Value { + Value::Map((0..n).map(|_| (value(bytes, at).str().to_string(), value(bytes, at))).collect()) + } + fn value(bytes: &[u8], at: &mut usize) -> Value { + let tag = take(bytes, at, 1)[0]; + match tag { + 0x00..=0x7f => Value::Int(i64::from(tag)), + 0x80..=0x8f => pairs(bytes, at, usize::from(tag & 0x0f)), + 0x90..=0x9f => Value::List(items(bytes, at, usize::from(tag & 0x0f))), + 0xa0..=0xbf => text(bytes, at, usize::from(tag & 0x1f)), + 0xc0 => Value::Nil, + 0xc2 => Value::Bool(false), + 0xc3 => Value::Bool(true), + 0xcc => Value::Int(be(take(bytes, at, 1)) as i64), + 0xcd => Value::Int(be(take(bytes, at, 2)) as i64), + 0xce => Value::Int(be(take(bytes, at, 4)) as i64), + 0xd9 => { + let n = be(take(bytes, at, 1)) as usize; + text(bytes, at, n) + } + 0xda => { + let n = be(take(bytes, at, 2)) as usize; + text(bytes, at, n) + } + 0xdc => { + let n = be(take(bytes, at, 2)) as usize; + Value::List(items(bytes, at, n)) + } + 0xde => { + let n = be(take(bytes, at, 2)) as usize; + pairs(bytes, at, n) + } + other => panic!("MessagePack tag {other:#04x} at {} is not in this decoder", *at - 1), + } + } + let mut at = 0; + let decoded = value(bytes, &mut at); + assert_eq!(at, bytes.len(), "trailing bytes"); + decoded +} + +const CARD_KEYS: [&str; 18] = [ + "schemaVersion", "fork_id", "title", "context", "options", "recommendation", "depends", "leans", "flag", "raised_in", "raised_at", "deciders", + "decider_count", "status", "ruling", "ruled_from", "order", "options_hash", +]; +const RECORD_KEYS: [&str; 6] = ["schemaVersion", "fork_id", "options_hash", "counted_answer_id", "answerer_count", "answers"]; + +#[test] +fn the_vendored_cultlib_vectors_are_the_pinned_bytes() { + assert_eq!(CULTLIB_SHA.len(), 40); + for (name, sha) in VENDORED { + let digest: String = Sha256::digest(vendored(name)).iter().map(|byte| format!("{byte:02x}")).collect(); + assert_eq!(&digest, sha, "{name} is not CultLib {CULTLIB_SHA}'s bytes"); + } +} + +/// The card: header `DE 00 12` (map16, 18 entries: `schemaVersion` and 17 fields), the keys in this +/// order, `raised_at` a string or nil, `decider_count` 1..64 right after `deciders`, and `deciders` empty +/// (withheld) or exactly `decider_count` account ids. +fn check_card(card: &Value, withheld: bool) { + assert_eq!(card.keys(), CARD_KEYS); + assert!(matches!(card.get("raised_at"), Value::Nil | Value::Str(_))); + let count = card.get("decider_count").int(); + assert!((1..=64).contains(&count)); + let ids = card.get("deciders").list(); + assert_eq!(ids.is_empty(), withheld); + assert!(ids.is_empty() || ids.len() as i64 == count); + assert!(ids.iter().all(|id| account_id(id.str())), "deciders are lowercase D-format UUIDs"); + assert_eq!(card.get("options_hash").str().len(), 64); +} + +#[test] +fn the_cards_are_eighteen_keys_with_raised_at_and_decider_count() { + for name in ["card-operator-fork", "card-raised-at-nil", "card-withheld-deciders"] { + let bytes = vendored(name); + assert_eq!(bytes[..3], [0xde, 0x00, 0x12], "{name}: header"); + check_card(&decode(&bytes), name == "card-withheld-deciders"); + } + assert!(matches!(decode(&vendored("card-operator-fork")).get("raised_at"), Value::Str(_))); + assert_eq!(decode(&vendored("card-raised-at-nil")).get("raised_at"), &Value::Nil); + assert_eq!(decode(&vendored("card-withheld-deciders")).get("decider_count").int(), 2); +} + +/// `answerer` is a string or nil on every answer; `Value::Nil` only when `named` is false. +fn check_answer(answer: &Value, named: bool) { + assert_eq!(answer.keys().len(), 16); + assert_eq!(answer.keys()[13], "answerer"); + match answer.get("answerer") { + Value::Str(id) => assert!(named && account_id(id), "a named answerer is a lowercase D-format UUID"), + Value::Nil => assert!(!named), + other => panic!("answerer is {other:?}"), + } +} + +#[test] +fn the_answers_keep_sixteen_keys_and_answerer_is_a_string_or_nil() { + for (name, named) in [("answer-counted", true), ("answer-refused-stale", true), ("answer-withheld-answerer", false)] { + let bytes = vendored(name); + assert_eq!(bytes[..3], [0xde, 0x00, 0x10], "{name}: header"); + check_answer(&decode(&bytes), named); + } + assert_eq!(decode(&vendored("answer-refused-stale")).get("outcome").str(), "Refused"); +} + +/// The entry is `83` and its record `86`, `answerer_count` the fifth key. In a decider's view the count +/// is the number of distinct answerer ids over every stored answer, refusals included; in a +/// non-decider's view no answerer is named and the count is between 1 and the number of answers (0 for +/// none). A non-decider entry holds none of its twin's account ids anywhere in its bytes. +#[test] +fn the_entries_are_83_over_a_record_of_86_with_answerer_count_before_answers() { + for (decider, non_decider) in [("entry-open-counted", "entry-withheld-open-counted"), ("entry-closed-with-ruling-and-a-refusal", "entry-withheld-closed-with-ruling-and-a-refusal")] { + let (decider_bytes, non_decider_bytes) = (vendored(decider), vendored(non_decider)); + let mut ids = Vec::new(); + for (bytes, named) in [(&decider_bytes, true), (&non_decider_bytes, false)] { + assert_eq!(bytes[0], 0x83, "entry header"); + let entry = decode(bytes); + assert_eq!(entry.keys(), ["schemaVersion", "card", "record"]); + check_card(entry.get("card"), !named); + let record = entry.get("record"); + assert_eq!(record.keys(), RECORD_KEYS); + let answers = record.list_of_answers(); + let count = record.get("answerer_count").int(); + answers.iter().for_each(|answer| check_answer(answer, named)); + if named { + let mut distinct: Vec<&str> = answers.iter().map(|answer| answer.get("answerer").str()).collect(); + distinct.sort_unstable(); + distinct.dedup(); + assert_eq!(count, distinct.len() as i64, "{decider}: distinct answerers"); + ids.extend(distinct.iter().map(|id| id.to_string())); + ids.extend(entry.get("card").get("deciders").list().iter().map(|id| id.str().to_string())); + } else if answers.is_empty() { + assert_eq!(count, 0); + } else { + assert!((1..=answers.len() as i64).contains(&count)); + } + } + assert!(!ids.is_empty()); + assert!(ids.iter().all(|id| !holds(&non_decider_bytes, id)), "{non_decider} carries an account id"); + assert!(ids.iter().all(|id| holds(&decider_bytes, id)), "{decider} names every id"); + // The twins agree on everything the view does not move. + let (a, b) = (decode(&decider_bytes), decode(&non_decider_bytes)); + assert_eq!(a.get("card").get("options_hash"), b.get("card").get("options_hash")); + assert_eq!(a.get("card").get("decider_count"), b.get("card").get("decider_count")); + assert_eq!(a.get("record").get("answerer_count"), b.get("record").get("answerer_count")); + } + // A refusal is an answer and its answerer counts: the closed fork's two answers are two accounts. + let closed = decode(&vendored("entry-closed-with-ruling-and-a-refusal")); + assert!(closed.get("record").list_of_answers().iter().any(|answer| answer.get("outcome").str() == "Refused")); + assert_eq!(closed.get("record").get("answerer_count").int(), 2); +} + fn holds(bytes: &[u8], text: &str) -> bool { bytes.windows(text.len()).any(|window| window == text.as_bytes()) } -/// 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 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.mirror"] { - let held = bytes(name); - assert!([READER, OTHER_DECIDER, OUTSIDER].iter().all(|id| !holds(&held, id)), "{name} carries an account id"); - } - 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 -/// (four answers, one account answering twice, count 3), -/// 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() { - let outcomes: Vec = record().answers.iter().map(|answer| answer.outcome.clone()).collect(); - assert_eq!(outcomes, ["Counted", "Revision", "Refused", "Retraction"], "all four variants"); - assert_eq!(record().answers.len(), 4, "four answers by three accounts"); - 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}"); +impl Value { + fn list_of_answers(&self) -> &[Value] { + self.get("answers").list() } } -/// `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. +/// Huginn's real types against CultLib's bytes. A decider's answers already decode (the answer keeps +/// its 16 keys). The card and the record do not: `ForkCard` has no `raised_at` or `decider_count`, +/// `ForkRecord` no `answerer_count`, and `ForkDeskAnswer.answerer` is a non-null string, and each type +/// refuses unknown keys. This pins the gap `cut-desk-feed-listing` closes; when it does, these +/// refusals become successful decodes of the same bytes and this test changes with them. #[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); +fn huginn_types_do_not_yet_carry_the_pinned_shapes() { + use huginn_mind::desk_wire::ForkCard; + for name in ["answer-counted", "answer-refused-stale"] { + rmp_serde::from_slice::(&vendored(name)).unwrap_or_else(|error| panic!("{name}: {error}")); } - 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)); + fn refused(bytes: &[u8]) -> String { + rmp_serde::from_slice::(bytes).err().expect("the real type refuses these bytes").to_string() + } + assert!(refused::(&vendored("card-operator-fork")).contains("raised_at")); + assert!(refused::(&vendored("card-withheld-deciders")).contains("raised_at")); + assert!(refused::(&vendored("answer-withheld-answerer")).contains("string")); + // The record is the entry's last key: its bytes run from the `86` after the key to the end. + let entry = vendored("entry-open-counted"); + let key = [&[0xa6u8][..], b"record", &[0x86]].concat(); + let at = entry.windows(key.len()).position(|window| window == key).expect("the record key") + key.len() - 1; + assert!(refused::(&entry[at..]).contains("answerer_count")); } diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/README.md b/crates/huginn-mind/tests/fixtures/desk-feed/README.md index 0c68d7b..4b98b4f 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/README.md +++ b/crates/huginn-mind/tests/fixtures/desk-feed/README.md @@ -36,12 +36,9 @@ lands (see `docs/desk-feed-door.md`). | `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 all four answer variants (Counted, Revision, Refused, Retraction) by three accounts, 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 four answers (Counted, Revision, Refused, Retraction) by three accounts, 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}` (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. +The reader-count shapes (`raised_at`, `decider_count`, `answerer_count`, nullable `answerer`) are +CultLib's own bytes, vendored unedited in `cultlib-3a51bbab/` (see its README for the pinned commit and +each vector). They are not Huginn's output: Huginn's types do not carry those shapes yet. +`cut-desk-feed-listing` and `cut-desk-feed-raised-at` replace them with entry-bearing fixtures the +encoders write, after decoding these bytes into the real types. diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.hex deleted file mode 100644 index 1624ae5..0000000 --- a/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.hex +++ /dev/null @@ -1 +0,0 @@ -82a8646563696465727392d92435663063326231652d386134642d346333622d396537662d316132623363346435653666d92430623964376533612d366332312d346635382d613064342d396538663761366235633464ad646563696465725f636f756e7402 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.msgpack deleted file mode 100644 index 71b15b3..0000000 --- a/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-decider.synthetic.payload.msgpack +++ /dev/null @@ -1 +0,0 @@ -‚¨deciders’Ù$5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6fÙ$0b9d7e3a-6c21-4f58-a0d4-9e8f7a6b5c4d­decider_count \ No newline at end of file diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.hex deleted file mode 100644 index 29a1257..0000000 --- a/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.hex +++ /dev/null @@ -1 +0,0 @@ -82a8646563696465727390ad646563696465725f636f756e7402 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.msgpack deleted file mode 100644 index 252f158..0000000 --- a/crates/huginn-mind/tests/fixtures/desk-feed/card-viewers-non-decider.synthetic.payload.msgpack +++ /dev/null @@ -1 +0,0 @@ -‚¨deciders�­decider_count \ No newline at end of file diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/README.md b/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/README.md new file mode 100644 index 0000000..ed1953c --- /dev/null +++ b/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/README.md @@ -0,0 +1,24 @@ +# CultLib desk-feed vectors, vendored + +Source: `GameCult/CultLib` at commit `3a51bbabad244a56ec4521080530d88744001ce4` (#117, on +`af914253` #115), file `contracts/cultnet/governance/fork-card-vectors.json`. Each `.msgpack` file is +the vector's `messagePackHex` decoded to bytes, nothing else changed. Do not edit them or rewrite them +from Huginn's encoders: Huginn's own types do not carry these shapes yet (see +`docs/desk-feed-door.md`). To re-pin, take the new commit's vectors and update the sha here, in +`docs/desk-feed-door.md` and in `CULTLIB_SHA` in `desk_feed_fixtures.rs`. + +| file | upstream vector | what it pins | +|---|---|---| +| `card-operator-fork` | `card.valid` `operator_fork` | 18 keys (`DE 00 12`), `raised_at` a string, a decider's `deciders` and `decider_count` 1 | +| `card-raised-at-nil` | `card.valid` `raised_at_nil` | `raised_at` nil, key kept | +| `card-withheld-deciders` | `card.valid` `withheld_deciders` | empty `deciders`, `decider_count` 2 | +| `answer-counted` | `deskAnswer.valid` `counted` | 16 keys (`DE 00 10`), `answerer` a string | +| `answer-withheld-answerer` | `deskAnswer.valid` `withheld_answerer` | `answerer` nil | +| `answer-refused-stale` | `deskAnswer.valid` `refused_stale` | a `Refused` answer, `answerer` a string | +| `entry-open-counted` | `transport.entry.valid` `open_counted` | decider view: entry `83`, record `86`, `answerer_count` 1 | +| `entry-withheld-open-counted` | `transport.entry.valid` `withheld_open_counted` | non-decider twin: `deciders` empty, `answerer` nil, count 1 | +| `entry-closed-with-ruling-and-a-refusal` | `transport.entry.valid` `closed_with_ruling_and_a_refusal` | decider view, a refusal among the answers, `answerer_count` 2 | +| `entry-withheld-closed-with-ruling-and-a-refusal` | `transport.entry.valid` `withheld_closed_with_ruling_and_a_refusal` | its non-decider twin, count 2 | + +The test `desk_feed_fixtures.rs` checks each file's SHA-256, decodes it with a decoder of its own +(not `rmp_serde`), and then tries Huginn's real types. diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/answer-counted.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/answer-counted.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..332a9791fd2eab47b37ae366e02d0912dbd949fb GIT binary patch literal 516 zcmZ{hy^a$x5XX509>K*)P~gb+dOxxi5+wx^LKN4mCga&#E4y~Ey@z<6=tOcHhz5x^ z;w{qfEUd{)p>Am+Bo{eCkR`P-bHfh;OB&XqQfgcyF_O1`_nN{3V$;NlW>Yn)D0^K|0lh2) z5EPX}-F(KuTco}7cg`m|zkN-#V>YqTBaAHrmR+*!lG(`8Kn&(I`0y;U1sGp2aE8SJ zf^|1pw?K=Ab`T&fET+}N-Y>PMvjK zKc_j+ujAv&aK0j8B&topL9a`$tET2nQ?Gdi1jS5XX509>GN?%1yv`}&E(wSRDQ(1C zT*I@l&Oyl(^CA7_ufJw~zj*mQ;7BukBp-~kkJm60E>qkn3fmrR_)LCtBks;!Z`9t( zgLZzqwx`$sXpSSq?$bgct|DEST;wzOEk|bq*?4eEL^spnlJpWt?VWx)dWs$}hNoL} zita;%iTWh{&Fzv*=tl_S>8&h=M(aThm6W1V1ByyltMHW3SWR3avY|0}pX!F>W5t=ONWY{F#ZY;!<*%eUNP@vQCJr2Tw@m=6^tW^#dm` zb8l>~m$-QPb^I&oPZuPV#B9hp6m7|is;PO?)Ge=|916@;MpECMEt~k|_Owq6p7T_Bky4XV8qbmEe*iwRyfgp+ literal 0 HcmV?d00001 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/answer-withheld-answerer.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/answer-withheld-answerer.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..507479347851701ae2837f81aef0c13410e596f7 GIT binary patch literal 479 zcmZ{gzfQw25XN}~9>D_=45W(lx6Opa$bf_p150$+m&R&h7yDAg>r{xg6vTqWTJaWL zcot$>Ms7HtrQdh=>H9u7d~>447H-fwW%TD5S`=MF1Bq@@f_4vR2eWXsn@T0;nlCD2 zC;hlR`J)3-0~PljI_j4ct}zs}U?+hv+M%X}=}4Fs7w4wagmP}G&C()S5ac#b!bse_ znyDANvIbXg&vH3RD>2Mv8b?VKW}H`;@KkV#Rg^=VBzea3ySr={#c7pf7(>P55D?;w zi)8pLYNG_swRzHpaC>!uIAw}hV+1q>1Lj>a?~*CUs73}I)y5t>)&tEc11*`W!74rW zx*qB^I0XyTvw{ZeT_`9^ppPhwvVik|N7sB*gi#TOw`+?6)$BhYy?qxAsJ2wPtUBr} qel7Pa?Pj%eqv&yYImP~_u`d>XBlgiBg}dT`e}lxgp>O`XFa86FG_BJB literal 0 HcmV?d00001 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/card-operator-fork.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/card-operator-fork.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..325e6688c37b2c16b049964287414c4cadbe8c1f GIT binary patch literal 577 zcmZ`$OODhq5Cw4qPLKr)5ONgf-`Rk~l4TDz|p=ZgeT<^VldR6{&@8Pv$9ro}Vt&>Lo+Cq)PP$V}g(=CD`< z)x>cKtvjEe{3pGT3du*Kk-*3$YK8}epxtpV6l{-I7NkSIk@{@ElbTO2M!z1Ag%I=8 zF38jQ4uaZ@hB~6mN`O1+=9=_px zeuN39ENwFa`Klm!Sy!a4s}(7sXmghF6sfpA={L-b+TXu)9{lL`CM_KTi)vc literal 0 HcmV?d00001 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/card-raised-at-nil.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/card-raised-at-nil.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..a5e848efced5c95af8f3bdae948382c6109c69a6 GIT binary patch literal 557 zcmZ`#OODhq6a;YtPLKr)M#xboaX#G}kXW*8iEZ>^KX|i?xSk4I;jxcmj&md+b z7A!~VYh=S&u!o){tLIl$uT-u-PoCd8*5d&0(K>1L?+pydCgoFM>^5a!dHQj2h)XN^ z{PK||tOvjPFro9|YUPyiWB|J*%xH(&@4fU2f2{+aL_9g+hcG>U^7rfBOo}Iv#M6J0 z4inddMgIC=#%SxlonK1+{WH2%C%+z%g%ITMP5TeX1*x|Id!DEbDQAk7%sW>UISKGA~`)X|qll7@mH~rZ}{M z?_WOBg!SN>&mOG{s>Vv~$N*+dm{t~*+dAPS{#^t-iFk6rr%)Y!b^jK<_L3)%MCrrI z5)<2lLH=gaW3;tDPtFAY^$qPNYAzR%O@vD$e5Uk0di@@83}7NP1DReMWO^7-aflXS zYvMSD(f-)KdLn%%B$Dr1Apy??Du!E$pzLnbNm%VJ4G4?873yTO7K%^ryjzUOLWuqA zcq=KEWCf#bn0KuWOl-*gO`Mi!AULL`eDnc#lV^EFC?z!ekR3Ng)@0eI3yw_i@C~O| zhpuJXEBE}|I&j`@ba=+O@lvSO?supmQfKsVy45DgUi>*6t?j|~`?obeo^@ literal 0 HcmV?d00001 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/entry-closed-with-ruling-and-a-refusal.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/entry-closed-with-ruling-and-a-refusal.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..93e6b2def21e3714ec772affdc0d818f9620fb5b GIT binary patch literal 1853 zcmd6o&2AGh5Xbp=0mKvR0f7*Bwd>t%Ho1UQi3SmqV-a=9@qRr#sM`oOEg z`kB(}K9p8(`J2IrGcArTTwNA_ToyYGYHPQ%BgL2FRl;*-+Zz=ai@(s`h<2^C1&r^Z z&WmcUN()cz*VSBAh47eX3T3@M!^JLmCTAMNomvgtX0!8rD8;KSFeA5Tf7Pve()w3t*+0 zjIx6DCi|L61EM3#=c9F5ih8B8s{`Pe#PpV{ULm`r#|Ea&E88)+;=T}fFk)2!nqfp= z$H#G){XhenkT4`+uokr1QP2*8$9n>}6bNQ}dJO^*a@vgui(3s6Cux(UX|qKV*67AO z5){Jh?Lyz-sw(Zpv9ZinX3PtP$&PhZ$g(%RzLcK^ql#+IWdX?Fi2O2qjdDC;rd5A? zScbbwV4kB$)=q5?TDTPQhfkP6|W{?JP1ki{ZStIGTL=16EL($9{ z-Hfp`;c=L9pks$dijaTh3V#MQCgoFwHD#x%Cegb;RnP z=zh8uw%P%8-G6}KL2kP8pC)KLe#Z+Y`)I;WR@q9!x4mODXS2)xJY{LO_8tt-lUC{1 z8fNztqMlw0W)BUC`y{b@rS9d1^M{6i1)cr@^oNjD=-yMHAI$FAxw-!he|Mq&m-yp$ w@N4|7EQO{>IKj)2kvM8ml7>x0mxVw@#N%#*{lH(ZT6ltgUZ>Hgzo1XQ12i?R1ONa4 literal 0 HcmV?d00001 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/entry-open-counted.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/entry-open-counted.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..eb8604549b892b27b168bdc355ed646b9243dcd9 GIT binary patch literal 1298 zcmb_cO>fgc5G@Dph+p7CC4|7y+KCe<7mzA(;ebRd4!uO<-Eq9C<4t$hg>ozsXUb0q zX+Bz7gv5t9AVvI(T>4v>&4<)TRP<0EY|rl7@ywg|{O;WF47>0OwUMg$(Sa`VGPjjf z>AeaTRv%Ul*noo;mmBxj!Qa=x4nx?HJK2`v*J{`28Q9i-iN?|wTKmz7l{UxWGs1b% zh^xZl8~b}ZQ)MnZ@Jz0(*9F>4&@&lm6i-H~=MI~XM|~+i9ikbyGm~w%>q%R2%l$&l zS1+C;_Gr<|6^FcXp^>X@7YhL{1))X)dlZ`E*~4pUOl6M#TUDSBrI1)~JjoFXGaaNk zbfyyx(x7O|;%KlZ3o+U%?RF126)}6{*vn<&cc5olytHjYJMP7IjrmsPsDUz8bZwOf z?6;7Gbw3FFfbFnmtIAr8?T!UH0uZj?Z z^n8hLb5$1h&d^w}r5Pw{cBso-7M!!n{41eYL2}gt;-)hL}tbX)3Xdp~_M&I@5a# zJ1fvK%}T~+)O%YMPXtE@$J%2hyZe0+97=^-+59EsY?!Zey36h=F;{At zYcB6&!6UMOn(lzYZbc?e%K*b-_GVtj`rvHo_GRZ_pf_hp@Eqgw>3;8nTxJGa6Y^E7 zDzLtcCnE($5o)m2sAMp?=7SGuA~}V^K|&gst}U3Qv0y*09g~1VGLuMkUpwfg^pQgZ zD_n^!XHAA6v@G<9A_lS4c*ipyk==8B9j*r}4L`JfeT>2b-?sE$1gLI&pef;9v{x(6 zOs3$+XfDlUTvo%)?&x_Bjbx-cQczqjsN(dlUp!&p+Fv)aueyENuzbScuk;N6K+iRZ qGc@J}dL9>-W9i#}Bdxs!|4U_~hJQt6^QM6v{4=>o*zgy^hCcy`W{`RS literal 0 HcmV?d00001 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/entry-withheld-open-counted.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/entry-withheld-open-counted.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..2d91b27b832b7e8f16793ad3158f1794e4f8476e GIT binary patch literal 1223 zcmb_c&2AGh5H1Jqh$q+s5<;+%cblY1E+AFn!U2g^9D0c+_H5SbuASQ6LOB+x=axqZ zX*OwT5fVS*fE4i-x%62W=SONJDmc^!+hc#8o%!aQeScwjiaESSW2IK#yO1LMGpGw*t#<01GX{xR-)LYnVZ70)MIymkhizb^-oyxEfM%`H z5Eu!pH8v9H3_X!X9IcAr$CARiV&<6tlEE#9_hrTnQ9yNigN78?SJCC%Aal-KIk)Y} z)!MzA@(^fu7fvXT@pX6q)^@22M{>h*(^m>bUni51L1%zFk`LDcC%2sVkf&O5^zv8Q z;q=vt{#z>@C2inR44`nSHV``wxZO#nN{ufll W=>s+!F$?_~R{hlQlQ-uhW`6*#%hbUD literal 0 HcmV?d00001 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 deleted file mode 100644 index 28d9000..0000000 --- a/crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.hex +++ /dev/null @@ -1 +0,0 @@ -86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7403a7616e737765727394de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e33a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a752656675736564a26f66c0a87265747261637473c0a4636f6465ad6e6f745f615f64656369646572ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74d6520746f6f2eab616e7377657265645f6174b4323032362d31312d31315431373a30363a30305aab72656365697665645f6174b4323032362d31312d31315431373a30363a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92463336131663265342d376236642d346538662d396130622d316332643365346635613662a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303033de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e34a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65aa52657472616374696f6ea26f66c0a87265747261637473a26e32a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365c0a46e6f7465c0ab616e7377657265645f6174b4323032362d31312d31315431373a30373a30305aab72656365697665645f6174b4323032362d31312d31315431373a30373a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303034 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 deleted file mode 100644 index 6db3bbb76cd7fe8157068fbe807bad2903044f6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2068 zcmc(f&5qMB5XX^t0Un`;y}F7W=c7G=IPaxeao`d;_PB}K#9%vJ@d7*nPf)Ek-7U)s z35gr3c#B?m7VNZq9G0xq!eUuYCZ1U1`OSac-WXm$#a@6mrK&$qSp~eA+o4o?Hq^kC z7Q;o*dNV7!8P4o>AGbT(y!-a($)E1$qun3k?sxV3afe^FRgLAUg7*0?%B7NCooRCXisFK5)nVHXfRo3F7Ru;aT6)AM|n`J9ziR@RwptSgGIs~;4$Cy@*|$uXdmQGqBA zb2NfPBFso0aw;N-<<-v;xjcm`2Ys!ft0iA%C&7Lo|3uU;sb9RgANU};5DqB5@)_F((*N8KsB?f;yb3Ojw*pj^@!M$w4H; z<4Lv?kc|OxhfZ&th%%N5;H3clK%#9=VPKW&Wg6edv~%cV%0q@F0g945cItE_(TD{( z!kmZ@9An4zXr|#oOzH7VuP3{HyVK{d_PeafzihypuIq04`l@fOy?wQF=p*+aYNX!* DB)03} diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.hex b/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.hex deleted file mode 100644 index dafd063..0000000 --- a/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.hex +++ /dev/null @@ -1 +0,0 @@ -86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7403a7616e737765727394de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e33a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a752656675736564a26f66c0a87265747261637473c0a4636f6465ad6e6f745f615f64656369646572ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74d6520746f6f2eab616e7377657265645f6174b4323032362d31312d31315431373a30363a30305aab72656365697665645f6174b4323032362d31312d31315431373a30363a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303033de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e34a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65aa52657472616374696f6ea26f66c0a87265747261637473a26e32a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365c0a46e6f7465c0ab616e7377657265645f6174b4323032362d31312d31315431373a30373a30305aab72656365697665645f6174b4323032362d31312d31315431373a30373a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303034 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/record-non-decider.mirror.payload.msgpack deleted file mode 100644 index d00019fd42dd7526ea4435980a3d14c671859ecf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1920 zcmc(f&2AGh5XX^t0UluwTsK;Kzd}x+o_mQZ4qR4_Jvv625>wv3 z%-M^S(_zli7$-Q&5aN)alp+Qk=Oj*&JVW_pk_~a3@+53_nvIAuT2P)Emz1cAqxAZ9e>X^6XFd^U-b&are9Y{kYvP+o;-7RY7yqN4ZqIU1@NM zqSmzMR557(azZX>sRV^JF*Bd{N=SAtm4$C7B?Rsx=)f~ELFwMqnG8HhGgxm@<`QYY z;0R|SLT;X*d=y1)zF9l=AQnE;4?En@IT2MMWWj6eisJj~7lronj{e0lR1?rw8oDy^ z74$sXjL<*B#4kg?cylw*PT=tV;8tT;2;T^foGYyV#IAWY)c|ZOr1UXJox>p^;hiAG z`$KBRz-t5Sf$q9?rC*SO0Tm4NKWeq+7y_%*R$SR*xH^YJ;)-{|mF$n}YTOH8-&OZ| axRm&iir98C*lrU#uXS66u5&mfl;kIrTf*D` diff --git a/docs/desk-feed-door.md b/docs/desk-feed-door.md index fd538dd..d7f1ff5 100644 --- a/docs/desk-feed-door.md +++ b/docs/desk-feed-door.md @@ -4,7 +4,8 @@ Status, 2026-11-11: the list and read wire described here is on `main` (`DeskReq `DeskRequest::Fork`, `DeskResponse::Forks`, `DeskResponse::Fork`; `crates/huginn-mind/src/desk_wire.rs` and `desk_wire/reads.rs`; the socket is `crates/huginn-daemon/src/desk_door.rs`). No running daemon binds a desk socket yet (follow-up `eureka-substrate:follow_up:desk-door-ops-root-run`). Two parts -are planned and not built: `raised_at` on the card, and the listing gate that decides which forks a +are planned and not built: the card's `raised_at`, `decider_count` and the record's `answerer_count` +and nullable `answerer` (the shapes pinned below), and the listing gate that decides which forks a reader sees. They are cut specs `cut-desk-feed-raised-at` and `cut-desk-feed-listing` in the `eureka-substrate` campaign. @@ -18,7 +19,8 @@ go live on a Mind that holds private questions. This page is the wire contract for the consumer, Bifrost's `UnixSocketDeskDoor` (Bifrost PR #67, `docs/desk-feed-design.md`). The golden bytes are in `crates/huginn-mind/tests/fixtures/desk-feed/`. Huginn's own encoders write them (`crates/huginn-mind/tests/desk_feed_fixtures.rs`), so a decoder -that reads them reads the door. The document shapes inside the payload (`gamecult.fork.list.v0`, +that reads them reads the door. The reader-count shapes are CultLib's bytes, vendored in +`cultlib-3a51bbab/` (below). The document shapes inside the payload (`gamecult.fork.list.v0`, `gamecult.fork.entry.v0`, `gamecult.fork.card.v0`, `gamecult.fork.record.v0`, `gamecult.desk_answer.v0`, `gamecult.fork.refusal.v0`) are CultLib's (`contracts/cultnet/governance/fork-card.md`) and are not restated here. @@ -106,7 +108,7 @@ shape at decode is owed by `cut-desk-feed-listing.r6`. The test `every_account_id_in_the_fixtures_is_a_lowercase_d_format_uuid` pins the grammar. The fixtures use synthetic UUIDs. -## Account ids by reader (planned; needs a CultLib change) +## Account ids by reader (shape pinned in CultLib; Huginn side planned) A reader is a **decider** when the call's `subject` is in the call's `deciders`, compared byte for byte; both values are in the account-id format above. Any other reader is a non-decider. @@ -118,37 +120,34 @@ 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`. -- **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. +- **The shape**, pinned to CultLib commit `3a51bbabad244a56ec4521080530d88744001ce4` (#117 "Fork card: + decider_count, answerer_count, nullable answerer (reader views)", on `af914253` #115 `raised_at`; + ruling `eureka-substrate:ruling:desk-reader-counts-shape`). The bytes are CultLib's own vectors + (`contracts/cultnet/governance/fork-card-vectors.json`, `messagePackHex`), vendored unedited in + `crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/` (its README lists each file and + its vector name). Read off those bytes: + - **Card:** 18 keys, header `DE 00 12` (map16): `schemaVersion` plus 17 fields, in this order: + `fork_id, title, context, options, recommendation, depends, leans, flag, raised_in, raised_at, + deciders, decider_count, status, ruling, ruled_from, order, options_hash`. `raised_at` is an + RFC3339 string or nil, always written. `decider_count` is an integer from 1 to 64 right after + `deciders`. An empty `deciders` means the ids are withheld from this reader, never "no + deciders"; a non-empty list holds exactly `decider_count` ids. + - **Record:** 6 keys, header `86`: `schemaVersion, fork_id, options_hash, counted_answer_id, + answerer_count, answers`, so `answerer_count` is the fifth key, right before `answers`. It is + the number of distinct answerers over ALL the fork's stored answers, refusals included, and is + the same for both readers. In a non-decider's view every answerer is nil, and the count is then + between 1 and the number of answers (0 with no answers). + - **Answer:** `gamecult.desk_answer.v0` keeps its 16 keys (header `DE 00 10`); `answerer` is a + string or nil on all four variants (`Counted`, `Revision`, `Retraction`, `Refused`), nil only in a + non-decider's view, with the key kept. Stored answers always carry it. + - **Entry:** header `83` (`schemaVersion, card, record`). The view is read off `deciders`: a + non-empty `deciders` names every answerer, an empty one names none. - `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 four answers by three accounts, one of - each variant (`Counted`, `Revision`, `Refused`, `Retraction`), with one account answering twice, as each reader sees it. - - The tests check: - - a non-decider's bytes hold no account id anywhere, and a decider's hold every id; - - `answerer_count` is 3 over four answers, 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. + - Huginn's real types do not carry these shapes yet: `ForkCard` has no `raised_at` or + `decider_count`, `ForkRecord` no `answerer_count`, `ForkDeskAnswer.answerer` is a non-null + string, and each type refuses unknown keys. The test + `huginn_types_do_not_yet_carry_the_pinned_shapes` pins that gap, and + `cut-desk-feed-listing` closes it by decoding these same bytes into the real types. Response (`huginn.desk_response.v1`): @@ -196,23 +195,21 @@ affected, because each Mind has its own socket and the client asks per Mind: An empty list is never served in place of a failure. `open_count: 0` means zero open forks. -## Card fields (pending CultLib's re-pinned card vectors) +## Card fields -`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 (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 -imported question the source mind's `committed_at` (`imported_from.committed_at`). A value that does not -parse as RFC3339 is served as nil. One known limit: after a campaign has moved twice (A to B to C), -C sees B's admission time, because an import quotes its source's receipt and B's receipt is B's +`raised_at` is a required card key whose value is an RFC3339 string or nil (CultLib #115, `af914253`). +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 imported question the source mind's `committed_at` (`imported_from.committed_at`). A value that +does not parse as RFC3339 is served as nil. One known limit: after a campaign has moved twice (A to B +to C), C sees B's admission time, because an import quotes its source's receipt and B's receipt is B's own (follow-up `desk-raised-at-import-chain`). -**No fixture here carries card bytes.** The card's key order and hex wait on the CultLib commit that -re-pins the card vectors with `raised_at`. Until then the card is opaque to these fixtures, and -every pinned response carries no entry. The entry-bearing fixtures (an open list page and a read -with `raised_at` as a string and as nil) are added by `cut-desk-feed-raised-at` against that -CultLib commit. +The card's key order and bytes are pinned by the vendored CultLib vectors (above): `card-operator-fork` +(`raised_at` a string), `card-raised-at-nil` and `card-withheld-deciders`. The fixtures Huginn's own +encoders write carry no entry; entry-bearing list and read fixtures from Huginn's output are added by +`cut-desk-feed-raised-at` and `cut-desk-feed-listing`. ## Trust boundary: what Huginn sends, what Bifrost does -- 2.47.3 From d5de551276ff23a7c04d824966d02202f2b0325d Mon Sep 17 00:00:00 2001 From: Eureka Date: Wed, 11 Nov 2026 18:27:49 +0000 Subject: [PATCH 4/4] Desk feed pin: say which answerer nulls the bytes show, tie CULTLIB_SHA to the README and doc, note Bifrost consumers Vendor CultLib's revision and retraction answer vectors (string answerer; no vector nulls them). The doc and test now say two variants are pinned nulled by bytes (Counted, Refused) and all four by ruling desk-reader-counts-shape. The sha test requires the fixture README and the doc to name CULTLIB_SHA verbatim. Doc notes Bifrost heimdall/desk-card-repin (f977a43) re-pin and DeskDocuments.cs JsonRequired answerer_count. Co-Authored-By: Claude Sonnet 5.5 --- .../huginn-mind/tests/desk_feed_fixtures.rs | 19 +++++++++++++++--- .../desk-feed/cultlib-3a51bbab/README.md | 2 ++ .../answer-retraction.msgpack | Bin 0 -> 436 bytes .../cultlib-3a51bbab/answer-revision.msgpack | Bin 0 -> 461 bytes docs/desk-feed-door.md | 12 +++++++++-- 5 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/answer-retraction.msgpack create mode 100644 crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/answer-revision.msgpack diff --git a/crates/huginn-mind/tests/desk_feed_fixtures.rs b/crates/huginn-mind/tests/desk_feed_fixtures.rs index f8e95fc..24b359c 100644 --- a/crates/huginn-mind/tests/desk_feed_fixtures.rs +++ b/crates/huginn-mind/tests/desk_feed_fixtures.rs @@ -264,6 +264,8 @@ const VENDORED: &[(&str, &str)] = &[ ("card-raised-at-nil", "b81bb825790ffe03fabe63c3f8a61c36bd50ca268aa6dff67b5bcfad3957e58c"), ("card-withheld-deciders", "d13b9b77461cfdf78f943ef56c6aeab852ab6b6f0b355e26c5b361f99c79c7f7"), ("answer-counted", "55d69ae3cc7ebbeeae7d501e8641640e115c3fb5aac399ea760588bca343b9e6"), + ("answer-revision", "19d7289395ae68cbaf6de1c0a1ef6c843211b2caf9462e1d2268cd0883386315"), + ("answer-retraction", "89156f41ec3ea54f5d47bd3318913ff013785f4727916875be97785935cb7735"), ("answer-withheld-answerer", "b66b4f921cdd374be04af3de34c59e254a5e542027ae8c57f196a3ea3406e427"), ("answer-refused-stale", "3183bc9c483f4a625332cda11ef22a9c8560868a7728b77daa63a1960a3cd3d6"), ("entry-open-counted", "b51f499e8dd0465cf5df9d68d8fccbe94dd1339fd3157969693dd0d4ecb2549b"), @@ -377,6 +379,13 @@ const RECORD_KEYS: [&str; 6] = ["schemaVersion", "fork_id", "options_hash", "cou #[test] fn the_vendored_cultlib_vectors_are_the_pinned_bytes() { assert_eq!(CULTLIB_SHA.len(), 40); + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/desk-feed/cultlib-3a51bbab"); + let docs = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../docs/desk-feed-door.md"); + for path in [dir.join("README.md"), docs] { + let text = std::fs::read_to_string(&path).unwrap_or_else(|error| panic!("{}: {error}", path.display())); + assert!(text.contains(CULTLIB_SHA), "{} does not name the pinned sha {CULTLIB_SHA}", path.display()); + } + assert!(dir.to_str().unwrap().ends_with(&format!("cultlib-{}", &CULTLIB_SHA[..8])), "the directory is named for the pin"); for (name, sha) in VENDORED { let digest: String = Sha256::digest(vendored(name)).iter().map(|byte| format!("{byte:02x}")).collect(); assert_eq!(&digest, sha, "{name} is not CultLib {CULTLIB_SHA}'s bytes"); @@ -410,7 +419,9 @@ fn the_cards_are_eighteen_keys_with_raised_at_and_decider_count() { assert_eq!(decode(&vendored("card-withheld-deciders")).get("decider_count").int(), 2); } -/// `answerer` is a string or nil on every answer; `Value::Nil` only when `named` is false. +/// `answerer` is a string or nil on every answer; `Value::Nil` only when `named` is false. The bytes +/// null it on `Counted` and `Refused` only; nullable on all four variants is the ruling +/// `desk-reader-counts-shape`, and CultLib has no vector with a `Revision` or `Retraction` nulled. fn check_answer(answer: &Value, named: bool) { assert_eq!(answer.keys().len(), 16); assert_eq!(answer.keys()[13], "answerer"); @@ -423,12 +434,14 @@ fn check_answer(answer: &Value, named: bool) { #[test] fn the_answers_keep_sixteen_keys_and_answerer_is_a_string_or_nil() { - for (name, named) in [("answer-counted", true), ("answer-refused-stale", true), ("answer-withheld-answerer", false)] { + for (name, named) in [("answer-counted", true), ("answer-revision", true), ("answer-retraction", true), ("answer-refused-stale", true), ("answer-withheld-answerer", false)] { let bytes = vendored(name); assert_eq!(bytes[..3], [0xde, 0x00, 0x10], "{name}: header"); check_answer(&decode(&bytes), named); } - assert_eq!(decode(&vendored("answer-refused-stale")).get("outcome").str(), "Refused"); + for (name, outcome) in [("answer-counted", "Counted"), ("answer-revision", "Revision"), ("answer-retraction", "Retraction"), ("answer-refused-stale", "Refused")] { + assert_eq!(decode(&vendored(name)).get("outcome").str(), outcome); + } } /// The entry is `83` and its record `86`, `answerer_count` the fifth key. In a decider's view the count diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/README.md b/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/README.md index ed1953c..a3203a7 100644 --- a/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/README.md +++ b/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/README.md @@ -13,6 +13,8 @@ from Huginn's encoders: Huginn's own types do not carry these shapes yet (see | `card-raised-at-nil` | `card.valid` `raised_at_nil` | `raised_at` nil, key kept | | `card-withheld-deciders` | `card.valid` `withheld_deciders` | empty `deciders`, `decider_count` 2 | | `answer-counted` | `deskAnswer.valid` `counted` | 16 keys (`DE 00 10`), `answerer` a string | +| `answer-revision` | `deskAnswer.valid` `revision` | a `Revision`, `answerer` a string (no CultLib vector nulls a Revision) | +| `answer-retraction` | `deskAnswer.valid` `retraction` | a `Retraction`, `answerer` a string (no CultLib vector nulls a Retraction) | | `answer-withheld-answerer` | `deskAnswer.valid` `withheld_answerer` | `answerer` nil | | `answer-refused-stale` | `deskAnswer.valid` `refused_stale` | a `Refused` answer, `answerer` a string | | `entry-open-counted` | `transport.entry.valid` `open_counted` | decider view: entry `83`, record `86`, `answerer_count` 1 | diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/answer-retraction.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/answer-retraction.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..7f2fb22cb08d654a5665ed6354832bb8b0597102 GIT binary patch literal 436 zcmZutJ5Iwu6yyjTA_dJx`}4Qr21p1|q_LiN-(oAVH|(y1xK0$2ABYBtHsTg(I16zM z9aGGEqcd#G71tEXz(1%k z;bGO1sOcy$bUz^`Z zcN`(rOz~0>kXb}zoF*hqlZ?a=l~M`Kk(n)Yp|Y+H&zqY$ItbS79G-jY`yA0T1SkC@ MvZu7CUJela2QOQjoB#j- literal 0 HcmV?d00001 diff --git a/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/answer-revision.msgpack b/crates/huginn-mind/tests/fixtures/desk-feed/cultlib-3a51bbab/answer-revision.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..aaff18f05180827ad5b5d1739a4a627507255ba0 GIT binary patch literal 461 zcmZ`#F>V_%5cCuL1Qjk^gaXOq@lLAp0|7yh!p&i|T>1#~sLn_&xi`n9K&Uqhy888Yi2E*J)%$md2F}o<3G)Qd+DlQx;N5u7$9qM2W(Xl@%09Ra#W@xvphVTBWG~D+&$- zg%+slAJ_(m^!<*~)`v_FGe3%CmxRNX52MsVq>;ip^5zdN!F7%EjqP(E>1qES8BbTljX*gs7^q3fl&&jQ*VTk+DCY_bBT3i; l^tlbu<-7fCMG=(9pg^z5sVCq=5hc literal 0 HcmV?d00001 diff --git a/docs/desk-feed-door.md b/docs/desk-feed-door.md index d7f1ff5..a930172 100644 --- a/docs/desk-feed-door.md +++ b/docs/desk-feed-door.md @@ -138,11 +138,19 @@ byte; both values are in the account-id format above. Any other reader is a non- the same for both readers. In a non-decider's view every answerer is nil, and the count is then between 1 and the number of answers (0 with no answers). - **Answer:** `gamecult.desk_answer.v0` keeps its 16 keys (header `DE 00 10`); `answerer` is a - string or nil on all four variants (`Counted`, `Revision`, `Retraction`, `Refused`), nil only in a - non-decider's view, with the key kept. Stored answers always carry it. + string or nil, nil only in a non-decider's view, with the key kept. The bytes pin a nil `answerer` + on two variants, `Counted` (`answer-withheld-answerer`) and `Refused` (the withheld entries). That + it is nullable on all four variants (`Counted`, `Revision`, `Retraction`, `Refused`) is the ruling + `eureka-substrate:ruling:desk-reader-counts-shape`, not a vector: CultLib's `revision` and + `retraction` vectors carry a string `answerer` and no CultLib vector has either nulled. Stored + answers always carry it. - **Entry:** header `83` (`schemaVersion, card, record`). The view is read off `deciders`: a non-empty `deciders` names every answerer, an empty one names none. - `options_hash` does not change (still five members). + - **Consumers:** Bifrost branch `heimdall/desk-card-repin` (`f977a43`) vendors Huginn's desk + fixtures from `79d954c` and needs a re-pin after this PR. Bifrost's `DeskDocuments.cs` marks + `answerer_count` `JsonRequired`, so its merge must wait for `cut-desk-feed-listing`: today's door + does not send the field. - Huginn's real types do not carry these shapes yet: `ForkCard` has no `raised_at` or `decider_count`, `ForkRecord` no `answerer_count`, `ForkDeskAnswer.answerer` is a non-null string, and each type refuses unknown keys. The test -- 2.47.3