Skip to main content

chappi_core/
resume.rs

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}