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 <noreply@anthropic.com>
This commit is contained in:
parent
79d954cad1
commit
30a05309c2
9 changed files with 124 additions and 56 deletions
|
|
@ -28,6 +28,8 @@ const RUNTIME: &str = "huginn-eureka";
|
||||||
/// other decider). Synthetic values.
|
/// other decider). Synthetic values.
|
||||||
const READER: &str = "5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6f";
|
const READER: &str = "5f0c2b1e-8a4d-4c3b-9e7f-1a2b3c4d5e6f";
|
||||||
const OTHER_DECIDER: &str = "0b9d7e3a-6c21-4f58-a0d4-9e8f7a6b5c4d";
|
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
|
/// 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.
|
/// synthetic: the hash is CultLib's sample digest, not this question's.
|
||||||
const FORK: &str = "eureka-substrate:question:desk-listing-authority";
|
const FORK: &str = "eureka-substrate:question:desk-listing-authority";
|
||||||
|
|
@ -102,33 +104,40 @@ fn record() -> ForkRecord {
|
||||||
answers: vec![
|
answers: vec![
|
||||||
answer("n1", "Counted", None, READER, "The asker lists her own forks.", "2026-11-11T17:00:00Z"),
|
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"),
|
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
|
// ---- Reader views, as MIRROR TYPES (ruling desk-read-visibility-withheld-ids; shape ruling
|
||||||
// deciders, and returns counts). CultLib's card and desk_answer cannot carry this yet (card
|
// desk-reader-counts-shape, Libby's CultLib reader-counts cut). These types exist only here: Huginn
|
||||||
// `deciders` has minItems 1 and no count key; `answerer` is a required non-null id), so these are
|
// does not project reader views yet (cut-desk-feed-listing), and CultLib's re-pinned vectors are not
|
||||||
// fragments in the shape proposed to Libby (follow-up `cultlib-fork-card-reader-counts`), built from
|
// posted yet. They pin the rules, not CultLib's bytes; when Libby posts the sha, her vectors are
|
||||||
// mirror types that exist only here. They are not Huginn's output until that contract lands and
|
// decoded against Huginn's real types instead. ----
|
||||||
// `cut-desk-feed-listing` builds it. ----
|
|
||||||
|
|
||||||
/// The two card keys the proposal changes, as a fragment: a decider gets the ids, anyone else `[]`
|
/// The card's `deciders` and `decider_count` (right after `deciders`), as a fragment. Not a card.
|
||||||
/// and the count. Not a card.
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct CardViewers {
|
struct CardViewers {
|
||||||
deciders: Vec<String>,
|
deciders: Vec<String>,
|
||||||
decider_count: u64,
|
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 {
|
fn card_viewers(decider: bool) -> CardViewers {
|
||||||
let ids = vec![READER.to_string(), OTHER_DECIDER.to_string()];
|
let ids = vec![READER.to_string(), OTHER_DECIDER.to_string()];
|
||||||
CardViewers { decider_count: ids.len() as u64, deciders: if decider { ids } else { Vec::new() } }
|
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)]
|
#[derive(Serialize)]
|
||||||
struct WithheldAnswer {
|
struct ViewAnswer {
|
||||||
#[serde(rename = "schemaVersion")]
|
#[serde(rename = "schemaVersion")]
|
||||||
schema_version: AnswerSchema,
|
schema_version: AnswerSchema,
|
||||||
answer_id: String,
|
answer_id: String,
|
||||||
|
|
@ -148,25 +157,27 @@ struct WithheldAnswer {
|
||||||
request_id: String,
|
request_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `gamecult.fork.record.v0` as proposed for a reader who is not a decider: every `answerer` nil and
|
/// `gamecult.fork.record.v0` with `answerer_count`: six keys (header 0x86). The count's position
|
||||||
/// `answerer_count`, the number of distinct answerers, in their place.
|
/// among the six is CultLib's; this mirror puts it before `answers` until the vectors say.
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct WithheldRecord {
|
struct ViewRecord {
|
||||||
#[serde(rename = "schemaVersion")]
|
#[serde(rename = "schemaVersion")]
|
||||||
schema_version: RecordSchema,
|
schema_version: RecordSchema,
|
||||||
fork_id: String,
|
fork_id: String,
|
||||||
options_hash: String,
|
options_hash: String,
|
||||||
counted_answer_id: Option<String>,
|
counted_answer_id: Option<String>,
|
||||||
answerer_count: u64,
|
answerer_count: u64,
|
||||||
answers: Vec<WithheldAnswer>,
|
answers: Vec<ViewAnswer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
let mut answerers: Vec<&str> = record.answers.iter().map(|answer| answer.answerer.as_str()).collect();
|
||||||
answerers.sort_unstable();
|
answerers.sort_unstable();
|
||||||
answerers.dedup();
|
answerers.dedup();
|
||||||
let answerer_count = answerers.len() as u64;
|
let answerer_count = answerers.len() as u64;
|
||||||
WithheldRecord {
|
ViewRecord {
|
||||||
schema_version: record.schema_version,
|
schema_version: record.schema_version,
|
||||||
fork_id: record.fork_id,
|
fork_id: record.fork_id,
|
||||||
options_hash: record.options_hash,
|
options_hash: record.options_hash,
|
||||||
|
|
@ -175,7 +186,7 @@ fn withheld(record: ForkRecord) -> WithheldRecord {
|
||||||
answers: record
|
answers: record
|
||||||
.answers
|
.answers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|answer| WithheldAnswer {
|
.map(|answer| ViewAnswer {
|
||||||
schema_version: answer.schema_version,
|
schema_version: answer.schema_version,
|
||||||
answer_id: answer.answer_id,
|
answer_id: answer.answer_id,
|
||||||
fork_id: answer.fork_id,
|
fork_id: answer.fork_id,
|
||||||
|
|
@ -189,7 +200,7 @@ fn withheld(record: ForkRecord) -> WithheldRecord {
|
||||||
answered_at: answer.answered_at,
|
answered_at: answer.answered_at,
|
||||||
received_at: answer.received_at,
|
received_at: answer.received_at,
|
||||||
claim_in_future: answer.claim_in_future,
|
claim_in_future: answer.claim_in_future,
|
||||||
answerer: None,
|
answerer: decider.then_some(answer.answerer),
|
||||||
authority: answer.authority,
|
authority: answer.authority,
|
||||||
request_id: answer.request_id,
|
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<u8>)> {
|
fn reader_views() -> Vec<(&'static str, Vec<u8>)> {
|
||||||
vec![
|
vec![
|
||||||
("card-viewers-decider.synthetic", rmp_serde::to_vec_named(&card_viewers(true)).unwrap()),
|
("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()),
|
("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())
|
bytes.windows(text.len()).any(|window| window == text.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This test pins MIRROR TYPES ONLY (`CardViewers`, `WithheldRecord`), the shapes proposed to CultLib.
|
/// MIRROR TYPES ONLY: these tests pin the reader-view rules on the types above, not Huginn's
|
||||||
/// It does not exercise Huginn's projection, which does not withhold ids yet. The tests on real list and
|
/// projection (which does not withhold ids yet) and not CultLib's bytes (pending Libby's sha). The
|
||||||
/// read responses land with `cut-desk-feed-listing` (r6).
|
/// 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
|
/// A non-decider's fragments hold no account id anywhere in their bytes, only counts; a decider's
|
||||||
/// anywhere in its fragments' bytes. A decider gets every id. (Proposed shapes; the build cut pins
|
/// hold every id and the same counts.
|
||||||
/// the same on Huginn's real list and read responses.)
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_non_decider_gets_counts_and_no_ids_a_decider_gets_ids() {
|
fn a_non_decider_gets_counts_and_no_ids_a_decider_gets_ids() {
|
||||||
let views = reader_views();
|
let views = reader_views();
|
||||||
let bytes = |name: &str| views.iter().find(|(stem, _)| *stem == name).unwrap().1.clone();
|
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"] {
|
for name in ["card-viewers-non-decider.synthetic", "record-non-decider.mirror"] {
|
||||||
let view = bytes(name);
|
let held = bytes(name);
|
||||||
assert!(!holds(&view, READER) && !holds(&view, OTHER_DECIDER), "{name} carries an account id");
|
assert!([READER, OTHER_DECIDER, OUTSIDER].iter().all(|id| !holds(&held, id)), "{name} carries an account id");
|
||||||
}
|
}
|
||||||
let non_decider = card_viewers(false);
|
let held = bytes("card-viewers-decider.synthetic");
|
||||||
assert_eq!((non_decider.deciders.len(), non_decider.decider_count), (0, 2));
|
assert!(holds(&held, READER) && holds(&held, OTHER_DECIDER));
|
||||||
assert_eq!(withheld(record()).answerer_count, 2);
|
let held = bytes("record-decider.mirror");
|
||||||
let decider = bytes("card-viewers-decider.synthetic");
|
assert!([READER, OTHER_DECIDER, OUTSIDER].iter().all(|id| holds(&held, id)), "a decider's record keeps every answerer id");
|
||||||
assert!(holds(&decider, READER) && holds(&decider, OTHER_DECIDER));
|
assert_eq!(card_viewers(false).decider_count, card_viewers(true).decider_count);
|
||||||
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");
|
|
||||||
|
/// `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::<Vec<_>>();
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,13 @@ lands (see `docs/desk-feed-door.md`).
|
||||||
| `unavailable.response` | Huginn to Bifrost | `Refused` `Unavailable` (Bifrost: 503 for this Mind) |
|
| `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) |
|
| `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) |
|
| `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-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-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-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-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
|
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.
|
vectors for `raised_at`, so no entry-bearing list page or read is pinned yet.
|
||||||
|
|
|
||||||
1
crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.hex
vendored
Normal file
1
crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.hex
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7403a7616e737765727393de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e33a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a752656675736564a26f66c0a87265747261637473c0a4636f6465ad6e6f745f615f64656369646572ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74d6520746f6f2eab616e7377657265645f6174b4323032362d31312d31315431373a30363a30305aab72656365697665645f6174b4323032362d31312d31315431373a30363a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92463336131663265342d376236642d346538662d396130622d316332643365346635613662a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303033
|
||||||
BIN
crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.msgpack
vendored
Normal file
BIN
crates/huginn-mind/tests/fixtures/desk-feed/record-decider.mirror.payload.msgpack
vendored
Normal file
Binary file not shown.
|
|
@ -1 +1 @@
|
||||||
86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7402a7616e737765727392de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032
|
86ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31ae616e7377657265725f636f756e7403a7616e737765727393de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e33a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a752656675736564a26f66c0a87265747261637473c0a4636f6465ad6e6f745f615f64656369646572ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74d6520746f6f2eab616e7377657265645f6174b4323032362d31312d31315431373a30363a30305aab72656365697665645f6174b4323032362d31312d31315431373a30363a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572c0a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303033
|
||||||
Binary file not shown.
|
|
@ -1 +1 @@
|
||||||
85ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31a7616e737765727392de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032
|
85ad736368656d6156657273696f6eb767616d6563756c742e666f726b2e7265636f72642e7630a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438b1636f756e7465645f616e737765725f6964a26e31a7616e737765727393de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e31a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a7436f756e746564a26f66c0a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465be5468652061736b6572206c6973747320686572206f776e20666f726b732eab616e7377657265645f6174b4323032362d31312d31315431373a30303a30305aab72656365697665645f6174b4323032362d31312d31315431373a30303a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92435663063326231652d386134642d346333622d396537662d316132623363346435653666a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303031de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e32a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a85265766973696f6ea26f66a26e31a87265747261637473c0a4636f6465c0ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74167726565642eab616e7377657265645f6174b4323032362d31312d31315431373a30353a30305aab72656365697665645f6174b4323032362d31312d31315431373a30353a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92430623964376533612d366332312d346635382d613064342d396538663761366235633464a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303032de0010ad736368656d6156657273696f6eb767616d6563756c742e6465736b5f616e737765722e7630a9616e737765725f6964a26e33a7666f726b5f6964d930657572656b612d7375627374726174653a7175657374696f6e3a6465736b2d6c697374696e672d617574686f72697479a76f7574636f6d65a752656675736564a26f66c0a87265747261637473c0a4636f6465ad6e6f745f615f64656369646572ac6f7074696f6e735f68617368d94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438a663686f696365ab61736b696e672d6d696e64a46e6f7465a74d6520746f6f2eab616e7377657265645f6174b4323032362d31312d31315431373a30363a30305aab72656365697665645f6174b4323032362d31312d31315431373a30363a30305aaf636c61696d5f696e5f667574757265c2a8616e737765726572d92463336131663265342d376236642d346538662d396130622d316332643365346635613662a9617574686f72697479a64d656d626572aa726571756573745f6964d92430303030303030302d303030302d343030302d383030302d303030303030303030303033
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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
|
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:
|
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`.
|
`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
|
- **The shape** (ruling `eureka-substrate:ruling:desk-reader-counts-shape`, from Libby's CultLib
|
||||||
`deciders` must hold at least one item. `answerer` is a required, non-null id. The proposal to
|
reader-counts cut). CultLib main does not have it yet: on main the card has 16 required keys and
|
||||||
Libby (follow-up `cultlib-fork-card-reader-counts`):
|
`deciders` needs at least one item, and `answerer` is a required, non-null id.
|
||||||
- the card gains a required `decider_count` (an integer). A non-decider gets `deciders: []`; a
|
- **Card:** `decider_count` is an integer from 1 to 64, placed right after `deciders`. An empty
|
||||||
decider gets the ids. The count is the same for both.
|
`deciders` means the ids are withheld from this reader; a non-empty list holds exactly
|
||||||
- `answerer` becomes nullable: nil for a non-decider, with the key kept, as `note` is stripped.
|
`decider_count` ids. With `raised_at` the card has 18 keys (MessagePack header `DE 00 12`).
|
||||||
- the record gains a required `answerer_count`, the number of distinct answerers.
|
- **Record:** `answerer_count` is the number of distinct answerers over ALL the fork's stored
|
||||||
- **Proposed fixtures**, built from mirror types that exist only in the test:
|
answers, refusals included. It is the same for both readers. The record has 6 keys (header `86`).
|
||||||
- `card-viewers-decider.synthetic` and `card-viewers-non-decider.synthetic` hold only the two card
|
- **Answers:** `answerer` is nullable on all four variants (`Counted`, `Revision`, `Retraction`,
|
||||||
keys the proposal changes. They are not cards.
|
`Refused`). It is null only in a non-decider's view, with the key kept. Stored answers always
|
||||||
- `record-non-decider.proposed` is a whole record as the proposal shapes it.
|
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
|
The tests check:
|
||||||
appears anywhere in a non-decider's bytes. `record-whole` is the decider's view.
|
- 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`):
|
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)
|
## 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
|
`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`,
|
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
|
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
|
question's admission: the `committed_at` of the receipt that first admitted the question, or for an
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue