1use std::cmp::Ordering;
2use std::collections::{HashMap, HashSet};
3use std::fmt::Write as _;
4
5use chappi_syntax::{Document, Form, canonicalize};
6use unicode_normalization::UnicodeNormalization;
7
8use crate::{
9 CognitiveState, Confidence, ConstraintStrength, FocusFrame, GoalStatus, Node, NodeKind,
10 QuestionStatus, Relation, RelationKind, TensionStatus, TransitionKind, state_hash,
11};
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub struct CapsuleBudget {
15 pub limit: usize,
16 pub unit: BudgetUnit,
17}
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum BudgetUnit {
21 Bytes,
22 Tokens,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq)]
26pub struct ReentryCapsule {
27 pub schema_version: u32,
28 pub source_state_hash: [u8; 32],
29 pub nodes: Vec<Node>,
30 pub relations: Vec<Relation>,
31 pub focus_cues: Vec<FocusCue>,
32 pub omitted_node_ids: Vec<String>,
33 pub used: usize,
34 pub requested_limit: usize,
35 pub requested_unit: BudgetUnit,
36 pub over_budget: bool,
37}
38
39#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct FocusCue {
41 pub stream_id: String,
42 pub sequence: u64,
43 pub owner: String,
44 pub transition: TransitionKind,
45 pub foreground: Vec<String>,
46 pub background: Vec<String>,
47 pub residue: Vec<String>,
48 pub trigger: String,
49}
50
51pub trait TokenEstimator {
52 fn estimate(&self, canonical: &str) -> usize;
53}
54
55#[derive(Clone, Copy, Debug, Default)]
56pub struct LexicalTokenEstimator;
57
58impl TokenEstimator for LexicalTokenEstimator {
59 fn estimate(&self, canonical: &str) -> usize {
60 let mut count = 0;
61 let mut chars = canonical.chars().peekable();
62 while let Some(character) = chars.next() {
63 match character {
64 '(' | ')' => count += 1,
65 '"' => {
66 count += 1;
67 let mut escaped = false;
68 for next in chars.by_ref() {
69 if escaped {
70 escaped = false;
71 } else if next == '\\' {
72 escaped = true;
73 } else if next == '"' {
74 break;
75 }
76 }
77 }
78 character if character.is_whitespace() => {}
79 _ => {
80 count += 1;
81 while chars
82 .peek()
83 .is_some_and(|next| !next.is_whitespace() && !matches!(next, '(' | ')'))
84 {
85 chars.next();
86 }
87 }
88 }
89 }
90 count
91 }
92}
93
94pub fn build_capsule(state: &CognitiveState, budget: CapsuleBudget) -> ReentryCapsule {
95 build_capsule_with_estimator(state, budget, &LexicalTokenEstimator)
96}
97
98pub fn build_capsule_with_estimator(
99 state: &CognitiveState,
100 budget: CapsuleBudget,
101 estimator: &dyn TokenEstimator,
102) -> ReentryCapsule {
103 let nodes_by_id = state
104 .nodes
105 .iter()
106 .map(|node| (node.id.as_str(), node))
107 .collect::<HashMap<_, _>>();
108 let focus_rank = focus_rank(state);
109 let mandatory = state
110 .nodes
111 .iter()
112 .filter(|node| is_mandatory(node))
113 .map(|node| node.id.as_str())
114 .collect::<Vec<_>>();
115 let mut selected = HashSet::new();
116 for id in mandatory {
117 add_node_closure(id, &nodes_by_id, &mut selected);
118 }
119
120 loop {
121 let mut candidates = state
122 .nodes
123 .iter()
124 .filter(|node| !selected.contains(node.id.as_str()))
125 .collect::<Vec<_>>();
126 candidates.sort_by(|left, right| compare_nodes(left, right, &focus_rank, state, &selected));
127
128 let mut accepted = false;
129 for candidate in candidates {
130 let mut expanded = selected.clone();
131 add_node_closure(&candidate.id, &nodes_by_id, &mut expanded);
132 let used = measure_selection(state, &expanded, budget.unit, estimator, &focus_rank);
133 if used <= budget.limit {
134 selected = expanded;
135 accepted = true;
136 break;
137 }
138 }
139 if !accepted {
140 break;
141 }
142 }
143
144 assemble_capsule(state, selected, budget, estimator, &focus_rank)
145}
146
147impl ReentryCapsule {
148 pub fn canonical_content(&self) -> String {
149 canonical_content(&self.nodes, &self.relations, &self.focus_cues)
150 }
151}
152
153fn assemble_capsule(
154 state: &CognitiveState,
155 selected: HashSet<&str>,
156 budget: CapsuleBudget,
157 estimator: &dyn TokenEstimator,
158 focus_rank: &HashMap<&str, (u8, u64, String)>,
159) -> ReentryCapsule {
160 let mut nodes = state
161 .nodes
162 .iter()
163 .filter(|node| selected.contains(node.id.as_str()))
164 .cloned()
165 .collect::<Vec<_>>();
166 nodes.sort_by(|left, right| compare_nodes(left, right, focus_rank, state, &selected));
167
168 let relations = selected_relations(state, &selected);
169 let focus_cues = selected_focus_cues(state, &selected);
170 let canonical = canonical_content(&nodes, &relations, &focus_cues);
171 let used = measure(&canonical, budget.unit, estimator);
172 let mut omitted_node_ids = state
173 .nodes
174 .iter()
175 .filter(|node| !selected.contains(node.id.as_str()))
176 .map(|node| node.id.clone())
177 .collect::<Vec<_>>();
178 omitted_node_ids.sort();
179
180 ReentryCapsule {
181 schema_version: 1,
182 source_state_hash: state_hash(state),
183 nodes,
184 relations,
185 focus_cues,
186 omitted_node_ids,
187 used,
188 requested_limit: budget.limit,
189 requested_unit: budget.unit,
190 over_budget: used > budget.limit,
191 }
192}
193
194fn is_mandatory(node: &Node) -> bool {
195 matches!(node.kind, NodeKind::Constraint { .. })
196 || matches!(
197 node.kind,
198 NodeKind::Goal {
199 status: GoalStatus::Committed
200 }
201 )
202}
203
204fn add_node_closure<'a>(
205 id: &'a str,
206 nodes_by_id: &HashMap<&'a str, &'a Node>,
207 selected: &mut HashSet<&'a str>,
208) {
209 if !selected.insert(id) {
210 return;
211 }
212 let Some(node) = nodes_by_id.get(id) else {
213 return;
214 };
215 add_node_closure(&node.owner, nodes_by_id, selected);
216 match &node.kind {
217 NodeKind::Relationship { target, .. }
218 | NodeKind::Affect { target, .. }
219 | NodeKind::Perspective(crate::Perspective { target, .. }) => {
220 add_node_closure(target, nodes_by_id, selected);
221 }
222 _ => {}
223 }
224}
225
226fn focus_rank(state: &CognitiveState) -> HashMap<&str, (u8, u64, String)> {
227 let mut ranks = HashMap::new();
228 for stream in &state.focus_streams {
229 let Some(frame) = stream.frames.iter().max_by_key(|frame| frame.sequence) else {
230 continue;
231 };
232 if frame.transition == TransitionKind::Resolved {
233 continue;
234 }
235 for (bucket, ids) in [(0, &frame.foreground), (1, &frame.residue)] {
236 for id in ids {
237 let rank = (bucket, u64::MAX - frame.sequence, stream.id.clone());
238 ranks
239 .entry(id.as_str())
240 .and_modify(|old| {
241 if rank < *old {
242 *old = rank.clone();
243 }
244 })
245 .or_insert(rank);
246 }
247 }
248 }
249 ranks
250}
251
252fn compare_nodes(
253 left: &Node,
254 right: &Node,
255 focus_rank: &HashMap<&str, (u8, u64, String)>,
256 state: &CognitiveState,
257 selected: &HashSet<&str>,
258) -> Ordering {
259 node_rank(left, focus_rank, state, selected)
260 .cmp(&node_rank(right, focus_rank, state, selected))
261 .then_with(|| left.id.cmp(&right.id))
262}
263
264fn node_rank(
265 node: &Node,
266 focus_rank: &HashMap<&str, (u8, u64, String)>,
267 state: &CognitiveState,
268 selected: &HashSet<&str>,
269) -> (u8, u8, u8, u64, String) {
270 let stable = node.id.clone();
271 match &node.kind {
272 NodeKind::Constraint {
273 strength: ConstraintStrength::Hard,
274 } => (0, 0, 0, 0, stable),
275 NodeKind::Constraint {
276 strength: ConstraintStrength::Soft,
277 } => (1, 0, 0, 0, stable),
278 NodeKind::Goal {
279 status: GoalStatus::Committed,
280 } => (2, 0, 0, 0, stable),
281 NodeKind::Tension {
282 status: TensionStatus::Open,
283 } => (3, 0, 0, 0, stable),
284 NodeKind::Question {
285 status: QuestionStatus::Open,
286 } => (4, 0, 0, 0, stable),
287 _ if focus_rank.contains_key(node.id.as_str()) => {
288 let (bucket, inverse_sequence, stream) = &focus_rank[node.id.as_str()];
289 (
290 5,
291 *bucket,
292 0,
293 *inverse_sequence,
294 format!("{stream}\0{stable}"),
295 )
296 }
297 NodeKind::Relationship { .. } => (6, 0, 0, u64::MAX - node.salience as u64, stable),
298 NodeKind::Affect { .. } => (7, 0, 0, u64::MAX - node.salience as u64, stable),
299 NodeKind::Evidence { .. } if supports_selected(node, state, selected) => {
300 (8, 0, 0, u64::MAX - node.salience as u64, stable)
301 }
302 NodeKind::Claim { confidence } => (
303 9,
304 confidence_rank(*confidence),
305 0,
306 u64::MAX - node.salience as u64,
307 stable,
308 ),
309 _ => (10, 0, 0, u64::MAX - node.salience as u64, stable),
310 }
311}
312
313fn confidence_rank(confidence: Confidence) -> u8 {
314 match confidence {
315 Confidence::Settled => 0,
316 Confidence::Likely => 1,
317 Confidence::Plausible => 2,
318 Confidence::Tentative => 3,
319 }
320}
321
322fn supports_selected(node: &Node, state: &CognitiveState, selected: &HashSet<&str>) -> bool {
323 state.relations.iter().any(|relation| {
324 relation.kind == RelationKind::Supports
325 && relation.source == node.id
326 && selected.contains(relation.target.as_str())
327 && state.nodes.iter().any(|target| {
328 target.id == relation.target
329 && matches!(
330 target.kind,
331 NodeKind::Claim { .. } | NodeKind::Prediction { .. }
332 )
333 })
334 })
335}
336
337fn measure_selection(
338 state: &CognitiveState,
339 selected: &HashSet<&str>,
340 unit: BudgetUnit,
341 estimator: &dyn TokenEstimator,
342 focus_rank: &HashMap<&str, (u8, u64, String)>,
343) -> usize {
344 let mut nodes = state
345 .nodes
346 .iter()
347 .filter(|node| selected.contains(node.id.as_str()))
348 .cloned()
349 .collect::<Vec<_>>();
350 nodes.sort_by(|left, right| compare_nodes(left, right, focus_rank, state, selected));
351 let relations = selected_relations(state, selected);
352 let focus = selected_focus_cues(state, selected);
353 measure(
354 &canonical_content(&nodes, &relations, &focus),
355 unit,
356 estimator,
357 )
358}
359
360fn measure(canonical: &str, unit: BudgetUnit, estimator: &dyn TokenEstimator) -> usize {
361 match unit {
362 BudgetUnit::Bytes => canonical.len(),
363 BudgetUnit::Tokens => estimator.estimate(canonical),
364 }
365}
366
367fn selected_relations(state: &CognitiveState, selected: &HashSet<&str>) -> Vec<Relation> {
368 let mut relations = state
369 .relations
370 .iter()
371 .filter(|relation| {
372 selected.contains(relation.source.as_str())
373 && selected.contains(relation.target.as_str())
374 })
375 .cloned()
376 .collect::<Vec<_>>();
377 relations.sort_by(|left, right| {
378 left.kind
379 .as_str()
380 .cmp(right.kind.as_str())
381 .then_with(|| left.source.cmp(&right.source))
382 .then_with(|| left.target.cmp(&right.target))
383 });
384 relations
385}
386
387fn selected_focus_cues(state: &CognitiveState, selected: &HashSet<&str>) -> Vec<FocusCue> {
388 let mut cues = state
389 .focus_streams
390 .iter()
391 .filter_map(|stream| stream.frames.iter().max_by_key(|frame| frame.sequence))
392 .filter(|frame| frame.transition != TransitionKind::Resolved)
393 .filter(|frame| focus_is_closed(frame, selected))
394 .map(|frame| FocusCue {
395 stream_id: frame.stream_id.clone(),
396 sequence: frame.sequence,
397 owner: frame.owner.clone(),
398 transition: frame.transition,
399 foreground: sorted_strings(&frame.foreground),
400 background: sorted_strings(&frame.background),
401 residue: sorted_strings(&frame.residue),
402 trigger: canonical_form(&frame.trigger),
403 })
404 .collect::<Vec<_>>();
405 cues.sort_by(|left, right| {
406 left.stream_id
407 .cmp(&right.stream_id)
408 .then_with(|| left.sequence.cmp(&right.sequence))
409 });
410 cues
411}
412
413fn focus_is_closed(frame: &FocusFrame, selected: &HashSet<&str>) -> bool {
414 selected.contains(frame.owner.as_str())
415 && frame
416 .foreground
417 .iter()
418 .chain(&frame.background)
419 .chain(&frame.residue)
420 .all(|id| selected.contains(id.as_str()))
421}
422
423fn sorted_strings(values: &[String]) -> Vec<String> {
424 let mut values = values.to_vec();
425 values.sort();
426 values
427}
428
429fn canonical_content(nodes: &[Node], relations: &[Relation], focus: &[FocusCue]) -> String {
430 let mut output = String::new();
431 for node in nodes {
432 write_node(&mut output, node);
433 }
434 for relation in relations {
435 write!(
436 output,
437 "(R{}:{}>{})",
438 short_relation(relation.kind),
439 atom(&relation.source),
440 atom(&relation.target)
441 )
442 .expect("writing to String cannot fail");
443 }
444 for cue in focus {
445 write_focus(&mut output, cue);
446 }
447 output
448}
449
450fn write_node(output: &mut String, node: &Node) {
451 write!(
452 output,
453 "({}~{}@{}/{}{}:",
454 record_leading_node_id(&node.id),
455 base36(node.revision),
456 atom(&node.owner),
457 short_lifetime(node.lifetime),
458 base36(node.salience.into())
459 )
460 .expect("writing to String cannot fail");
461 write_kind(output, &node.kind);
462 output.push(' ');
463 output.push_str(&canonical_form(&node.payload));
464 output.push(')');
465}
466
467fn write_kind(output: &mut String, kind: &NodeKind) {
468 match kind {
469 NodeKind::Entity { entity_kind } if entity_kind == "person" => write!(output, "ep"),
470 NodeKind::Entity { entity_kind } => write!(output, "e/{}", atom(entity_kind)),
471 NodeKind::Claim { confidence } => {
472 write!(output, "c{}", short_confidence(*confidence))
473 }
474 NodeKind::Evidence {
475 evidence_kind,
476 source,
477 } => write!(
478 output,
479 "v/{}/{}/{}",
480 atom(evidence_kind),
481 match source {
482 crate::Source::Symbol(_) => "s",
483 crate::Source::String(_) => "q",
484 },
485 atom(match source {
486 crate::Source::Symbol(value) | crate::Source::String(value) => value,
487 })
488 ),
489 NodeKind::Goal { status } => write!(output, "g{}", short_goal(*status)),
490 NodeKind::Constraint { strength } => {
491 write!(output, "k{}", short_constraint(*strength))
492 }
493 NodeKind::Question { status } => write!(output, "q{}", short_question(*status)),
494 NodeKind::Relationship { target, status } => {
495 write!(output, "r/{}/{}", atom(target), atom(status))
496 }
497 NodeKind::Desire { force } => write!(output, "d{}", short_desire(*force)),
498 NodeKind::Attention { mode } => write!(output, "a{}", short_attention(*mode)),
499 NodeKind::Association => write!(output, "s"),
500 NodeKind::Tension { status } => write!(output, "t{}", short_tension(*status)),
501 NodeKind::Prediction { confidence, status } => write!(
502 output,
503 "p{}{}",
504 short_confidence(*confidence),
505 short_prediction(*status)
506 ),
507 NodeKind::Perspective(value) => write!(
508 output,
509 "w/{}/{}",
510 atom(&value.target),
511 short_confidence(value.confidence)
512 ),
513 NodeKind::Affect {
514 target,
515 affect_kind,
516 intensity,
517 } => write!(
518 output,
519 "f/{}/{}/{}",
520 atom(target),
521 atom(affect_kind),
522 short_affect(*intensity)
523 ),
524 NodeKind::Metacognition { status } => {
525 write!(output, "m{}", short_metacognition(*status))
526 }
527 }
528 .expect("writing to String cannot fail");
529}
530
531fn write_focus(output: &mut String, cue: &FocusCue) {
532 write!(
533 output,
534 "(!{}~{}@{}:{}",
535 atom(&cue.stream_id),
536 base36(cue.sequence),
537 atom(&cue.owner),
538 short_transition(cue.transition)
539 )
540 .expect("writing to String cannot fail");
541 output.push('/');
542 write_ids(output, "f", &cue.foreground);
543 output.push('/');
544 write_ids(output, "b", &cue.background);
545 output.push('/');
546 write_ids(output, "r", &cue.residue);
547 output.push('#');
548 output.push_str(&cue.trigger);
549 output.push(')');
550}
551
552fn write_ids(output: &mut String, head: &str, ids: &[String]) {
553 output.push_str(head);
554 output.push('[');
555 for (index, id) in ids.iter().enumerate() {
556 if index > 0 {
557 output.push(',');
558 }
559 output.push_str(&atom(id));
560 }
561 output.push(']');
562}
563
564fn base36(mut value: u64) -> String {
565 if value == 0 {
566 return "0".into();
567 }
568 let mut digits = Vec::new();
569 while value > 0 {
570 let digit = (value % 36) as u8;
571 digits.push(if digit < 10 {
572 (b'0' + digit) as char
573 } else {
574 (b'a' + digit - 10) as char
575 });
576 value /= 36;
577 }
578 digits.iter().rev().collect()
579}
580
581fn short_relation(value: crate::RelationKind) -> &'static str {
582 match value {
583 crate::RelationKind::Supports => "u",
584 crate::RelationKind::Contradicts => "x",
585 crate::RelationKind::Refines => "r",
586 crate::RelationKind::Causes => "c",
587 crate::RelationKind::Supersedes => "s",
588 crate::RelationKind::About => "a",
589 crate::RelationKind::Blocks => "b",
590 crate::RelationKind::Associates => "o",
591 }
592}
593
594fn short_transition(value: TransitionKind) -> &'static str {
595 match value {
596 TransitionKind::Evoked => "e",
597 TransitionKind::Associated => "a",
598 TransitionKind::Interrupted => "i",
599 TransitionKind::Returned => "r",
600 TransitionKind::Reframed => "f",
601 TransitionKind::Resolved => "s",
602 }
603}
604
605pub(crate) fn canonical_form(form: &Form) -> String {
606 let bytes = canonicalize(&Document {
607 forms: vec![form.clone()],
608 });
609 String::from_utf8(bytes)
610 .expect("canonical syntax is UTF-8")
611 .trim_end_matches('\n')
612 .to_owned()
613}
614
615pub(crate) fn hash_hex(hash: [u8; 32]) -> String {
616 let mut output = String::with_capacity(64);
617 for byte in hash {
618 write!(output, "{byte:02x}").expect("writing to String cannot fail");
619 }
620 output
621}
622
623fn atom(value: &str) -> String {
624 let value = value.nfc().collect::<String>();
625 if !value.is_empty()
626 && value.chars().all(|character| {
627 !character.is_control()
628 && !matches!(
629 character,
630 ' ' | '\t'
631 | '('
632 | ')'
633 | '"'
634 | ';'
635 | '~'
636 | '@'
637 | '/'
638 | ':'
639 | ','
640 | '['
641 | ']'
642 | '#'
643 | '>'
644 )
645 })
646 {
647 return value;
648 }
649 quoted_atom(&value)
650}
651
652fn record_leading_node_id(value: &str) -> String {
653 let value = value.nfc().collect::<String>();
654 if matches!(value.chars().next(), Some('R' | '!')) {
655 quoted_atom(&value)
656 } else {
657 atom(&value)
658 }
659}
660
661fn quoted_atom(value: &str) -> String {
662 let mut output = String::from("\"");
663 for character in value.chars() {
664 match character {
665 '"' => output.push_str("\\\""),
666 '\\' => output.push_str("\\\\"),
667 '\n' => output.push_str("\\n"),
668 '\r' => output.push_str("\\r"),
669 '\t' => output.push_str("\\t"),
670 character if character.is_control() => {
671 write!(output, "\\u{{{:x}}}", character as u32)
672 .expect("writing to String cannot fail");
673 }
674 character => output.push(character),
675 }
676 }
677 output.push('"');
678 output
679}
680
681pub(crate) fn lifetime_text(value: crate::Lifetime) -> &'static str {
682 match value {
683 crate::Lifetime::Persistent => "persistent",
684 crate::Lifetime::Working => "working",
685 crate::Lifetime::Transient => "transient",
686 }
687}
688
689fn short_lifetime(value: crate::Lifetime) -> &'static str {
690 match value {
691 crate::Lifetime::Persistent => "p",
692 crate::Lifetime::Working => "w",
693 crate::Lifetime::Transient => "t",
694 }
695}
696
697fn short_confidence(value: Confidence) -> &'static str {
698 match value {
699 Confidence::Tentative => "t",
700 Confidence::Plausible => "p",
701 Confidence::Likely => "l",
702 Confidence::Settled => "s",
703 }
704}
705
706fn short_goal(value: GoalStatus) -> &'static str {
707 match value {
708 GoalStatus::Considering => "n",
709 GoalStatus::Active => "a",
710 GoalStatus::Committed => "c",
711 GoalStatus::Done => "d",
712 GoalStatus::Dropped => "x",
713 }
714}
715
716fn short_constraint(value: ConstraintStrength) -> &'static str {
717 match value {
718 ConstraintStrength::Soft => "s",
719 ConstraintStrength::Hard => "h",
720 }
721}
722
723fn short_question(value: QuestionStatus) -> &'static str {
724 match value {
725 QuestionStatus::Open => "o",
726 QuestionStatus::Answered => "a",
727 QuestionStatus::Deferred => "d",
728 }
729}
730
731fn short_tension(value: TensionStatus) -> &'static str {
732 match value {
733 TensionStatus::Open => "o",
734 TensionStatus::Resolved => "r",
735 }
736}
737
738fn short_prediction(value: crate::PredictionStatus) -> &'static str {
739 match value {
740 crate::PredictionStatus::Pending => "p",
741 crate::PredictionStatus::Matched => "m",
742 crate::PredictionStatus::Surprised => "s",
743 }
744}
745
746fn short_desire(value: crate::DesireForce) -> &'static str {
747 match value {
748 crate::DesireForce::Weak => "w",
749 crate::DesireForce::Moderate => "m",
750 crate::DesireForce::Strong => "s",
751 }
752}
753
754fn short_attention(value: crate::AttentionMode) -> &'static str {
755 match value {
756 crate::AttentionMode::Foreground => "f",
757 crate::AttentionMode::Background => "b",
758 }
759}
760
761fn short_affect(value: crate::AffectIntensity) -> &'static str {
762 match value {
763 crate::AffectIntensity::Weak => "w",
764 crate::AffectIntensity::Moderate => "m",
765 crate::AffectIntensity::Strong => "s",
766 }
767}
768
769fn short_metacognition(value: crate::MetacognitionStatus) -> &'static str {
770 match value {
771 crate::MetacognitionStatus::Uncertain => "u",
772 crate::MetacognitionStatus::Monitoring => "m",
773 crate::MetacognitionStatus::Resolved => "r",
774 }
775}
776
777pub(crate) fn confidence_text(value: Confidence) -> &'static str {
778 match value {
779 Confidence::Tentative => "tentative",
780 Confidence::Plausible => "plausible",
781 Confidence::Likely => "likely",
782 Confidence::Settled => "settled",
783 }
784}
785
786pub(crate) fn goal_status(value: GoalStatus) -> &'static str {
787 match value {
788 GoalStatus::Considering => "considering",
789 GoalStatus::Active => "active",
790 GoalStatus::Committed => "committed",
791 GoalStatus::Done => "done",
792 GoalStatus::Dropped => "dropped",
793 }
794}
795
796pub(crate) fn constraint_text(value: ConstraintStrength) -> &'static str {
797 match value {
798 ConstraintStrength::Soft => "soft",
799 ConstraintStrength::Hard => "hard",
800 }
801}
802
803pub(crate) fn question_text(value: QuestionStatus) -> &'static str {
804 match value {
805 QuestionStatus::Open => "open",
806 QuestionStatus::Answered => "answered",
807 QuestionStatus::Deferred => "deferred",
808 }
809}
810
811pub(crate) fn tension_text(value: TensionStatus) -> &'static str {
812 match value {
813 TensionStatus::Open => "open",
814 TensionStatus::Resolved => "resolved",
815 }
816}
817
818pub(crate) fn prediction_text(value: crate::PredictionStatus) -> &'static str {
819 match value {
820 crate::PredictionStatus::Pending => "pending",
821 crate::PredictionStatus::Matched => "matched",
822 crate::PredictionStatus::Surprised => "surprised",
823 }
824}
825
826pub(crate) fn desire_text(value: crate::DesireForce) -> &'static str {
827 match value {
828 crate::DesireForce::Weak => "weak",
829 crate::DesireForce::Moderate => "moderate",
830 crate::DesireForce::Strong => "strong",
831 }
832}
833
834pub(crate) fn attention_text(value: crate::AttentionMode) -> &'static str {
835 match value {
836 crate::AttentionMode::Foreground => "foreground",
837 crate::AttentionMode::Background => "background",
838 }
839}
840
841pub(crate) fn affect_text(value: crate::AffectIntensity) -> &'static str {
842 match value {
843 crate::AffectIntensity::Weak => "weak",
844 crate::AffectIntensity::Moderate => "moderate",
845 crate::AffectIntensity::Strong => "strong",
846 }
847}
848
849pub(crate) fn metacognition_text(value: crate::MetacognitionStatus) -> &'static str {
850 match value {
851 crate::MetacognitionStatus::Uncertain => "uncertain",
852 crate::MetacognitionStatus::Monitoring => "monitoring",
853 crate::MetacognitionStatus::Resolved => "resolved",
854 }
855}