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}