From 5785c0c8c85f50164d1cb724bd2d327071e6e2f3 Mon Sep 17 00:00:00 2001 From: Eureka Date: Tue, 10 Nov 2026 15:26:03 +0000 Subject: [PATCH] huginn-mind: options_hash at the bytes CultLib PR #72 pins NFC every string, JCS (struct fields in key order through serde_json), SHA-256, lowercase hex; the seam stays the single owner. Libby's 15 vectors, A-G included, run as tests; the test fixture hash is gone. Adds unicode-normalization. Co-Authored-By: Claude Sonnet 5.5 --- Cargo.lock | 16 + crates/huginn-mind/Cargo.toml | 1 + crates/huginn-mind/src/desk.rs | 141 ++++--- .../vectors/fork-card-options-hash.json | 343 ++++++++++++++++++ 4 files changed, 459 insertions(+), 42 deletions(-) create mode 100644 crates/huginn-mind/vectors/fork-card-options-hash.json diff --git a/Cargo.lock b/Cargo.lock index a3cfb26..d887bf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -961,6 +961,7 @@ dependencies = [ "serde_path_to_error", "sha2", "tempfile", + "unicode-normalization", ] [[package]] @@ -1988,6 +1989,12 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + [[package]] name = "tokio" version = "1.53.1" @@ -2073,6 +2080,15 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.13.3" diff --git a/crates/huginn-mind/Cargo.toml b/crates/huginn-mind/Cargo.toml index 48e61ad..c5a25ff 100644 --- a/crates/huginn-mind/Cargo.toml +++ b/crates/huginn-mind/Cargo.toml @@ -28,6 +28,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" serde_path_to_error = "0.1" sha2 = "0.10" +unicode-normalization = "0.1" [dev-dependencies] tempfile = "3" diff --git a/crates/huginn-mind/src/desk.rs b/crates/huginn-mind/src/desk.rs index c1616d4..29ea9e8 100644 --- a/crates/huginn-mind/src/desk.rs +++ b/crates/huginn-mind/src/desk.rs @@ -62,20 +62,44 @@ pub enum DeskAnswerOutcome { Refused(MindRefusal), } -/// The hash of a fork as the desk shows it, which an answer must quote (rule 4). -/// -/// PENDING: the bytes are Libby's CultNet card contract (CultLib PR #72) and are -/// not implemented in this seam yet. Until they are, the seam fails closed: no -/// request can quote the value returned here, so every answer is stored -/// `Refused { StaleFork }`. Tests are served by `fixtures::fixture_options_hash`. +/// The hash of a fork as the desk shows it, which an answer must quote (rule 4): the +/// lowercase hex SHA-256 of the canonical bytes pinned by CultLib PR #72 +/// (`contracts/cultnet/governance/fork-card.md`, "Options hash"). The five members +/// `context`, `depends`, `options[{id, text}]`, `recommendation`, `title`, every string +/// NFC-normalised, serialised as RFC 8785 JCS. The struct declares its fields in JCS key +/// order, and `serde_json::to_string` writes no whitespace, escapes only `"`, `\` and the +/// controls below U+0020 (lowercase `\u00xx`, `\b \t \n \f \r` short forms) and +/// writes everything else raw, which is JCS for strings; the five members hold no number. +/// `depends` are the stored lines verbatim (`PipelineQuestion.depends` is prose lines). +/// This is the one owner: the counting rule calls it and nothing else computes the hash. pub fn options_hash(question: &PipelineQuestion) -> Sha256Hex { - #[cfg(test)] - return fixtures::fixture_options_hash(question); - #[cfg(not(test))] - { - let _ = question; - Sha256Hex("0".repeat(64)) + use sha2::{Digest, Sha256}; + use unicode_normalization::UnicodeNormalization; + + // Declared in JCS key order (UTF-16 code units: context < depends < options < recommendation < title). + #[derive(serde::Serialize)] + struct Shown { + context: String, + depends: Vec, + options: Vec, + recommendation: String, + title: String, } + #[derive(serde::Serialize)] + struct ShownOption { + id: String, + text: String, + } + let nfc = |text: &str| text.nfc().collect::(); + let shown = Shown { + context: nfc(&question.question.0), + depends: question.depends.iter().map(|line| nfc(&line.0)).collect(), + options: question.options.iter().map(|option| ShownOption { id: nfc(&option.label.0), text: nfc(&option.text.0) }).collect(), + recommendation: nfc(&question.recommended.0), + title: nfc(&question.title.0), + }; + let canonical = serde_json::to_string(&shown).expect("strings serialise"); + Sha256Hex(Sha256::digest(canonical.as_bytes()).iter().map(|byte| format!("{byte:02x}")).collect()) } /// Every desk answer of one fork, with its key. @@ -228,36 +252,6 @@ impl Mind { } } -#[cfg(test)] -pub(crate) mod fixtures { - use super::*; - - /// A stand-in for `options_hash` served to tests only: a deterministic function of every - /// member the binding spec names, order-sensitive, NOT SHA-256 and NOT Libby's bytes. - /// It lets the counting rules be tested while the real bytes are pending. - pub(crate) fn fixture_options_hash(question: &PipelineQuestion) -> Sha256Hex { - let text = format!( - "{:?}|{:?}|{:?}|{:?}|{:?}", - question.title.0, - question.question.0, - question.options.iter().map(|option| (&option.label.0, &option.text.0)).collect::>(), - question.recommended.0, - question.depends.iter().map(|line| &line.0).collect::>() - ); - // Four rounds of FNV-1a with distinct seeds make 64 hex digits. - let digits: String = (0u64..4) - .map(|round| { - let mut hash = 0xcbf2_9ce4_8422_2325u64 ^ round.wrapping_mul(0x9e37_79b9_7f4a_7c15); - for byte in text.bytes() { - hash = (hash ^ u64::from(byte)).wrapping_mul(0x0100_0000_01b3); - } - format!("{hash:016x}") - }) - .collect(); - Sha256Hex(digits) - } -} - #[cfg(test)] mod tests { use super::*; @@ -670,4 +664,67 @@ mod tests { assert_eq!(head(&mind), before); assert_eq!(counted(&mind, FORK), Some(answer)); } + + /// Libby's vectors (CultLib PR #72 head 7aec4961, `fork-card-vectors.json`, `optionsHash`): the seven + /// of the binding file, A to G, and the eight she adds. Each is recomputed from its shown members. + fn vectors() -> Vec { + serde_json::from_str(include_str!("../vectors/fork-card-options-hash.json")).unwrap() + } + + fn shown(vector: &serde_json::Value) -> PipelineQuestion { + let text = |value: &serde_json::Value| -> String { value.as_str().unwrap().to_string() }; + let D::Question(mut question) = question("Q1", &["A", "B"], "A") else { unreachable!() }; + question.title = text(&vector["title"]).as_str().into(); + question.question = text(&vector["context"]).as_str().into(); + question.recommended = Label(text(&vector["recommendation"])); + question.options = vector["options"] + .as_array() + .unwrap() + .iter() + .map(|option| eureka_pipeline::QuestionOption { label: Label(text(&option["id"])), text: text(&option["text"]).as_str().into() }) + .collect(); + question.depends = vector["depends"].as_array().unwrap().iter().map(|line| text(line).as_str().into()).collect(); + question + } + + #[test] + fn options_hash_reproduces_binding_vectors() { + let vectors = vectors(); + assert_eq!(vectors.len(), 15); + for vector in &vectors { + let name = vector["name"].as_str().unwrap(); + assert_eq!(options_hash(&shown(vector)).0, vector["sha256"].as_str().unwrap(), "{name}"); + } + let by = |tag: &str| vectors.iter().find(|vector| vector["heimdall"] == tag).unwrap(); + let hash = |tag: &str| options_hash(&shown(by(tag))).0; + // The pinned values of the binding file, 1.5. + assert_eq!(hash("A"), "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48"); + assert_eq!(hash("B"), "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc"); + assert_eq!(hash("C"), "ac67b41539f869f6203ac0908819df046e4328b27795743a919bd4ab390596b5"); + assert_eq!(hash("D"), "71d5c413478ad06aaa1cbdd9e91c53f0cb4cef1dbd4c51bf3d473bccc6a5a272"); + assert_eq!(hash("F"), "fb53aa3a9f7e74029f755d774064eedd240ab90d8fc64723f97c31b3d2846d40"); + assert_eq!(hash("G"), "f856edce121db4121b5f1af400b1c471ebbfa6579a042aca9a34ae31c15f5c86"); + // E spells "Cafe" decomposed; NFC makes it D. + assert_eq!(hash("E"), hash("D")); + // Each shown member moves the hash. + let distinct = ["A", "B", "C", "D", "F", "G"].map(hash); + assert_eq!(distinct.iter().collect::>().len(), 6); + } + + #[test] + fn options_hash_needs_nfc_to_equal_across_spellings() { + // Without NFC the decomposed spelling hashes differently: this is what E pins. + use sha2::{Digest, Sha256}; + let vectors = vectors(); + let raw = |tag: &str| { + let vector = vectors.iter().find(|vector| vector["heimdall"] == tag).unwrap(); + let json = serde_json::json!({ + "context": vector["context"], "depends": vector["depends"], "options": vector["options"], + "recommendation": vector["recommendation"], "title": vector["title"], + }); + Sha256::digest(serde_json::to_string(&json).unwrap().as_bytes()).iter().map(|byte| format!("{byte:02x}")).collect::() + }; + assert_ne!(raw("E"), raw("D")); + assert_eq!(raw("D"), options_hash(&shown(vectors.iter().find(|vector| vector["heimdall"] == "D").unwrap())).0); + } } diff --git a/crates/huginn-mind/vectors/fork-card-options-hash.json b/crates/huginn-mind/vectors/fork-card-options-hash.json new file mode 100644 index 0000000..ea9af45 --- /dev/null +++ b/crates/huginn-mind/vectors/fork-card-options-hash.json @@ -0,0 +1,343 @@ +[ + { + "name": "heimdall_A_five_members", + "heimdall": "A", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + { + "name": "heimdall_B_recommendation_changed", + "heimdall": "B", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "yes", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"yes\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A22796573222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc" + }, + { + "name": "heimdall_C_option_order_changed", + "heimdall": "C", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "no", + "text": "Keep it local." + }, + { + "id": "yes", + "text": "Share it." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"no\",\"text\":\"Keep it local.\"},{\"id\":\"yes\",\"text\":\"Share it.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D2C7B226964223A22796573222C2274657874223A2253686172652069742E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "ac67b41539f869f6203ac0908819df046e4328b27795743a919bd4ab390596b5" + }, + { + "name": "heimdall_D_nfc_escapes_astral", + "heimdall": "D", + "title": "Café “desk”", + "context": "Line one\nLine two — 😀", + "options": [ + { + "id": "a", + "text": "x" + }, + { + "id": "b", + "text": "y" + } + ], + "recommendation": "a", + "depends": [], + "canonical": "{\"context\":\"Line one\\nLine two — 😀\",\"depends\":[],\"options\":[{\"id\":\"a\",\"text\":\"x\"},{\"id\":\"b\",\"text\":\"y\"}],\"recommendation\":\"a\",\"title\":\"Café “desk”\"}", + "canonicalHex": "7B22636F6E74657874223A224C696E65206F6E655C6E4C696E652074776F20E2809420F09F9880222C22646570656E6473223A5B5D2C226F7074696F6E73223A5B7B226964223A2261222C2274657874223A2278227D2C7B226964223A2262222C2274657874223A2279227D5D2C227265636F6D6D656E646174696F6E223A2261222C227469746C65223A22436166C3A920E2809C6465736BE2809D227D", + "sha256": "71d5c413478ad06aaa1cbdd9e91c53f0cb4cef1dbd4c51bf3d473bccc6a5a272" + }, + { + "name": "heimdall_E_decomposed_equals_D", + "heimdall": "E", + "title": "Café “desk”", + "context": "Line one\nLine two — 😀", + "options": [ + { + "id": "a", + "text": "x" + }, + { + "id": "b", + "text": "y" + } + ], + "recommendation": "a", + "depends": [], + "canonical": "{\"context\":\"Line one\\nLine two — 😀\",\"depends\":[],\"options\":[{\"id\":\"a\",\"text\":\"x\"},{\"id\":\"b\",\"text\":\"y\"}],\"recommendation\":\"a\",\"title\":\"Café “desk”\"}", + "canonicalHex": "7B22636F6E74657874223A224C696E65206F6E655C6E4C696E652074776F20E2809420F09F9880222C22646570656E6473223A5B5D2C226F7074696F6E73223A5B7B226964223A2261222C2274657874223A2278227D2C7B226964223A2262222C2274657874223A2279227D5D2C227265636F6D6D656E646174696F6E223A2261222C227469746C65223A22436166C3A920E2809C6465736BE2809D227D", + "sha256": "71d5c413478ad06aaa1cbdd9e91c53f0cb4cef1dbd4c51bf3d473bccc6a5a272" + }, + { + "name": "heimdall_F_option_id_changed", + "heimdall": "F", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "share", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"share\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A227368617265222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "fb53aa3a9f7e74029f755d774064eedd240ab90d8fc64723f97c31b3d2846d40" + }, + { + "name": "heimdall_G_depends_emptied", + "heimdall": "G", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B5D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "f856edce121db4121b5f1af400b1c471ebbfa6579a042aca9a34ae31c15f5c86" + }, + { + "name": "option_text_changed", + "heimdall": null, + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it!" + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it!\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A22536861726520697421227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "9ce0d2770497fdd2d5338e66f9f1ae1741d87301db4c793407c25762929392b6" + }, + { + "name": "title_changed", + "heimdall": null, + "title": "Slot mark consent?", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent?\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E743F227D", + "sha256": "30fe0c5bc88bd21d64592a495316c30e80fcee6cacf5c63fd9a818ca5cf87d0f" + }, + { + "name": "context_changed", + "heimdall": null, + "title": "Slot mark consent", + "context": "Should slot marks be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should slot marks be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C6420736C6F74206D61726B73206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "716de65096e34fd89cc92848b667d90f5c27eeb66929da3d340cabb80155e173" + }, + { + "name": "depends_order_changed", + "heimdall": null, + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience", + "x:question:y" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\",\"x:question:y\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365222C22783A7175657374696F6E3A79225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "fc3f651c6f498385256fc1232d5db31ece9dcc542e391f97245d27bc9e385b3d" + }, + { + "name": "depends_order_changed_reversed", + "heimdall": null, + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "x:question:y", + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"x:question:y\",\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B22783A7175657374696F6E3A79222C2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "b22642e30f8d68839e3a0df5bc23b83ff50a839fcecf0366a65fa6e3cf873796" + }, + { + "name": "hidden_members_do_not_move_it", + "heimdall": null, + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + { + "name": "control_characters_and_markup_are_escaped_minimally", + "heimdall": null, + "title": "q \" b \\ t \t n \n bell \u0007 del ", + "context": " 
 &", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\" 
 &\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"q \\\" b \\\\ t \\t n \\n bell \\u0007 del \"}", + "canonicalHex": "7B22636F6E74657874223A223C2F7363726970743E20E280A82026222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A2271205C222062205C5C2074205C74206E205C6E2062656C6C205C75303030372064656C207F227D", + "sha256": "f2d5a09513ce728129f6ca13f7123aa75b2bb77541b1de8946f39239634b40fe" + }, + { + "name": "astral_option_id_and_text", + "heimdall": null, + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "😀", + "text": "𝄞" + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"😀\",\"text\":\"𝄞\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22F09F9880222C2274657874223A22F09D849E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "836fbfce16127132bae100aa773d328a85ad08d241ae9bf47ea7f84766762cac" + } +] \ No newline at end of file