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 <noreply@anthropic.com>
This commit is contained in:
Eureka 2026-11-10 15:26:03 +00:00
commit 5785c0c8c8
4 changed files with 460 additions and 43 deletions

16
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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<String>,
options: Vec<ShownOption>,
recommendation: String,
title: String,
}
#[derive(serde::Serialize)]
struct ShownOption {
id: String,
text: String,
}
let nfc = |text: &str| text.nfc().collect::<String>();
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<S: MindStore> Mind<S> {
}
}
#[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::<Vec<_>>(),
question.recommended.0,
question.depends.iter().map(|line| &line.0).collect::<Vec<_>>()
);
// 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::Value> {
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::<std::collections::BTreeSet<_>>().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::<String>()
};
assert_ne!(raw("E"), raw("D"));
assert_eq!(raw("D"), options_hash(&shown(vectors.iter().find(|vector| vector["heimdall"] == "D").unwrap())).0);
}
}

View file

@ -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": "</script> 
 &",
"options": [
{
"id": "yes",
"text": "Share it."
},
{
"id": "no",
"text": "Keep it local."
}
],
"recommendation": "no",
"depends": [
"gamecult-chat:question:file-zone-audience"
],
"canonical": "{\"context\":\"</script> 
 &\",\"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"
}
]