Skip to main content

chappi_core/
projection.rs

1use std::fmt::Write as _;
2
3use serde::Serialize;
4
5use crate::resume::{
6    affect_text, attention_text, canonical_form, confidence_text, constraint_text, desire_text,
7    goal_status, hash_hex, lifetime_text, metacognition_text, prediction_text, question_text,
8    tension_text,
9};
10use crate::{
11    CognitiveState, FocusCue, FocusFrame, Node, NodeKind, ReentryCapsule, Relation, Source,
12};
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum Language {
16    Ja,
17    En,
18}
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum ProjectionFormat {
22    Text(Language),
23    Json,
24}
25
26pub fn project(state: &CognitiveState, format: ProjectionFormat) -> String {
27    let mut nodes = state.nodes.iter().collect::<Vec<_>>();
28    nodes.sort_by(|left, right| left.id.cmp(&right.id));
29    let relations = sorted_relations(&state.relations);
30    let mut focus = state
31        .focus_streams
32        .iter()
33        .flat_map(|stream| stream.frames.iter().map(FocusProjection::from_frame))
34        .collect::<Vec<_>>();
35    focus.sort_by(|left, right| {
36        left.stream_id
37            .cmp(&right.stream_id)
38            .then_with(|| left.sequence.cmp(&right.sequence))
39    });
40    render(
41        hash_hex(crate::state_hash(state)),
42        nodes,
43        relations,
44        focus,
45        format,
46    )
47}
48
49pub fn project_capsule(capsule: &ReentryCapsule, format: ProjectionFormat) -> String {
50    let mut nodes = capsule.nodes.iter().collect::<Vec<_>>();
51    nodes.sort_by(|left, right| left.id.cmp(&right.id));
52    let relations = sorted_relations(&capsule.relations);
53    let mut focus = capsule
54        .focus_cues
55        .iter()
56        .map(FocusProjection::from_cue)
57        .collect::<Vec<_>>();
58    focus.sort_by(|left, right| {
59        left.stream_id
60            .cmp(&right.stream_id)
61            .then_with(|| left.sequence.cmp(&right.sequence))
62    });
63    render(
64        hash_hex(capsule.source_state_hash),
65        nodes,
66        relations,
67        focus,
68        format,
69    )
70}
71
72fn render(
73    state_hash: String,
74    nodes: Vec<&Node>,
75    relations: Vec<&Relation>,
76    focus: Vec<FocusProjection>,
77    format: ProjectionFormat,
78) -> String {
79    match format {
80        ProjectionFormat::Json => render_json(state_hash, nodes, relations, focus),
81        ProjectionFormat::Text(language) => {
82            render_text(&state_hash, &nodes, &relations, &focus, language)
83        }
84    }
85}
86
87#[derive(Serialize)]
88struct Projection<'a> {
89    schema: &'static str,
90    version: u32,
91    state_hash: String,
92    nodes: Vec<NodeProjection<'a>>,
93    relations: Vec<RelationProjection<'a>>,
94    focus: Vec<FocusProjection>,
95}
96
97#[derive(Serialize)]
98struct NodeProjection<'a> {
99    id: &'a str,
100    revision: u64,
101    owner: &'a str,
102    lifetime: &'static str,
103    salience: u8,
104    #[serde(flatten)]
105    detail: NodeDetail<'a>,
106    payload: String,
107}
108
109#[derive(Serialize)]
110#[serde(tag = "kind")]
111enum NodeDetail<'a> {
112    #[serde(rename = "entity")]
113    Entity { entity_kind: &'a str },
114    #[serde(rename = "claim")]
115    Claim { confidence: &'static str },
116    #[serde(rename = "evidence")]
117    Evidence {
118        evidence_kind: &'a str,
119        source_type: &'static str,
120        source: &'a str,
121    },
122    #[serde(rename = "goal")]
123    Goal { status: &'static str },
124    #[serde(rename = "constraint")]
125    Constraint { strength: &'static str },
126    #[serde(rename = "question")]
127    Question { status: &'static str },
128    #[serde(rename = "relationship")]
129    Relationship { target: &'a str, status: &'a str },
130    #[serde(rename = "desire")]
131    Desire { force: &'static str },
132    #[serde(rename = "attention")]
133    Attention { mode: &'static str },
134    #[serde(rename = "association")]
135    Association,
136    #[serde(rename = "tension")]
137    Tension { status: &'static str },
138    #[serde(rename = "prediction")]
139    Prediction {
140        confidence: &'static str,
141        status: &'static str,
142    },
143    #[serde(rename = "perspective")]
144    Perspective {
145        target: &'a str,
146        confidence: &'static str,
147    },
148    #[serde(rename = "affect")]
149    Affect {
150        target: &'a str,
151        affect_kind: &'a str,
152        intensity: &'static str,
153    },
154    #[serde(rename = "metacognition")]
155    Metacognition { status: &'static str },
156}
157
158#[derive(Serialize)]
159struct RelationProjection<'a> {
160    kind: &'static str,
161    source: &'a str,
162    target: &'a str,
163}
164
165#[derive(Clone, Serialize)]
166struct FocusProjection {
167    stream_id: String,
168    sequence: u64,
169    owner: String,
170    transition: &'static str,
171    previous_sequence: Option<u64>,
172    foreground: Vec<String>,
173    background: Vec<String>,
174    residue: Vec<String>,
175    trigger: String,
176}
177
178impl<'a> From<&'a Node> for NodeProjection<'a> {
179    fn from(node: &'a Node) -> Self {
180        Self {
181            id: &node.id,
182            revision: node.revision,
183            owner: &node.owner,
184            lifetime: lifetime_text(node.lifetime),
185            salience: node.salience,
186            detail: NodeDetail::from(&node.kind),
187            payload: canonical_form(&node.payload),
188        }
189    }
190}
191
192impl<'a> From<&'a NodeKind> for NodeDetail<'a> {
193    fn from(kind: &'a NodeKind) -> Self {
194        match kind {
195            NodeKind::Entity { entity_kind } => Self::Entity { entity_kind },
196            NodeKind::Claim { confidence } => Self::Claim {
197                confidence: confidence_text(*confidence),
198            },
199            NodeKind::Evidence {
200                evidence_kind,
201                source,
202            } => Self::Evidence {
203                evidence_kind,
204                source_type: match source {
205                    Source::Symbol(_) => "symbol",
206                    Source::String(_) => "string",
207                },
208                source: match source {
209                    Source::Symbol(value) | Source::String(value) => value,
210                },
211            },
212            NodeKind::Goal { status } => Self::Goal {
213                status: goal_status(*status),
214            },
215            NodeKind::Constraint { strength } => Self::Constraint {
216                strength: constraint_text(*strength),
217            },
218            NodeKind::Question { status } => Self::Question {
219                status: question_text(*status),
220            },
221            NodeKind::Relationship { target, status } => Self::Relationship { target, status },
222            NodeKind::Desire { force } => Self::Desire {
223                force: desire_text(*force),
224            },
225            NodeKind::Attention { mode } => Self::Attention {
226                mode: attention_text(*mode),
227            },
228            NodeKind::Association => Self::Association,
229            NodeKind::Tension { status } => Self::Tension {
230                status: tension_text(*status),
231            },
232            NodeKind::Prediction { confidence, status } => Self::Prediction {
233                confidence: confidence_text(*confidence),
234                status: prediction_text(*status),
235            },
236            NodeKind::Perspective(value) => Self::Perspective {
237                target: &value.target,
238                confidence: confidence_text(value.confidence),
239            },
240            NodeKind::Affect {
241                target,
242                affect_kind,
243                intensity,
244            } => Self::Affect {
245                target,
246                affect_kind,
247                intensity: affect_text(*intensity),
248            },
249            NodeKind::Metacognition { status } => Self::Metacognition {
250                status: metacognition_text(*status),
251            },
252        }
253    }
254}
255
256impl FocusProjection {
257    fn from_frame(frame: &FocusFrame) -> Self {
258        Self {
259            stream_id: frame.stream_id.clone(),
260            sequence: frame.sequence,
261            owner: frame.owner.clone(),
262            transition: frame.transition.as_str(),
263            previous_sequence: frame.previous_sequence,
264            foreground: sorted(&frame.foreground),
265            background: sorted(&frame.background),
266            residue: sorted(&frame.residue),
267            trigger: canonical_form(&frame.trigger),
268        }
269    }
270
271    fn from_cue(cue: &FocusCue) -> Self {
272        Self {
273            stream_id: cue.stream_id.clone(),
274            sequence: cue.sequence,
275            owner: cue.owner.clone(),
276            transition: cue.transition.as_str(),
277            previous_sequence: None,
278            foreground: sorted(&cue.foreground),
279            background: sorted(&cue.background),
280            residue: sorted(&cue.residue),
281            trigger: cue.trigger.clone(),
282        }
283    }
284}
285
286fn render_json(
287    state_hash: String,
288    nodes: Vec<&Node>,
289    relations: Vec<&Relation>,
290    focus: Vec<FocusProjection>,
291) -> String {
292    serde_json::to_string(&Projection {
293        schema: "chappi.projection",
294        version: 1,
295        state_hash,
296        nodes: nodes.into_iter().map(NodeProjection::from).collect(),
297        relations: relations
298            .into_iter()
299            .map(|relation| RelationProjection {
300                kind: relation.kind.as_str(),
301                source: &relation.source,
302                target: &relation.target,
303            })
304            .collect(),
305        focus,
306    })
307    .expect("projection DTOs always serialize")
308}
309
310fn render_text(
311    state_hash: &str,
312    nodes: &[&Node],
313    relations: &[&Relation],
314    focus: &[FocusProjection],
315    language: Language,
316) -> String {
317    let mut output = String::new();
318    match language {
319        Language::En => writeln!(output, "Chappi projection v1 state_hash={state_hash}"),
320        Language::Ja => writeln!(output, "Chappi 投影 v1 状態ハッシュ={state_hash}"),
321    }
322    .expect("writing to String cannot fail");
323    for node in nodes {
324        write_node_text(&mut output, node, language);
325    }
326    for relation in relations {
327        match language {
328            Language::En => writeln!(
329                output,
330                "relation kind={} source={} target={}",
331                relation.kind.as_str(),
332                relation.source,
333                relation.target
334            ),
335            Language::Ja => writeln!(
336                output,
337                "関係 種別={} 起点={} 終点={}",
338                relation.kind.as_str(),
339                relation.source,
340                relation.target
341            ),
342        }
343        .expect("writing to String cannot fail");
344    }
345    for cue in focus {
346        match language {
347            Language::En => writeln!(
348                output,
349                "focus stream={} sequence={} owner={} transition={} previous={} foreground={} background={} residue={} trigger={}",
350                cue.stream_id,
351                cue.sequence,
352                cue.owner,
353                cue.transition,
354                optional_sequence(cue.previous_sequence),
355                cue.foreground.join(","),
356                cue.background.join(","),
357                cue.residue.join(","),
358                cue.trigger
359            ),
360            Language::Ja => writeln!(
361                output,
362                "焦点 ストリーム={} 連番={} 所有者={} 遷移={} 前連番={} 前景={} 背景={} 残留={} 契機={}",
363                cue.stream_id,
364                cue.sequence,
365                cue.owner,
366                cue.transition,
367                optional_sequence(cue.previous_sequence),
368                cue.foreground.join(","),
369                cue.background.join(","),
370                cue.residue.join(","),
371                cue.trigger
372            ),
373        }
374        .expect("writing to String cannot fail");
375    }
376    output
377}
378
379fn write_node_text(output: &mut String, node: &Node, language: Language) {
380    let detail = text_detail(&node.kind, language);
381    match language {
382        Language::En => writeln!(
383            output,
384            "node id={} revision={} owner={} lifetime={} salience={} {detail} payload={}",
385            node.id,
386            node.revision,
387            node.owner,
388            lifetime_text(node.lifetime),
389            node.salience,
390            canonical_form(&node.payload)
391        ),
392        Language::Ja => writeln!(
393            output,
394            "ノード id={} リビジョン={} 所有者={} 生存期間={} 顕著度={} {detail} ペイロード={}",
395            node.id,
396            node.revision,
397            node.owner,
398            lifetime_text(node.lifetime),
399            node.salience,
400            canonical_form(&node.payload)
401        ),
402    }
403    .expect("writing to String cannot fail");
404}
405
406fn text_detail(kind: &NodeKind, language: Language) -> String {
407    let label = |english: &'static str, japanese: &'static str| match language {
408        Language::En => english,
409        Language::Ja => japanese,
410    };
411    match kind {
412        NodeKind::Entity { entity_kind } => format!(
413            "{}=entity {}={entity_kind}",
414            label("kind", "種別"),
415            label("entity_kind", "実体種別")
416        ),
417        NodeKind::Claim { confidence } => format!(
418            "{}=claim {}={}",
419            label("kind", "種別"),
420            label("confidence", "確信度"),
421            confidence_text(*confidence)
422        ),
423        NodeKind::Evidence {
424            evidence_kind,
425            source,
426        } => format!(
427            "{}=evidence {}={} {}={}",
428            label("kind", "種別"),
429            label("evidence_kind", "証拠種別"),
430            evidence_kind,
431            label("source", "情報源"),
432            source_text(source)
433        ),
434        NodeKind::Goal { status } => format!(
435            "{}=goal {}={}",
436            label("kind", "種別"),
437            label("status", "状態"),
438            goal_status(*status)
439        ),
440        NodeKind::Constraint { strength } => format!(
441            "{}=constraint {}={}",
442            label("kind", "種別"),
443            label("strength", "強度"),
444            constraint_text(*strength)
445        ),
446        NodeKind::Question { status } => format!(
447            "{}=question {}={}",
448            label("kind", "種別"),
449            label("status", "状態"),
450            question_text(*status)
451        ),
452        NodeKind::Relationship { target, status } => format!(
453            "{}=relationship {}={} {}={}",
454            label("kind", "種別"),
455            label("target", "対象"),
456            target,
457            label("status", "状態"),
458            status
459        ),
460        NodeKind::Desire { force } => format!(
461            "{}=desire {}={}",
462            label("kind", "種別"),
463            label("force", "力"),
464            desire_text(*force)
465        ),
466        NodeKind::Attention { mode } => format!(
467            "{}=attention {}={}",
468            label("kind", "種別"),
469            label("mode", "モード"),
470            attention_text(*mode)
471        ),
472        NodeKind::Association => format!("{}=association", label("kind", "種別")),
473        NodeKind::Tension { status } => format!(
474            "{}=tension {}={}",
475            label("kind", "種別"),
476            label("status", "状態"),
477            tension_text(*status)
478        ),
479        NodeKind::Prediction { confidence, status } => format!(
480            "{}=prediction {}={} {}={}",
481            label("kind", "種別"),
482            label("confidence", "確信度"),
483            confidence_text(*confidence),
484            label("status", "状態"),
485            prediction_text(*status)
486        ),
487        NodeKind::Perspective(value) => format!(
488            "{}=perspective {}={} {}={}",
489            label("kind", "種別"),
490            label("perspective_target", "視点対象"),
491            value.target,
492            label("confidence", "確信度"),
493            confidence_text(value.confidence)
494        ),
495        NodeKind::Affect {
496            target,
497            affect_kind,
498            intensity,
499        } => format!(
500            "{}=affect {}={} {}={} {}={}",
501            label("kind", "種別"),
502            label("target", "対象"),
503            target,
504            label("affect_kind", "感情種別"),
505            affect_kind,
506            label("intensity", "強さ"),
507            affect_text(*intensity)
508        ),
509        NodeKind::Metacognition { status } => format!(
510            "{}=metacognition {}={}",
511            label("kind", "種別"),
512            label("status", "状態"),
513            metacognition_text(*status)
514        ),
515    }
516}
517
518fn source_text(source: &Source) -> String {
519    match source {
520        Source::Symbol(value) => value.clone(),
521        Source::String(value) => {
522            serde_json::to_string(value).expect("serializing a string cannot fail")
523        }
524    }
525}
526
527fn sorted(values: &[String]) -> Vec<String> {
528    let mut values = values.to_vec();
529    values.sort();
530    values
531}
532
533fn sorted_relations(relations: &[Relation]) -> Vec<&Relation> {
534    let mut relations = relations.iter().collect::<Vec<_>>();
535    relations.sort_by(|left, right| {
536        left.kind
537            .as_str()
538            .cmp(right.kind.as_str())
539            .then_with(|| left.source.cmp(&right.source))
540            .then_with(|| left.target.cmp(&right.target))
541    });
542    relations
543}
544
545fn optional_sequence(value: Option<u64>) -> String {
546    value.map_or_else(|| "none".to_owned(), |value| value.to_string())
547}