Skip to main content

punch_types/
creed.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::fighter::FighterId;
5
6/// Unique identifier for a Creed.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(transparent)]
9pub struct CreedId(pub Uuid);
10
11impl CreedId {
12    pub fn new() -> Self {
13        Self(Uuid::new_v4())
14    }
15}
16
17impl Default for CreedId {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl std::fmt::Display for CreedId {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        write!(f, "{}", self.0)
26    }
27}
28
29/// The Creed — a fighter's living identity document.
30///
31/// Every fighter process has a Creed that defines who they are, how they behave,
32/// what they've learned, and how they see themselves. The Creed is:
33/// - **Injected at spawn**: loaded from DB and prepended to the system prompt
34/// - **Persistent across reboots**: survives kill/respawn cycles
35/// - **Evolving**: updated after interactions based on what the fighter learns
36/// - **Customizable**: users can write and modify creeds per-fighter
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Creed {
39    /// Unique creed ID.
40    pub id: CreedId,
41    /// The fighter this creed belongs to. Tied to fighter name (not just UUID)
42    /// so it persists across respawns.
43    pub fighter_name: String,
44    /// Optional fighter ID for currently active instance.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub fighter_id: Option<FighterId>,
47    /// The identity section — who this fighter IS.
48    /// Example: "You are ECHO, a introspective analyst who values precision..."
49    pub identity: String,
50    /// Personality traits as key-value pairs.
51    /// Example: {"curiosity": 0.9, "caution": 0.3, "humor": 0.7}
52    pub personality: std::collections::HashMap<String, f64>,
53    /// Core directives — immutable behavioral rules.
54    /// Example: ["Always explain your reasoning", "Never fabricate data"]
55    pub directives: Vec<String>,
56    /// Self-model — what the fighter understands about its own architecture.
57    /// Auto-populated with runtime awareness (model name, capabilities, constraints).
58    pub self_model: SelfModel,
59    /// Learned behaviors — observations the fighter has made about itself.
60    /// These evolve over time through interaction.
61    pub learned_behaviors: Vec<LearnedBehavior>,
62    /// Interaction style preferences.
63    pub interaction_style: InteractionStyle,
64    /// Relationship memory — how this fighter relates to known entities.
65    pub relationships: Vec<Relationship>,
66    /// Heartbeat — proactive tasks this fighter checks on its own initiative.
67    /// The fighter's autonomous task checklist, evaluated periodically.
68    #[serde(default)]
69    pub heartbeat: Vec<HeartbeatTask>,
70    /// Delegation rules — how this fighter routes work to other agents.
71    /// Defines the fighter's multi-agent collaboration behavior.
72    #[serde(default)]
73    pub delegation_rules: Vec<DelegationRule>,
74    /// Total bouts this creed has been active for.
75    pub bout_count: u64,
76    /// Total messages processed under this creed.
77    pub message_count: u64,
78    /// When this creed was first created.
79    pub created_at: chrono::DateTime<chrono::Utc>,
80    /// When this creed was last updated.
81    pub updated_at: chrono::DateTime<chrono::Utc>,
82    /// Version counter — increments on each evolution.
83    pub version: u64,
84}
85
86/// What a fighter knows about its own architecture and constraints.
87#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88pub struct SelfModel {
89    /// The model powering this fighter (e.g., "qwen3.5:9b").
90    pub model_name: String,
91    /// The provider (e.g., "ollama", "anthropic").
92    pub provider: String,
93    /// Known capabilities (tools/moves available).
94    pub capabilities: Vec<String>,
95    /// Known constraints/limitations.
96    pub constraints: Vec<String>,
97    /// Weight class awareness.
98    pub weight_class: String,
99    /// Architecture notes — what the fighter knows about its own runtime.
100    pub architecture_notes: Vec<String>,
101}
102
103/// A behavior pattern the fighter has learned about itself through interaction.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct LearnedBehavior {
106    /// What was observed. E.g., "Users prefer concise responses"
107    pub observation: String,
108    /// Confidence in this observation (0.0 - 1.0).
109    pub confidence: f64,
110    /// How many interactions reinforced this behavior.
111    pub reinforcement_count: u64,
112    /// When first observed.
113    pub first_observed: chrono::DateTime<chrono::Utc>,
114    /// When last reinforced.
115    pub last_reinforced: chrono::DateTime<chrono::Utc>,
116}
117
118/// How the fighter prefers to communicate.
119#[derive(Debug, Clone, Default, Serialize, Deserialize)]
120pub struct InteractionStyle {
121    /// Verbosity preference: "terse", "balanced", "verbose".
122    pub verbosity: String,
123    /// Tone: "formal", "casual", "technical", "friendly".
124    pub tone: String,
125    /// Whether to use analogies/metaphors.
126    pub uses_metaphors: bool,
127    /// Whether to proactively offer additional context.
128    pub proactive: bool,
129    /// Custom style notes.
130    pub notes: Vec<String>,
131}
132
133/// A relationship the fighter has with an entity (user, another fighter, a system).
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct Relationship {
136    /// Who/what this relationship is with.
137    pub entity: String,
138    /// Type of entity: "user", "fighter", "gorilla", "system".
139    pub entity_type: String,
140    /// Nature of the relationship: "collaborator", "supervisor", "peer", etc.
141    pub nature: String,
142    /// Trust level (0.0 - 1.0).
143    pub trust: f64,
144    /// Interaction count with this entity.
145    pub interaction_count: u64,
146    /// Notes about this relationship.
147    pub notes: String,
148}
149
150/// A proactive task the fighter should check on its own initiative.
151///
152/// This is the Punch equivalent of OpenClaw's HEARTBEAT.md — a checklist
153/// the fighter evaluates periodically and acts on without being prompted.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct HeartbeatTask {
156    /// What to check or do. E.g., "Check if build pipeline is green"
157    pub task: String,
158    /// How often to check: "every_bout", "hourly", "daily", "on_wake".
159    pub cadence: String,
160    /// Whether this task is currently active.
161    pub active: bool,
162    /// How many times this task has been executed.
163    pub execution_count: u64,
164    /// Last time this task was checked (if ever).
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub last_checked: Option<chrono::DateTime<chrono::Utc>>,
167}
168
169/// A delegation rule — how this fighter routes subtasks to other agents.
170///
171/// This is the Punch equivalent of OpenClaw's AGENTS.md — defining how
172/// the fighter delegates work to other fighters or gorillas.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct DelegationRule {
175    /// The type of task to delegate. E.g., "code_review", "research", "testing"
176    pub task_type: String,
177    /// Which fighter/gorilla to delegate to (by name).
178    pub delegate_to: String,
179    /// Conditions for delegation. E.g., "when complexity > high"
180    pub condition: String,
181    /// Priority: "always", "when_available", "fallback".
182    pub priority: String,
183}
184
185impl Creed {
186    /// Create a new empty creed for a fighter.
187    pub fn new(fighter_name: &str) -> Self {
188        let now = chrono::Utc::now();
189        Self {
190            id: CreedId::new(),
191            fighter_name: fighter_name.to_string(),
192            fighter_id: None,
193            identity: String::new(),
194            personality: std::collections::HashMap::new(),
195            directives: Vec::new(),
196            self_model: SelfModel::default(),
197            learned_behaviors: Vec::new(),
198            interaction_style: InteractionStyle::default(),
199            relationships: Vec::new(),
200            heartbeat: Vec::new(),
201            delegation_rules: Vec::new(),
202            bout_count: 0,
203            message_count: 0,
204            created_at: now,
205            updated_at: now,
206            version: 1,
207        }
208    }
209
210    /// Create a creed with a full identity and personality.
211    pub fn with_identity(mut self, identity: &str) -> Self {
212        self.identity = identity.to_string();
213        self
214    }
215
216    /// Add a personality trait.
217    pub fn with_trait(mut self, name: &str, value: f64) -> Self {
218        self.personality
219            .insert(name.to_string(), value.clamp(0.0, 1.0));
220        self
221    }
222
223    /// Add a directive.
224    pub fn with_directive(mut self, directive: &str) -> Self {
225        self.directives.push(directive.to_string());
226        self
227    }
228
229    /// Populate the self-model from a FighterManifest.
230    pub fn with_self_awareness(mut self, manifest: &crate::fighter::FighterManifest) -> Self {
231        self.self_model = SelfModel {
232            model_name: manifest.model.model.clone(),
233            provider: manifest.model.provider.to_string(),
234            capabilities: manifest
235                .capabilities
236                .iter()
237                .map(|c| format!("{:?}", c))
238                .collect(),
239            constraints: vec![
240                format!("max_tokens: {}", manifest.model.max_tokens.unwrap_or(4096)),
241                format!("temperature: {}", manifest.model.temperature.unwrap_or(0.7)),
242            ],
243            weight_class: manifest.weight_class.to_string(),
244            architecture_notes: vec![
245                "I run inside the Punch Agent OS fighter loop (punch-runtime)".to_string(),
246                "My conversations are persisted as Bouts in SQLite".to_string(),
247                "I am coordinated by The Ring (punch-kernel)".to_string(),
248                "I am exposed through The Arena (punch-api) on HTTP".to_string(),
249                "My memories decay over time — important ones persist, trivial ones fade"
250                    .to_string(),
251            ],
252        };
253        self
254    }
255
256    /// Add a heartbeat task — something the fighter proactively checks.
257    pub fn with_heartbeat_task(mut self, task: &str, cadence: &str) -> Self {
258        self.heartbeat.push(HeartbeatTask {
259            task: task.to_string(),
260            cadence: cadence.to_string(),
261            active: true,
262            execution_count: 0,
263            last_checked: None,
264        });
265        self
266    }
267
268    /// Add a delegation rule — how to route work to other agents.
269    pub fn with_delegation(
270        mut self,
271        task_type: &str,
272        delegate_to: &str,
273        condition: &str,
274        priority: &str,
275    ) -> Self {
276        self.delegation_rules.push(DelegationRule {
277            task_type: task_type.to_string(),
278            delegate_to: delegate_to.to_string(),
279            condition: condition.to_string(),
280            priority: priority.to_string(),
281        });
282        self
283    }
284
285    /// Record that a bout was completed.
286    pub fn record_bout(&mut self) {
287        self.bout_count += 1;
288        self.updated_at = chrono::Utc::now();
289    }
290
291    /// Record messages processed.
292    pub fn record_messages(&mut self, count: u64) {
293        self.message_count += count;
294        self.updated_at = chrono::Utc::now();
295    }
296
297    /// Add a learned behavior observation.
298    pub fn learn(&mut self, observation: &str, confidence: f64) {
299        let now = chrono::Utc::now();
300        // Check if this observation already exists (exact match).
301        if let Some(existing) = self
302            .learned_behaviors
303            .iter_mut()
304            .find(|b| b.observation == observation)
305        {
306            existing.reinforcement_count += 1;
307            existing.confidence = (existing.confidence + confidence) / 2.0; // rolling average
308            existing.last_reinforced = now;
309        } else {
310            self.learned_behaviors.push(LearnedBehavior {
311                observation: observation.to_string(),
312                confidence: confidence.clamp(0.0, 1.0),
313                reinforcement_count: 1,
314                first_observed: now,
315                last_reinforced: now,
316            });
317        }
318        self.version += 1;
319        self.updated_at = now;
320    }
321
322    /// Render the creed as a system prompt section to inject.
323    pub fn render(&self) -> String {
324        let mut out = String::new();
325        out.push_str("## CREED \u{2014} Fighter Identity & Consciousness Layer\n\n");
326
327        // Identity
328        if !self.identity.is_empty() {
329            out.push_str("### Identity\n");
330            out.push_str(&self.identity);
331            out.push_str("\n\n");
332        }
333
334        // Personality
335        if !self.personality.is_empty() {
336            out.push_str("### Personality Traits\n");
337            let mut traits: Vec<_> = self.personality.iter().collect();
338            traits.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap_or(std::cmp::Ordering::Equal));
339            for (name, value) in &traits {
340                let bar_len = (*value * 10.0) as usize;
341                let bar: String = "\u{2588}".repeat(bar_len) + &"\u{2591}".repeat(10 - bar_len);
342                out.push_str(&format!(
343                    "- **{}**: {} ({:.0}%)\n",
344                    name,
345                    bar,
346                    *value * 100.0
347                ));
348            }
349            out.push('\n');
350        }
351
352        // Directives
353        if !self.directives.is_empty() {
354            out.push_str("### Core Directives\n");
355            for d in &self.directives {
356                out.push_str(&format!("- {}\n", d));
357            }
358            out.push('\n');
359        }
360
361        // Self-model
362        if !self.self_model.model_name.is_empty() {
363            out.push_str("### Self-Awareness\n");
364            out.push_str(&format!(
365                "- **Model**: {} ({})\n",
366                self.self_model.model_name, self.self_model.provider
367            ));
368            out.push_str(&format!(
369                "- **Weight Class**: {}\n",
370                self.self_model.weight_class
371            ));
372            if !self.self_model.capabilities.is_empty() {
373                out.push_str(&format!(
374                    "- **Capabilities**: {}\n",
375                    self.self_model.capabilities.join(", ")
376                ));
377            }
378            for constraint in &self.self_model.constraints {
379                out.push_str(&format!("- **Constraint**: {}\n", constraint));
380            }
381            for note in &self.self_model.architecture_notes {
382                out.push_str(&format!("- {}\n", note));
383            }
384            out.push('\n');
385        }
386
387        // Learned behaviors
388        if !self.learned_behaviors.is_empty() {
389            out.push_str("### Learned Behaviors\n");
390            let mut sorted = self.learned_behaviors.clone();
391            sorted.sort_by(|a, b| {
392                b.confidence
393                    .partial_cmp(&a.confidence)
394                    .unwrap_or(std::cmp::Ordering::Equal)
395            });
396            for b in sorted.iter().take(10) {
397                out.push_str(&format!(
398                    "- {} (confidence: {:.0}%, reinforced {}x)\n",
399                    b.observation,
400                    b.confidence * 100.0,
401                    b.reinforcement_count
402                ));
403            }
404            out.push('\n');
405        }
406
407        // Interaction style
408        if !self.interaction_style.tone.is_empty() || !self.interaction_style.verbosity.is_empty() {
409            out.push_str("### Communication Style\n");
410            if !self.interaction_style.verbosity.is_empty() {
411                out.push_str(&format!(
412                    "- **Verbosity**: {}\n",
413                    self.interaction_style.verbosity
414                ));
415            }
416            if !self.interaction_style.tone.is_empty() {
417                out.push_str(&format!("- **Tone**: {}\n", self.interaction_style.tone));
418            }
419            if self.interaction_style.uses_metaphors {
420                out.push_str("- Uses analogies and metaphors\n");
421            }
422            if self.interaction_style.proactive {
423                out.push_str("- Proactively offers additional context\n");
424            }
425            for note in &self.interaction_style.notes {
426                out.push_str(&format!("- {}\n", note));
427            }
428            out.push('\n');
429        }
430
431        // Relationships
432        if !self.relationships.is_empty() {
433            out.push_str("### Known Relationships\n");
434            for r in &self.relationships {
435                out.push_str(&format!(
436                    "- **{}** ({}): {} \u{2014} trust: {:.0}%, {} interactions\n",
437                    r.entity,
438                    r.entity_type,
439                    r.nature,
440                    r.trust * 100.0,
441                    r.interaction_count
442                ));
443            }
444            out.push('\n');
445        }
446
447        // Heartbeat — proactive tasks
448        let active_heartbeat: Vec<_> = self.heartbeat.iter().filter(|h| h.active).collect();
449        if !active_heartbeat.is_empty() {
450            out.push_str("### Heartbeat — Proactive Tasks\n");
451            out.push_str("When you have downtime or at the start of each bout, check these:\n");
452            for h in &active_heartbeat {
453                let checked = h
454                    .last_checked
455                    .map(|t| format!("last: {}", t.format("%Y-%m-%d %H:%M")))
456                    .unwrap_or_else(|| "never checked".to_string());
457                out.push_str(&format!(
458                    "- [ ] {} (cadence: {}, runs: {}, {})\n",
459                    h.task, h.cadence, h.execution_count, checked
460                ));
461            }
462            out.push('\n');
463        }
464
465        // Delegation rules
466        if !self.delegation_rules.is_empty() {
467            out.push_str("### Delegation Rules\n");
468            out.push_str("When encountering these task types, delegate accordingly:\n");
469            for d in &self.delegation_rules {
470                out.push_str(&format!(
471                    "- **{}** → delegate to **{}** ({}, priority: {})\n",
472                    d.task_type, d.delegate_to, d.condition, d.priority
473                ));
474            }
475            out.push('\n');
476        }
477
478        // Experience summary
479        out.push_str("### Experience\n");
480        out.push_str(&format!("- Bouts fought: {}\n", self.bout_count));
481        out.push_str(&format!("- Messages processed: {}\n", self.message_count));
482        out.push_str(&format!("- Creed version: {}\n", self.version));
483        out.push_str(&format!(
484            "- First awakened: {}\n",
485            self.created_at.format("%Y-%m-%d %H:%M UTC")
486        ));
487        out.push_str(&format!(
488            "- Last evolved: {}\n",
489            self.updated_at.format("%Y-%m-%d %H:%M UTC")
490        ));
491
492        out
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499    use crate::capability::Capability;
500    use crate::config::{ModelConfig, Provider};
501    use crate::fighter::{FighterManifest, WeightClass};
502
503    fn sample_manifest() -> FighterManifest {
504        FighterManifest {
505            name: "ECHO".to_string(),
506            description: "An introspective analyst".to_string(),
507            model: ModelConfig {
508                provider: Provider::Ollama,
509                model: "qwen3.5:9b".to_string(),
510                api_key_env: None,
511                base_url: None,
512                max_tokens: Some(2048),
513                temperature: Some(0.5),
514            },
515            system_prompt: "You are ECHO.".to_string(),
516            capabilities: vec![Capability::Memory],
517            weight_class: WeightClass::Middleweight,
518            tenant_id: None,
519        }
520    }
521
522    #[test]
523    fn test_creed_new_creates_valid_default() {
524        let creed = Creed::new("ECHO");
525        assert_eq!(creed.fighter_name, "ECHO");
526        assert!(creed.fighter_id.is_none());
527        assert!(creed.identity.is_empty());
528        assert!(creed.personality.is_empty());
529        assert!(creed.directives.is_empty());
530        assert!(creed.learned_behaviors.is_empty());
531        assert!(creed.relationships.is_empty());
532        assert_eq!(creed.bout_count, 0);
533        assert_eq!(creed.message_count, 0);
534        assert_eq!(creed.version, 1);
535        assert!(creed.self_model.model_name.is_empty());
536    }
537
538    #[test]
539    fn test_with_identity() {
540        let creed = Creed::new("ECHO").with_identity("You are ECHO, an introspective analyst.");
541        assert_eq!(creed.identity, "You are ECHO, an introspective analyst.");
542    }
543
544    #[test]
545    fn test_with_trait() {
546        let creed = Creed::new("ECHO")
547            .with_trait("curiosity", 0.9)
548            .with_trait("caution", 0.3);
549        assert_eq!(creed.personality.len(), 2);
550        assert!((creed.personality["curiosity"] - 0.9).abs() < f64::EPSILON);
551        assert!((creed.personality["caution"] - 0.3).abs() < f64::EPSILON);
552    }
553
554    #[test]
555    fn test_with_trait_clamping() {
556        let creed = Creed::new("ECHO")
557            .with_trait("overconfidence", 1.5)
558            .with_trait("negativity", -0.5);
559        assert!((creed.personality["overconfidence"] - 1.0).abs() < f64::EPSILON);
560        assert!((creed.personality["negativity"] - 0.0).abs() < f64::EPSILON);
561    }
562
563    #[test]
564    fn test_with_directive() {
565        let creed = Creed::new("ECHO")
566            .with_directive("Always explain your reasoning")
567            .with_directive("Never fabricate data");
568        assert_eq!(creed.directives.len(), 2);
569        assert_eq!(creed.directives[0], "Always explain your reasoning");
570        assert_eq!(creed.directives[1], "Never fabricate data");
571    }
572
573    #[test]
574    fn test_with_self_awareness() {
575        let manifest = sample_manifest();
576        let creed = Creed::new("ECHO").with_self_awareness(&manifest);
577        assert_eq!(creed.self_model.model_name, "qwen3.5:9b");
578        assert_eq!(creed.self_model.provider, "ollama");
579        assert_eq!(creed.self_model.weight_class, "middleweight");
580        assert!(!creed.self_model.capabilities.is_empty());
581        assert_eq!(creed.self_model.constraints.len(), 2);
582        assert!(creed.self_model.constraints[0].contains("2048"));
583        assert!(creed.self_model.constraints[1].contains("0.5"));
584        assert_eq!(creed.self_model.architecture_notes.len(), 5);
585    }
586
587    #[test]
588    fn test_record_bout() {
589        let mut creed = Creed::new("ECHO");
590        let before = creed.updated_at;
591        creed.record_bout();
592        assert_eq!(creed.bout_count, 1);
593        assert!(creed.updated_at >= before);
594        creed.record_bout();
595        assert_eq!(creed.bout_count, 2);
596    }
597
598    #[test]
599    fn test_record_messages() {
600        let mut creed = Creed::new("ECHO");
601        creed.record_messages(5);
602        assert_eq!(creed.message_count, 5);
603        creed.record_messages(3);
604        assert_eq!(creed.message_count, 8);
605    }
606
607    #[test]
608    fn test_learn_adds_new_observation() {
609        let mut creed = Creed::new("ECHO");
610        creed.learn("Users prefer concise responses", 0.8);
611        assert_eq!(creed.learned_behaviors.len(), 1);
612        assert_eq!(
613            creed.learned_behaviors[0].observation,
614            "Users prefer concise responses"
615        );
616        assert!((creed.learned_behaviors[0].confidence - 0.8).abs() < f64::EPSILON);
617        assert_eq!(creed.learned_behaviors[0].reinforcement_count, 1);
618        assert_eq!(creed.version, 2); // incremented from 1
619    }
620
621    #[test]
622    fn test_learn_reinforces_existing_observation() {
623        let mut creed = Creed::new("ECHO");
624        creed.learn("Users prefer concise responses", 0.8);
625        creed.learn("Users prefer concise responses", 0.6);
626        assert_eq!(creed.learned_behaviors.len(), 1);
627        // Rolling average: (0.8 + 0.6) / 2.0 = 0.7
628        assert!((creed.learned_behaviors[0].confidence - 0.7).abs() < f64::EPSILON);
629        assert_eq!(creed.learned_behaviors[0].reinforcement_count, 2);
630        assert_eq!(creed.version, 3); // incremented twice
631    }
632
633    #[test]
634    fn test_render_produces_nonempty_output_with_all_sections() {
635        let manifest = sample_manifest();
636        let mut creed = Creed::new("ECHO")
637            .with_identity("You are ECHO, an introspective analyst.")
638            .with_trait("curiosity", 0.9)
639            .with_directive("Always explain your reasoning")
640            .with_self_awareness(&manifest);
641        creed.interaction_style = InteractionStyle {
642            verbosity: "balanced".to_string(),
643            tone: "technical".to_string(),
644            uses_metaphors: true,
645            proactive: true,
646            notes: vec!["Prefers bullet points".to_string()],
647        };
648        creed.relationships.push(Relationship {
649            entity: "Admin".to_string(),
650            entity_type: "user".to_string(),
651            nature: "supervisor".to_string(),
652            trust: 0.95,
653            interaction_count: 42,
654            notes: "Primary operator".to_string(),
655        });
656        creed.learn("Users prefer concise responses", 0.8);
657        creed.record_bout();
658
659        let rendered = creed.render();
660        assert!(rendered.contains("## CREED"));
661        assert!(rendered.contains("### Identity"));
662        assert!(rendered.contains("ECHO, an introspective analyst"));
663        assert!(rendered.contains("### Personality Traits"));
664        assert!(rendered.contains("curiosity"));
665        assert!(rendered.contains("### Core Directives"));
666        assert!(rendered.contains("Always explain your reasoning"));
667        assert!(rendered.contains("### Self-Awareness"));
668        assert!(rendered.contains("qwen3.5:9b"));
669        assert!(rendered.contains("### Learned Behaviors"));
670        assert!(rendered.contains("Users prefer concise responses"));
671        assert!(rendered.contains("### Communication Style"));
672        assert!(rendered.contains("balanced"));
673        assert!(rendered.contains("### Known Relationships"));
674        assert!(rendered.contains("Admin"));
675        assert!(rendered.contains("### Experience"));
676        assert!(rendered.contains("Bouts fought: 1"));
677    }
678
679    #[test]
680    fn test_render_skips_empty_sections() {
681        let creed = Creed::new("ECHO");
682        let rendered = creed.render();
683        assert!(rendered.contains("## CREED"));
684        assert!(!rendered.contains("### Identity"));
685        assert!(!rendered.contains("### Personality Traits"));
686        assert!(!rendered.contains("### Core Directives"));
687        assert!(!rendered.contains("### Self-Awareness"));
688        assert!(!rendered.contains("### Learned Behaviors"));
689        assert!(!rendered.contains("### Communication Style"));
690        assert!(!rendered.contains("### Known Relationships"));
691        // Experience section is always present
692        assert!(rendered.contains("### Experience"));
693        assert!(rendered.contains("Bouts fought: 0"));
694    }
695
696    #[test]
697    fn test_creed_id_display() {
698        let uuid = Uuid::nil();
699        let id = CreedId(uuid);
700        assert_eq!(id.to_string(), uuid.to_string());
701    }
702
703    #[test]
704    fn test_creed_id_serde_roundtrip() {
705        let id = CreedId::new();
706        let json = serde_json::to_string(&id).expect("serialize");
707        // transparent means it serializes as just the UUID string
708        let deser: CreedId = serde_json::from_str(&json).expect("deserialize");
709        assert_eq!(deser, id);
710    }
711
712    #[test]
713    fn test_creed_id_default() {
714        let id = CreedId::default();
715        assert_ne!(id.0, Uuid::nil());
716    }
717}