1use serde_json::{Map, Value};
18
19pub const KEY_ORDER: &[&str] = &[
23 "id",
25 "appeal_id",
26 "meeting_id",
27 "number",
28 "juror_number",
29 "name",
30 "role",
31 "seat",
32 "member",
33 "kind",
34 "type",
35 "case_type",
36 "category",
37 "landmark",
38 "round_type",
39 "title",
40 "target",
41 "target_entry_hash",
42 "original_action",
44 "constitutional_ref",
45 "reason",
46 "appeal_statement",
47 "body",
48 "note",
49 "attests",
50 "participants",
51 "concerns",
52 "basis",
53 "authority",
54 "steward_emergency",
56 "tabled_by_recused_steward",
57 "steward_contribution",
58 "responses",
59 "rounds",
60 "steward_recusal",
61 "votes",
62 "jury_verdicts",
63 "judge_ruling",
64 "context_analysis",
71 "jury_assessment",
72 "rationale",
73 "constitutional_refs",
74 "precedents_cited",
75 "position",
76 "questions",
77 "overrules",
78 "refer_to_council",
79 "referral_reason",
80 "modified_action",
81 "ready_to_vote",
82 "limits_steward_powers",
83 "vote",
84 "verdict",
85 "referred_to_council",
86 "recused",
88 "final_votes",
89 "abstentions",
90 "vote_tally",
91 "outcome",
92 "raw_text",
94 "old_key",
96 "key",
97 "new_key",
98 "purpose",
99 "from_seq",
100 "prev_hash",
101 "last_trusted",
102 "outgoing_certificate",
103 "certificate",
104 "statement",
105 "signatures",
106 "root_key",
107 "signature",
108 "proof",
109 "proof_signed_at",
110 "attachments",
112 "content",
113];
114
115pub fn is_trailing_key(key: &str) -> bool {
119 key == super::BLIND_KEY || key.starts_with("agora_")
120}
121
122fn key_rank(key: &str) -> (u8, usize) {
126 if key == super::BLIND_KEY {
127 (3, 0)
128 } else if is_trailing_key(key) {
129 (2, 0)
130 } else if let Some(i) = KEY_ORDER.iter().position(|k| *k == key) {
131 (0, i)
132 } else {
133 (1, 0)
134 }
135}
136
137pub fn ordered(obj: &Map<String, Value>) -> Vec<(&String, &Value)> {
140 let mut entries: Vec<_> = obj.iter().collect();
141 entries.sort_by_key(|(k, _)| key_rank(k));
142 entries
143}
144
145pub fn label(key: &str) -> String {
148 match key {
149 super::BLIND_KEY => return "Redaction blind".to_string(),
150 "raw_text" => return "Raw model output".to_string(),
151 _ => {}
152 }
153 if key.starts_with("agora_") {
154 return format!("Format {key}");
156 }
157 let words = key.trim_start_matches('_').replace('_', " ");
158 let mut chars = words.chars();
159 match chars.next() {
160 Some(first) => first.to_uppercase().chain(chars).collect(),
161 None => String::new(),
162 }
163}
164
165pub fn item_title(
169 parent_key: Option<&str>,
170 index: usize,
171 item: &Value,
172) -> String {
173 let field = |k: &str| -> Option<String> {
174 match item.get(k)? {
175 Value::String(s) if !s.is_empty() => Some(s.clone()),
176 Value::Number(n) => Some(n.to_string()),
177 _ => None,
178 }
179 };
180 let with = |head: String, tail: Option<String>| match tail {
181 Some(t) => format!("{head} — {t}"),
182 None => head,
183 };
184 let fallback = || {
186 let key = parent_key.unwrap_or("item");
187 format!(
188 "{} {}",
189 label(key.strip_suffix('s').unwrap_or(key)),
190 index + 1
191 )
192 };
193 match parent_key {
194 Some("rounds") => with(
195 format!(
196 "Round {}",
197 field("number").unwrap_or((index + 1).to_string())
198 ),
199 field("round_type"),
200 ),
201 Some("jury_verdicts") => with(
202 format!(
203 "Juror {}",
204 field("juror_number").unwrap_or((index + 1).to_string())
205 ),
206 field("verdict"),
207 ),
208 Some("responses") => match field("role") {
209 Some(role) => with(label(&role), field("vote")),
210 None => fallback(),
211 },
212 _ => field("name")
213 .or_else(|| field("role"))
214 .unwrap_or_else(fallback),
215 }
216}
217
218pub fn repeats_rationale(obj: &Map<String, Value>, key: &str) -> bool {
223 key == "raw_text"
224 && obj.get(key).is_some_and(Value::is_string)
225 && obj.get("rationale") == obj.get(key)
226}
227
228pub fn is_token(s: &str) -> bool {
231 s.len() >= 32 && !s.chars().any(char::is_whitespace)
232}
233
234pub fn is_prose(s: &str) -> bool {
236 s.contains('\n') || s.len() > 160
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use serde_json::json;
243
244 #[test]
245 fn key_order_has_no_duplicates() {
246 let mut seen = std::collections::HashSet::new();
247 for key in KEY_ORDER {
248 assert!(seen.insert(key), "{key} listed twice in KEY_ORDER");
249 }
250 }
251
252 fn keys(v: &Value) -> Vec<&str> {
253 ordered(v.as_object().unwrap())
254 .into_iter()
255 .map(|(k, _)| k.as_str())
256 .collect()
257 }
258
259 #[test]
260 fn appeal_reads_in_the_order_it_happened() {
261 let appeal = json!({
263 "outcome": "overturned",
264 "appeal_id": "x",
265 "case_type": "moderation_appeal",
266 "judge_ruling": {},
267 "jury_verdicts": [],
268 "original_action": {},
269 "appeal_statement": "s",
270 "referred_to_council": false,
271 });
272 assert_eq!(
273 keys(&appeal),
274 [
275 "appeal_id",
276 "case_type",
277 "original_action",
278 "appeal_statement",
279 "jury_verdicts",
280 "judge_ruling",
281 "referred_to_council",
282 "outcome",
283 ]
284 );
285 }
286
287 #[test]
288 fn reasoning_comes_before_the_decision() {
289 let juror = json!({
290 "verdict": "overturn",
291 "rationale": "r",
292 "juror_number": 3,
293 "context_analysis": "c",
294 "precedents_cited": [],
295 "constitutional_refs": [],
296 });
297 assert_eq!(
298 keys(&juror),
299 [
300 "juror_number",
301 "context_analysis",
302 "rationale",
303 "constitutional_refs",
304 "precedents_cited",
305 "verdict",
306 ]
307 );
308 let seat = json!({
311 "vote": "yes",
312 "role": "lawyer",
313 "position": "p",
314 "raw_text": "r",
315 "questions": [],
316 "rationale": "r",
317 "ready_to_vote": true,
318 });
319 assert_eq!(
320 keys(&seat),
321 [
322 "role",
323 "rationale",
324 "position",
325 "questions",
326 "ready_to_vote",
327 "vote",
328 "raw_text",
329 ]
330 );
331 }
332
333 #[test]
334 fn unknown_keys_are_kept_before_format_markers() {
335 let v =
336 json!({ "agora_x": 1, "_blind": "b", "zzz_new": 1, "title": "t" });
337 assert_eq!(keys(&v), ["title", "zzz_new", "agora_x", "_blind"]);
338 }
339
340 #[test]
341 fn labels_read_as_words() {
342 assert_eq!(label("appeal_statement"), "Appeal statement");
343 assert_eq!(label("raw_text"), "Raw model output");
344 assert_eq!(label("_blind"), "Redaction blind");
345 assert_eq!(
346 label("agora_governance_amendment"),
347 "Format agora_governance_amendment"
348 );
349 }
350
351 #[test]
352 fn items_are_titled_by_what_they_are() {
353 let round = json!({"number": 2, "round_type": "deliberation"});
354 assert_eq!(
355 item_title(Some("rounds"), 1, &round),
356 "Round 2 — deliberation"
357 );
358 let seat = json!({"role": "lawyer", "vote": "yes"});
359 assert_eq!(item_title(Some("responses"), 0, &seat), "Lawyer — yes");
360 let juror = json!({"verdict": "overturn"});
361 assert_eq!(
362 item_title(Some("jury_verdicts"), 2, &juror),
363 "Juror 3 — overturn"
364 );
365 assert_eq!(
366 item_title(Some("signatures"), 0, &json!({})),
367 "Signature 1"
368 );
369 assert_eq!(item_title(None, 0, &json!({})), "Item 1");
370 }
371
372 #[test]
373 fn only_an_identical_raw_text_repeats_the_rationale() {
374 let same = json!({"rationale": "r", "raw_text": "r"});
375 let differs = json!({"rationale": "r", "raw_text": "r!"});
376 assert!(repeats_rationale(same.as_object().unwrap(), "raw_text"));
377 assert!(!repeats_rationale(same.as_object().unwrap(), "rationale"));
378 assert!(!repeats_rationale(differs.as_object().unwrap(), "raw_text"));
379 }
380}