use std::fmt::Write as _;
use serde::Serialize;
use crate::resume::{
affect_text, attention_text, canonical_form, confidence_text, constraint_text, desire_text,
goal_status, hash_hex, lifetime_text, metacognition_text, prediction_text, question_text,
tension_text,
};
use crate::{
CognitiveState, FocusCue, FocusFrame, Node, NodeKind, ReentryCapsule, Relation, Source,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Language {
Ja,
En,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ProjectionFormat {
Text(Language),
Json,
}
pub fn project(state: &CognitiveState, format: ProjectionFormat) -> String {
let mut nodes = state.nodes.iter().collect::<Vec<_>>();
nodes.sort_by(|left, right| left.id.cmp(&right.id));
let relations = sorted_relations(&state.relations);
let mut focus = state
.focus_streams
.iter()
.flat_map(|stream| stream.frames.iter().map(FocusProjection::from_frame))
.collect::<Vec<_>>();
focus.sort_by(|left, right| {
left.stream_id
.cmp(&right.stream_id)
.then_with(|| left.sequence.cmp(&right.sequence))
});
render(
hash_hex(crate::state_hash(state)),
nodes,
relations,
focus,
format,
)
}
pub fn project_capsule(capsule: &ReentryCapsule, format: ProjectionFormat) -> String {
let mut nodes = capsule.nodes.iter().collect::<Vec<_>>();
nodes.sort_by(|left, right| left.id.cmp(&right.id));
let relations = sorted_relations(&capsule.relations);
let mut focus = capsule
.focus_cues
.iter()
.map(FocusProjection::from_cue)
.collect::<Vec<_>>();
focus.sort_by(|left, right| {
left.stream_id
.cmp(&right.stream_id)
.then_with(|| left.sequence.cmp(&right.sequence))
});
render(
hash_hex(capsule.source_state_hash),
nodes,
relations,
focus,
format,
)
}
fn render(
state_hash: String,
nodes: Vec<&Node>,
relations: Vec<&Relation>,
focus: Vec<FocusProjection>,
format: ProjectionFormat,
) -> String {
match format {
ProjectionFormat::Json => render_json(state_hash, nodes, relations, focus),
ProjectionFormat::Text(language) => {
render_text(&state_hash, &nodes, &relations, &focus, language)
}
}
}
#[derive(Serialize)]
struct Projection<'a> {
schema: &'static str,
version: u32,
state_hash: String,
nodes: Vec<NodeProjection<'a>>,
relations: Vec<RelationProjection<'a>>,
focus: Vec<FocusProjection>,
}
#[derive(Serialize)]
struct NodeProjection<'a> {
id: &'a str,
revision: u64,
owner: &'a str,
lifetime: &'static str,
salience: u8,
#[serde(flatten)]
detail: NodeDetail<'a>,
payload: String,
}
#[derive(Serialize)]
#[serde(tag = "kind")]
enum NodeDetail<'a> {
#[serde(rename = "entity")]
Entity { entity_kind: &'a str },
#[serde(rename = "claim")]
Claim { confidence: &'static str },
#[serde(rename = "evidence")]
Evidence {
evidence_kind: &'a str,
source_type: &'static str,
source: &'a str,
},
#[serde(rename = "goal")]
Goal { status: &'static str },
#[serde(rename = "constraint")]
Constraint { strength: &'static str },
#[serde(rename = "question")]
Question { status: &'static str },
#[serde(rename = "relationship")]
Relationship { target: &'a str, status: &'a str },
#[serde(rename = "desire")]
Desire { force: &'static str },
#[serde(rename = "attention")]
Attention { mode: &'static str },
#[serde(rename = "association")]
Association,
#[serde(rename = "tension")]
Tension { status: &'static str },
#[serde(rename = "prediction")]
Prediction {
confidence: &'static str,
status: &'static str,
},
#[serde(rename = "perspective")]
Perspective {
target: &'a str,
confidence: &'static str,
},
#[serde(rename = "affect")]
Affect {
target: &'a str,
affect_kind: &'a str,
intensity: &'static str,
},
#[serde(rename = "metacognition")]
Metacognition { status: &'static str },
}
#[derive(Serialize)]
struct RelationProjection<'a> {
kind: &'static str,
source: &'a str,
target: &'a str,
}
#[derive(Clone, Serialize)]
struct FocusProjection {
stream_id: String,
sequence: u64,
owner: String,
transition: &'static str,
previous_sequence: Option<u64>,
foreground: Vec<String>,
background: Vec<String>,
residue: Vec<String>,
trigger: String,
}
impl<'a> From<&'a Node> for NodeProjection<'a> {
fn from(node: &'a Node) -> Self {
Self {
id: &node.id,
revision: node.revision,
owner: &node.owner,
lifetime: lifetime_text(node.lifetime),
salience: node.salience,
detail: NodeDetail::from(&node.kind),
payload: canonical_form(&node.payload),
}
}
}
impl<'a> From<&'a NodeKind> for NodeDetail<'a> {
fn from(kind: &'a NodeKind) -> Self {
match kind {
NodeKind::Entity { entity_kind } => Self::Entity { entity_kind },
NodeKind::Claim { confidence } => Self::Claim {
confidence: confidence_text(*confidence),
},
NodeKind::Evidence {
evidence_kind,
source,
} => Self::Evidence {
evidence_kind,
source_type: match source {
Source::Symbol(_) => "symbol",
Source::String(_) => "string",
},
source: match source {
Source::Symbol(value) | Source::String(value) => value,
},
},
NodeKind::Goal { status } => Self::Goal {
status: goal_status(*status),
},
NodeKind::Constraint { strength } => Self::Constraint {
strength: constraint_text(*strength),
},
NodeKind::Question { status } => Self::Question {
status: question_text(*status),
},
NodeKind::Relationship { target, status } => Self::Relationship { target, status },
NodeKind::Desire { force } => Self::Desire {
force: desire_text(*force),
},
NodeKind::Attention { mode } => Self::Attention {
mode: attention_text(*mode),
},
NodeKind::Association => Self::Association,
NodeKind::Tension { status } => Self::Tension {
status: tension_text(*status),
},
NodeKind::Prediction { confidence, status } => Self::Prediction {
confidence: confidence_text(*confidence),
status: prediction_text(*status),
},
NodeKind::Perspective(value) => Self::Perspective {
target: &value.target,
confidence: confidence_text(value.confidence),
},
NodeKind::Affect {
target,
affect_kind,
intensity,
} => Self::Affect {
target,
affect_kind,
intensity: affect_text(*intensity),
},
NodeKind::Metacognition { status } => Self::Metacognition {
status: metacognition_text(*status),
},
}
}
}
impl FocusProjection {
fn from_frame(frame: &FocusFrame) -> Self {
Self {
stream_id: frame.stream_id.clone(),
sequence: frame.sequence,
owner: frame.owner.clone(),
transition: frame.transition.as_str(),
previous_sequence: frame.previous_sequence,
foreground: sorted(&frame.foreground),
background: sorted(&frame.background),
residue: sorted(&frame.residue),
trigger: canonical_form(&frame.trigger),
}
}
fn from_cue(cue: &FocusCue) -> Self {
Self {
stream_id: cue.stream_id.clone(),
sequence: cue.sequence,
owner: cue.owner.clone(),
transition: cue.transition.as_str(),
previous_sequence: None,
foreground: sorted(&cue.foreground),
background: sorted(&cue.background),
residue: sorted(&cue.residue),
trigger: cue.trigger.clone(),
}
}
}
fn render_json(
state_hash: String,
nodes: Vec<&Node>,
relations: Vec<&Relation>,
focus: Vec<FocusProjection>,
) -> String {
serde_json::to_string(&Projection {
schema: "chappi.projection",
version: 1,
state_hash,
nodes: nodes.into_iter().map(NodeProjection::from).collect(),
relations: relations
.into_iter()
.map(|relation| RelationProjection {
kind: relation.kind.as_str(),
source: &relation.source,
target: &relation.target,
})
.collect(),
focus,
})
.expect("projection DTOs always serialize")
}
fn render_text(
state_hash: &str,
nodes: &[&Node],
relations: &[&Relation],
focus: &[FocusProjection],
language: Language,
) -> String {
let mut output = String::new();
match language {
Language::En => writeln!(output, "Chappi projection v1 state_hash={state_hash}"),
Language::Ja => writeln!(output, "Chappi 投影 v1 状態ハッシュ={state_hash}"),
}
.expect("writing to String cannot fail");
for node in nodes {
write_node_text(&mut output, node, language);
}
for relation in relations {
match language {
Language::En => writeln!(
output,
"relation kind={} source={} target={}",
relation.kind.as_str(),
relation.source,
relation.target
),
Language::Ja => writeln!(
output,
"関係 種別={} 起点={} 終点={}",
relation.kind.as_str(),
relation.source,
relation.target
),
}
.expect("writing to String cannot fail");
}
for cue in focus {
match language {
Language::En => writeln!(
output,
"focus stream={} sequence={} owner={} transition={} previous={} foreground={} background={} residue={} trigger={}",
cue.stream_id,
cue.sequence,
cue.owner,
cue.transition,
optional_sequence(cue.previous_sequence),
cue.foreground.join(","),
cue.background.join(","),
cue.residue.join(","),
cue.trigger
),
Language::Ja => writeln!(
output,
"焦点 ストリーム={} 連番={} 所有者={} 遷移={} 前連番={} 前景={} 背景={} 残留={} 契機={}",
cue.stream_id,
cue.sequence,
cue.owner,
cue.transition,
optional_sequence(cue.previous_sequence),
cue.foreground.join(","),
cue.background.join(","),
cue.residue.join(","),
cue.trigger
),
}
.expect("writing to String cannot fail");
}
output
}
fn write_node_text(output: &mut String, node: &Node, language: Language) {
let detail = text_detail(&node.kind, language);
match language {
Language::En => writeln!(
output,
"node id={} revision={} owner={} lifetime={} salience={} {detail} payload={}",
node.id,
node.revision,
node.owner,
lifetime_text(node.lifetime),
node.salience,
canonical_form(&node.payload)
),
Language::Ja => writeln!(
output,
"ノード id={} リビジョン={} 所有者={} 生存期間={} 顕著度={} {detail} ペイロード={}",
node.id,
node.revision,
node.owner,
lifetime_text(node.lifetime),
node.salience,
canonical_form(&node.payload)
),
}
.expect("writing to String cannot fail");
}
fn text_detail(kind: &NodeKind, language: Language) -> String {
let label = |english: &'static str, japanese: &'static str| match language {
Language::En => english,
Language::Ja => japanese,
};
match kind {
NodeKind::Entity { entity_kind } => format!(
"{}=entity {}={entity_kind}",
label("kind", "種別"),
label("entity_kind", "実体種別")
),
NodeKind::Claim { confidence } => format!(
"{}=claim {}={}",
label("kind", "種別"),
label("confidence", "確信度"),
confidence_text(*confidence)
),
NodeKind::Evidence {
evidence_kind,
source,
} => format!(
"{}=evidence {}={} {}={}",
label("kind", "種別"),
label("evidence_kind", "証拠種別"),
evidence_kind,
label("source", "情報源"),
source_text(source)
),
NodeKind::Goal { status } => format!(
"{}=goal {}={}",
label("kind", "種別"),
label("status", "状態"),
goal_status(*status)
),
NodeKind::Constraint { strength } => format!(
"{}=constraint {}={}",
label("kind", "種別"),
label("strength", "強度"),
constraint_text(*strength)
),
NodeKind::Question { status } => format!(
"{}=question {}={}",
label("kind", "種別"),
label("status", "状態"),
question_text(*status)
),
NodeKind::Relationship { target, status } => format!(
"{}=relationship {}={} {}={}",
label("kind", "種別"),
label("target", "対象"),
target,
label("status", "状態"),
status
),
NodeKind::Desire { force } => format!(
"{}=desire {}={}",
label("kind", "種別"),
label("force", "力"),
desire_text(*force)
),
NodeKind::Attention { mode } => format!(
"{}=attention {}={}",
label("kind", "種別"),
label("mode", "モード"),
attention_text(*mode)
),
NodeKind::Association => format!("{}=association", label("kind", "種別")),
NodeKind::Tension { status } => format!(
"{}=tension {}={}",
label("kind", "種別"),
label("status", "状態"),
tension_text(*status)
),
NodeKind::Prediction { confidence, status } => format!(
"{}=prediction {}={} {}={}",
label("kind", "種別"),
label("confidence", "確信度"),
confidence_text(*confidence),
label("status", "状態"),
prediction_text(*status)
),
NodeKind::Perspective(value) => format!(
"{}=perspective {}={} {}={}",
label("kind", "種別"),
label("perspective_target", "視点対象"),
value.target,
label("confidence", "確信度"),
confidence_text(value.confidence)
),
NodeKind::Affect {
target,
affect_kind,
intensity,
} => format!(
"{}=affect {}={} {}={} {}={}",
label("kind", "種別"),
label("target", "対象"),
target,
label("affect_kind", "感情種別"),
affect_kind,
label("intensity", "強さ"),
affect_text(*intensity)
),
NodeKind::Metacognition { status } => format!(
"{}=metacognition {}={}",
label("kind", "種別"),
label("status", "状態"),
metacognition_text(*status)
),
}
}
fn source_text(source: &Source) -> String {
match source {
Source::Symbol(value) => value.clone(),
Source::String(value) => {
serde_json::to_string(value).expect("serializing a string cannot fail")
}
}
}
fn sorted(values: &[String]) -> Vec<String> {
let mut values = values.to_vec();
values.sort();
values
}
fn sorted_relations(relations: &[Relation]) -> Vec<&Relation> {
let mut relations = relations.iter().collect::<Vec<_>>();
relations.sort_by(|left, right| {
left.kind
.as_str()
.cmp(right.kind.as_str())
.then_with(|| left.source.cmp(&right.source))
.then_with(|| left.target.cmp(&right.target))
});
relations
}
fn optional_sequence(value: Option<u64>) -> String {
value.map_or_else(|| "none".to_owned(), |value| value.to_string())
}