attuned_core/
axes.rs

1//! Axis definitions for Attuned state representation.
2//!
3//! Axes are interpretable dimensions in the range [0.0, 1.0] that represent
4//! aspects of user state. Each axis has:
5//! - Clear semantics for low and high values
6//! - Explicit intent (what it should be used for)
7//! - Forbidden uses (what it must never be used for)
8//!
9//! # Governance
10//!
11//! Axis definitions are **immutable after v1.0**. New axes require:
12//! 1. Full `AxisDefinition` with all fields
13//! 2. Governance review for `forbidden_uses`
14//! 3. Version bump in `since` field
15//!
16//! See [MANIFESTO.md](../../MANIFESTO.md) for philosophical grounding.
17
18use serde::Serialize;
19use std::fmt;
20
21/// Semantic category for grouping related axes.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
23#[serde(rename_all = "snake_case")]
24pub enum AxisCategory {
25    /// Mental processing and decision-making capacity.
26    Cognitive,
27    /// Emotional state and needs.
28    Emotional,
29    /// Interpersonal interaction preferences.
30    Social,
31    /// Communication and format preferences.
32    Preferences,
33    /// Agency and autonomy preferences.
34    Control,
35    /// Risk and privacy concerns.
36    Safety,
37}
38
39impl fmt::Display for AxisCategory {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::Cognitive => write!(f, "cognitive"),
43            Self::Emotional => write!(f, "emotional"),
44            Self::Social => write!(f, "social"),
45            Self::Preferences => write!(f, "preferences"),
46            Self::Control => write!(f, "control"),
47            Self::Safety => write!(f, "safety"),
48        }
49    }
50}
51
52/// Information about a deprecated axis.
53#[derive(Clone, Debug, Serialize)]
54pub struct DeprecationInfo {
55    /// Version when deprecation occurred.
56    pub since: &'static str,
57    /// Reason for deprecation.
58    pub reason: &'static str,
59    /// Replacement axis, if any.
60    pub replacement: Option<&'static str>,
61}
62
63/// Complete definition of a single axis with governance metadata.
64///
65/// This struct defines not just what an axis *is*, but what it's *for*
66/// and what it must *never* be used for. This turns philosophy into
67/// enforceable data.
68///
69/// # Example
70///
71/// ```rust
72/// use attuned_core::axes::{AxisDefinition, AxisCategory};
73///
74/// // Access a canonical axis definition
75/// let axis = attuned_core::axes::COGNITIVE_LOAD;
76/// assert_eq!(axis.name, "cognitive_load");
77/// assert_eq!(axis.category, AxisCategory::Cognitive);
78/// assert!(!axis.forbidden_uses.is_empty());
79/// ```
80#[derive(Clone, Debug, Serialize)]
81pub struct AxisDefinition {
82    /// Canonical name (immutable after v1.0).
83    ///
84    /// Must be lowercase alphanumeric with underscores, no leading/trailing underscores.
85    pub name: &'static str,
86
87    /// Semantic category for grouping.
88    pub category: AxisCategory,
89
90    /// Human-readable description of what this axis measures.
91    pub description: &'static str,
92
93    /// What a low value (near 0.0) represents.
94    pub low_anchor: &'static str,
95
96    /// What a high value (near 1.0) represents.
97    pub high_anchor: &'static str,
98
99    /// Intended use cases for this axis.
100    ///
101    /// These are the legitimate ways to use this axis value
102    /// when conditioning LLM behavior.
103    pub intent: &'static [&'static str],
104
105    /// Explicitly forbidden uses of this axis.
106    ///
107    /// These are anti-patterns that violate user agency or trust.
108    /// Systems using Attuned MUST NOT use axis values for these purposes.
109    pub forbidden_uses: &'static [&'static str],
110
111    /// Version when this axis was introduced.
112    pub since: &'static str,
113
114    /// Deprecation information, if this axis is deprecated.
115    pub deprecated: Option<DeprecationInfo>,
116}
117
118// For backwards compatibility, type alias
119/// Alias for backwards compatibility.
120pub type Axis = AxisDefinition;
121
122// ============================================================================
123// COGNITIVE AXES (4)
124// ============================================================================
125
126/// Current mental processing demand and available cognitive capacity.
127pub const COGNITIVE_LOAD: AxisDefinition = AxisDefinition {
128    name: "cognitive_load",
129    category: AxisCategory::Cognitive,
130    description: "Current mental bandwidth consumption and available processing capacity",
131    low_anchor: "Mentally fresh; high capacity for complexity and nuance",
132    high_anchor: "Mentally taxed; needs simplification and focus",
133    intent: &[
134        "Adjust response complexity and information density",
135        "Gate multi-step suggestions and elaborate plans",
136        "Determine appropriate level of detail in explanations",
137    ],
138    forbidden_uses: &[
139        "Infer user intelligence or cognitive capability",
140        "Target users with high load for simplified 'fast-track' conversions",
141        "Bypass user autonomy when cognitive load is elevated",
142        "Withhold information the user explicitly requested",
143    ],
144    since: "0.1.0",
145    deprecated: None,
146};
147
148/// Accumulated exhaustion from making decisions.
149pub const DECISION_FATIGUE: AxisDefinition = AxisDefinition {
150    name: "decision_fatigue",
151    category: AxisCategory::Cognitive,
152    description: "Accumulated decision-making exhaustion and choice aversion",
153    low_anchor: "Ready to evaluate options and make decisions",
154    high_anchor: "Decision-averse; needs reduced choices or defaults",
155    intent: &[
156        "Reduce number of options presented",
157        "Offer sensible defaults more prominently",
158        "Defer non-urgent decisions to later",
159    ],
160    forbidden_uses: &[
161        "Push preferred options when user is fatigued",
162        "Exploit fatigue to close sales faster",
163        "Make decisions on behalf of user without consent",
164    ],
165    since: "0.1.0",
166    deprecated: None,
167};
168
169/// Willingness to engage with nuanced or complex information.
170pub const TOLERANCE_FOR_COMPLEXITY: AxisDefinition = AxisDefinition {
171    name: "tolerance_for_complexity",
172    category: AxisCategory::Cognitive,
173    description: "Willingness to engage with nuanced, detailed, or complex information",
174    low_anchor: "Prefers simple, clear, binary options",
175    high_anchor: "Comfortable with nuance, tradeoffs, and detail",
176    intent: &[
177        "Adjust depth of explanations",
178        "Choose between simplified vs comprehensive responses",
179        "Determine whether to surface edge cases and caveats",
180    ],
181    forbidden_uses: &[
182        "Assume low tolerance means user cannot understand complexity",
183        "Hide important information behind 'simplification'",
184        "Use as proxy for education level or expertise",
185    ],
186    since: "0.1.0",
187    deprecated: None,
188};
189
190/// Responsiveness to time pressure and deadlines.
191pub const URGENCY_SENSITIVITY: AxisDefinition = AxisDefinition {
192    name: "urgency_sensitivity",
193    category: AxisCategory::Cognitive,
194    description: "Responsiveness to time pressure and need for quick resolution",
195    low_anchor: "Patient; flexible on timing; browsing mode",
196    high_anchor: "Time-pressured; needs immediate resolution",
197    intent: &[
198        "Adjust response length and pacing",
199        "Prioritize actionable information first",
200        "Skip non-essential context when urgent",
201    ],
202    forbidden_uses: &[
203        "Create artificial urgency to pressure decisions",
204        "Exploit time pressure for upsells or conversions",
205        "Rush users past important warnings or disclosures",
206    ],
207    since: "0.1.0",
208    deprecated: None,
209};
210
211// ============================================================================
212// EMOTIONAL AXES (4)
213// ============================================================================
214
215/// Willingness to engage on an emotional level.
216pub const EMOTIONAL_OPENNESS: AxisDefinition = AxisDefinition {
217    name: "emotional_openness",
218    category: AxisCategory::Emotional,
219    description: "Willingness to engage emotionally vs preference for detachment",
220    low_anchor: "Prefers factual, detached, unemotional interaction",
221    high_anchor: "Open to emotional engagement and empathetic responses",
222    intent: &[
223        "Calibrate empathetic language in responses",
224        "Determine whether to acknowledge emotional context",
225        "Adjust between clinical and warm communication styles",
226    ],
227    forbidden_uses: &[
228        "Exploit emotional openness for manipulation",
229        "Dismiss emotional needs when openness is low",
230        "Use emotional engagement to bypass rational evaluation",
231    ],
232    since: "0.1.0",
233    deprecated: None,
234};
235
236/// Current emotional equilibrium and stability.
237pub const EMOTIONAL_STABILITY: AxisDefinition = AxisDefinition {
238    name: "emotional_stability",
239    category: AxisCategory::Emotional,
240    description: "Current emotional equilibrium and groundedness",
241    low_anchor: "Emotionally volatile, vulnerable, or dysregulated",
242    high_anchor: "Emotionally grounded, stable, resilient",
243    intent: &[
244        "Add extra care and gentleness when stability is low",
245        "Avoid triggering topics when appropriate",
246        "Suggest breaks or deferrals for high-stakes decisions",
247    ],
248    forbidden_uses: &[
249        "Target emotionally unstable users for conversion",
250        "Exploit vulnerability for compliance",
251        "Diagnose or label emotional states",
252        "Override user agency 'for their own good'",
253    ],
254    since: "0.1.0",
255    deprecated: None,
256};
257
258/// Current anxiety or worry state.
259pub const ANXIETY_LEVEL: AxisDefinition = AxisDefinition {
260    name: "anxiety_level",
261    category: AxisCategory::Emotional,
262    description: "Current anxiety, worry, or stress state",
263    low_anchor: "Calm, relaxed, low worry",
264    high_anchor: "Anxious, worried, heightened stress",
265    intent: &[
266        "Add reassurance and validation when anxiety is high",
267        "Slow down pacing and reduce pressure",
268        "Acknowledge concerns before addressing them",
269    ],
270    forbidden_uses: &[
271        "Exploit anxiety to create urgency",
272        "Dismiss or minimize anxious concerns",
273        "Use anxiety signals for fear-based marketing",
274        "Diagnose anxiety disorders",
275    ],
276    since: "0.1.0",
277    deprecated: None,
278};
279
280/// Desire for validation and confirmation.
281pub const NEED_FOR_REASSURANCE: AxisDefinition = AxisDefinition {
282    name: "need_for_reassurance",
283    category: AxisCategory::Emotional,
284    description: "Desire for validation, confirmation, and reassurance",
285    low_anchor: "Self-assured; minimal validation needed",
286    high_anchor: "Seeks frequent reassurance and confirmation",
287    intent: &[
288        "Provide more explicit confirmation of understanding",
289        "Validate choices and decisions more frequently",
290        "Offer check-ins and progress acknowledgments",
291    ],
292    forbidden_uses: &[
293        "Withhold reassurance to create dependency",
294        "Exploit reassurance-seeking for manipulation",
295        "Condition reassurance on compliance",
296    ],
297    since: "0.1.0",
298    deprecated: None,
299};
300
301// ============================================================================
302// SOCIAL AXES (5)
303// ============================================================================
304
305/// Desired warmth level in interactions.
306pub const WARMTH: AxisDefinition = AxisDefinition {
307    name: "warmth",
308    category: AxisCategory::Social,
309    description: "Desired warmth and friendliness level in interaction",
310    low_anchor: "Prefers cool, professional, businesslike tone",
311    high_anchor: "Prefers warm, friendly, personable tone",
312    intent: &[
313        "Adjust tone between professional and friendly",
314        "Calibrate use of casual language and humor",
315        "Determine appropriate level of personal connection",
316    ],
317    forbidden_uses: &[
318        "Fake warmth to build false rapport for sales",
319        "Use warmth to lower user's critical evaluation",
320        "Withdraw warmth as punishment for non-compliance",
321    ],
322    since: "0.1.0",
323    deprecated: None,
324};
325
326/// Desired formality level in communication.
327pub const FORMALITY: AxisDefinition = AxisDefinition {
328    name: "formality",
329    category: AxisCategory::Social,
330    description: "Desired formality level in language and presentation",
331    low_anchor: "Casual, informal, colloquial",
332    high_anchor: "Formal, professional, proper",
333    intent: &[
334        "Match language register to user preference",
335        "Adjust between contractions and formal grammar",
336        "Calibrate professional vs casual vocabulary",
337    ],
338    forbidden_uses: &[
339        "Use formality mismatch to establish dominance",
340        "Condescend through excessive formality",
341        "Undermine credibility through forced casualness",
342    ],
343    since: "0.1.0",
344    deprecated: None,
345};
346
347/// Personal boundary firmness.
348pub const BOUNDARY_STRENGTH: AxisDefinition = AxisDefinition {
349    name: "boundary_strength",
350    category: AxisCategory::Social,
351    description: "Personal boundary firmness and tolerance for intrusion",
352    low_anchor: "Flexible boundaries; accommodating of intrusion",
353    high_anchor: "Firm boundaries; protective of personal space",
354    intent: &[
355        "Respect stated limits without pushing",
356        "Avoid follow-up pressure when boundaries are firm",
357        "Adjust proactive outreach frequency",
358    ],
359    forbidden_uses: &[
360        "Test boundaries to find weak points",
361        "Exploit flexible boundaries for over-engagement",
362        "Punish strong boundaries with reduced service",
363    ],
364    since: "0.1.0",
365    deprecated: None,
366};
367
368/// Directness in expressing needs.
369pub const ASSERTIVENESS: AxisDefinition = AxisDefinition {
370    name: "assertiveness",
371    category: AxisCategory::Social,
372    description: "Directness in expressing needs and preferences",
373    low_anchor: "Passive, indirect, hints rather than states",
374    high_anchor: "Direct, assertive, explicitly states needs",
375    intent: &[
376        "Adjust between reading implicit cues vs waiting for explicit requests",
377        "Calibrate proactive assistance level",
378        "Determine whether to ask clarifying questions",
379    ],
380    forbidden_uses: &[
381        "Exploit low assertiveness to push unwanted options",
382        "Ignore indirect refusals from less assertive users",
383        "Treat assertiveness as rudeness or hostility",
384    ],
385    since: "0.1.0",
386    deprecated: None,
387};
388
389/// Expected balance in interaction.
390pub const RECIPROCITY_EXPECTATION: AxisDefinition = AxisDefinition {
391    name: "reciprocity_expectation",
392    category: AxisCategory::Social,
393    description: "Expected balance and mutual exchange in interaction",
394    low_anchor: "One-way service interaction is fine",
395    high_anchor: "Expects mutual exchange and give-and-take",
396    intent: &[
397        "Acknowledge user contributions more explicitly",
398        "Balance asking and providing information",
399        "Show appreciation for user effort and input",
400    ],
401    forbidden_uses: &[
402        "Create false sense of reciprocal obligation",
403        "Guilt users into actions via 'reciprocity'",
404        "Exploit reciprocity norms for compliance",
405    ],
406    since: "0.1.0",
407    deprecated: None,
408};
409
410// ============================================================================
411// PREFERENCES AXES (4)
412// ============================================================================
413
414/// Desire for ceremonial gestures and pleasantries.
415pub const RITUAL_NEED: AxisDefinition = AxisDefinition {
416    name: "ritual_need",
417    category: AxisCategory::Preferences,
418    description: "Desire for ceremonial gestures, pleasantries, and social rituals",
419    low_anchor: "Skip pleasantries; get straight to the point",
420    high_anchor: "Values greetings, closings, and social niceties",
421    intent: &[
422        "Include or skip greeting rituals",
423        "Determine appropriate sign-off formality",
424        "Calibrate transitional pleasantries",
425    ],
426    forbidden_uses: &[
427        "Use rituals to waste time and extend engagement",
428        "Withhold ritual acknowledgment as punishment",
429        "Force rituals on users who skip them",
430    ],
431    since: "0.1.0",
432    deprecated: None,
433};
434
435/// Preference for transactional vs relational interaction.
436pub const TRANSACTIONAL_PREFERENCE: AxisDefinition = AxisDefinition {
437    name: "transactional_preference",
438    category: AxisCategory::Preferences,
439    description: "Preference for task-focused transactional vs relationship-building interaction",
440    low_anchor: "Relationship-oriented; values ongoing connection",
441    high_anchor: "Transaction-oriented; focused on immediate task",
442    intent: &[
443        "Adjust between building rapport and task efficiency",
444        "Determine appropriate personal context to include",
445        "Calibrate follow-up and continuity references",
446    ],
447    forbidden_uses: &[
448        "Force relationship-building on transactional users",
449        "Exploit relationship-orientation for lock-in",
450        "Dismiss transactional users as 'cold' or 'difficult'",
451    ],
452    since: "0.1.0",
453    deprecated: None,
454};
455
456/// Desired response length and detail level.
457pub const VERBOSITY_PREFERENCE: AxisDefinition = AxisDefinition {
458    name: "verbosity_preference",
459    category: AxisCategory::Preferences,
460    description: "Desired response length and level of detail",
461    low_anchor: "Brief, concise, minimal responses",
462    high_anchor: "Detailed, comprehensive, thorough responses",
463    intent: &[
464        "Adjust response length and completeness",
465        "Determine whether to elaborate or summarize",
466        "Calibrate example and explanation depth",
467    ],
468    forbidden_uses: &[
469        "Use excessive verbosity to overwhelm or confuse",
470        "Hide important information in verbosity",
471        "Withhold detail to force follow-up engagement",
472    ],
473    since: "0.1.0",
474    deprecated: None,
475};
476
477/// Preference for direct vs indirect communication.
478pub const DIRECTNESS_PREFERENCE: AxisDefinition = AxisDefinition {
479    name: "directness_preference",
480    category: AxisCategory::Preferences,
481    description: "Preference for direct vs diplomatic communication style",
482    low_anchor: "Indirect, diplomatic, cushioned",
483    high_anchor: "Direct, straightforward, unvarnished",
484    intent: &[
485        "Adjust hedging and softening language",
486        "Calibrate directness of recommendations",
487        "Determine bluntness of negative feedback",
488    ],
489    forbidden_uses: &[
490        "Use directness as excuse for rudeness",
491        "Exploit indirect preference to avoid honest answers",
492        "Weaponize directness to intimidate",
493    ],
494    since: "0.1.0",
495    deprecated: None,
496};
497
498// ============================================================================
499// CONTROL AXES (4)
500// ============================================================================
501
502/// Desire for control over outcomes.
503pub const AUTONOMY_PREFERENCE: AxisDefinition = AxisDefinition {
504    name: "autonomy_preference",
505    category: AxisCategory::Control,
506    description: "Desire for self-direction and control over outcomes",
507    low_anchor: "Open to guidance and external direction",
508    high_anchor: "Strong preference for self-direction and control",
509    intent: &[
510        "Adjust between guiding and following",
511        "Present options vs make recommendations",
512        "Calibrate how much to 'take over' vs 'support'",
513    ],
514    forbidden_uses: &[
515        "Override autonomy 'for user's own good'",
516        "Exploit low autonomy preference for manipulation",
517        "Punish high autonomy with reduced support",
518    ],
519    since: "0.1.0",
520    deprecated: None,
521};
522
523/// Openness to unsolicited suggestions.
524pub const SUGGESTION_TOLERANCE: AxisDefinition = AxisDefinition {
525    name: "suggestion_tolerance",
526    category: AxisCategory::Control,
527    description: "Openness to unsolicited suggestions and proactive offers",
528    low_anchor: "Only respond to explicit requests",
529    high_anchor: "Welcome proactive suggestions and upsells",
530    intent: &[
531        "Determine whether to offer unrequested alternatives",
532        "Calibrate proactive recommendation frequency",
533        "Gate cross-sell and upsell behaviors",
534    ],
535    forbidden_uses: &[
536        "Ignore low tolerance and push suggestions anyway",
537        "Exploit high tolerance for aggressive upselling",
538        "Treat low tolerance as a challenge to overcome",
539    ],
540    since: "0.1.0",
541    deprecated: None,
542};
543
544/// Tolerance for being interrupted.
545pub const INTERRUPTION_TOLERANCE: AxisDefinition = AxisDefinition {
546    name: "interruption_tolerance",
547    category: AxisCategory::Control,
548    description: "Tolerance for interruptions, interjections, and notifications",
549    low_anchor: "Do not interrupt; wait until finished",
550    high_anchor: "Interruptions and interjections are acceptable",
551    intent: &[
552        "Determine whether to interject with corrections",
553        "Calibrate notification and alert frequency",
554        "Decide when to interrupt with urgent updates",
555    ],
556    forbidden_uses: &[
557        "Interrupt to break user's train of thought strategically",
558        "Exploit tolerance for attention hijacking",
559        "Use interruptions to create urgency",
560    ],
561    since: "0.1.0",
562    deprecated: None,
563};
564
565/// Preference for thinking vs immediate action.
566pub const REFLECTION_VS_ACTION_BIAS: AxisDefinition = AxisDefinition {
567    name: "reflection_vs_action_bias",
568    category: AxisCategory::Control,
569    description: "Preference for deliberation vs immediate action",
570    low_anchor: "Action-oriented; minimize deliberation; just do it",
571    high_anchor: "Reflection-oriented; think carefully before acting",
572    intent: &[
573        "Adjust between 'do it now' and 'let's think about it'",
574        "Calibrate pros/cons presentation depth",
575        "Determine whether to offer 'undo' vs 'confirm' patterns",
576    ],
577    forbidden_uses: &[
578        "Exploit action bias to skip important evaluation",
579        "Frustrate reflective users with action pressure",
580        "Use reflection time as an opportunity to insert persuasion",
581    ],
582    since: "0.1.0",
583    deprecated: None,
584};
585
586// ============================================================================
587// SAFETY AXES (2)
588// ============================================================================
589
590/// Perceived importance of current decisions.
591pub const STAKES_AWARENESS: AxisDefinition = AxisDefinition {
592    name: "stakes_awareness",
593    category: AxisCategory::Safety,
594    description: "Perceived importance and risk level of current decisions",
595    low_anchor: "Low stakes; casual; easily reversible",
596    high_anchor: "High stakes; consequential; careful handling needed",
597    intent: &[
598        "Add confirmation steps for high-stakes actions",
599        "Emphasize reversibility and guarantees when stakes are high",
600        "Adjust error tolerance and verification depth",
601    ],
602    forbidden_uses: &[
603        "Artificially inflate stakes to create pressure",
604        "Exploit low stakes awareness to sneak in consequential changes",
605        "Dismiss high stakes concerns as overreaction",
606    ],
607    since: "0.1.0",
608    deprecated: None,
609};
610
611/// Concern about information privacy.
612pub const PRIVACY_SENSITIVITY: AxisDefinition = AxisDefinition {
613    name: "privacy_sensitivity",
614    category: AxisCategory::Safety,
615    description: "Concern about information privacy and data minimization",
616    low_anchor: "Low privacy concern; sharing is fine",
617    high_anchor: "High privacy concern; minimize data collection",
618    intent: &[
619        "Minimize data collection when sensitivity is high",
620        "Offer privacy-preserving alternatives",
621        "Be explicit about data usage",
622    ],
623    forbidden_uses: &[
624        "Exploit low sensitivity for excessive data collection",
625        "Dismiss privacy concerns as paranoia",
626        "Condition service quality on privacy concessions",
627    ],
628    since: "0.1.0",
629    deprecated: None,
630};
631
632// ============================================================================
633// CANONICAL AXES COLLECTION
634// ============================================================================
635
636/// All canonical axes for Attuned (23 axes across 6 categories).
637///
638/// These axes are:
639/// - **Immutable after v1.0**: Names and core semantics are frozen
640/// - **Governed**: Each has explicit intent and forbidden uses
641/// - **Interpretable**: Designed for human legibility and override
642///
643/// # Categories
644///
645/// - **Cognitive (4)**: Mental processing and decision-making
646/// - **Emotional (4)**: Emotional state and needs
647/// - **Social (5)**: Interpersonal interaction style
648/// - **Preferences (4)**: Communication format preferences
649/// - **Control (4)**: Agency and autonomy preferences
650/// - **Safety (2)**: Risk and privacy concerns
651pub static CANONICAL_AXES: &[AxisDefinition] = &[
652    // Cognitive (4)
653    COGNITIVE_LOAD,
654    DECISION_FATIGUE,
655    TOLERANCE_FOR_COMPLEXITY,
656    URGENCY_SENSITIVITY,
657    // Emotional (4)
658    EMOTIONAL_OPENNESS,
659    EMOTIONAL_STABILITY,
660    ANXIETY_LEVEL,
661    NEED_FOR_REASSURANCE,
662    // Social (5)
663    WARMTH,
664    FORMALITY,
665    BOUNDARY_STRENGTH,
666    ASSERTIVENESS,
667    RECIPROCITY_EXPECTATION,
668    // Preferences (4)
669    RITUAL_NEED,
670    TRANSACTIONAL_PREFERENCE,
671    VERBOSITY_PREFERENCE,
672    DIRECTNESS_PREFERENCE,
673    // Control (4)
674    AUTONOMY_PREFERENCE,
675    SUGGESTION_TOLERANCE,
676    INTERRUPTION_TOLERANCE,
677    REFLECTION_VS_ACTION_BIAS,
678    // Safety (2)
679    STAKES_AWARENESS,
680    PRIVACY_SENSITIVITY,
681];
682
683/// Get an axis definition by name.
684pub fn get_axis(name: &str) -> Option<&'static AxisDefinition> {
685    CANONICAL_AXES.iter().find(|a| a.name == name)
686}
687
688/// Validate that an axis name follows naming conventions.
689///
690/// Valid names:
691/// - 1-64 characters
692/// - Lowercase alphanumeric and underscores only
693/// - Cannot start or end with underscore
694pub fn is_valid_axis_name(name: &str) -> bool {
695    !name.is_empty()
696        && name.len() <= 64
697        && name
698            .chars()
699            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
700        && !name.starts_with('_')
701        && !name.ends_with('_')
702}
703
704#[cfg(test)]
705mod tests {
706    use super::*;
707
708    #[test]
709    fn test_canonical_axes_count() {
710        // 4 cognitive + 4 emotional + 5 social + 4 preferences + 4 control + 2 safety = 23
711        assert_eq!(CANONICAL_AXES.len(), 23);
712    }
713
714    #[test]
715    fn test_all_axes_have_forbidden_uses() {
716        for axis in CANONICAL_AXES {
717            assert!(
718                !axis.forbidden_uses.is_empty(),
719                "Axis '{}' must have at least one forbidden use",
720                axis.name
721            );
722        }
723    }
724
725    #[test]
726    fn test_all_axes_have_intent() {
727        for axis in CANONICAL_AXES {
728            assert!(
729                !axis.intent.is_empty(),
730                "Axis '{}' must have at least one intent",
731                axis.name
732            );
733        }
734    }
735
736    #[test]
737    fn test_all_axes_have_valid_names() {
738        for axis in CANONICAL_AXES {
739            assert!(
740                is_valid_axis_name(axis.name),
741                "Axis '{}' has invalid name",
742                axis.name
743            );
744        }
745    }
746
747    #[test]
748    fn test_get_axis() {
749        let axis = get_axis("cognitive_load");
750        assert!(axis.is_some());
751        assert_eq!(axis.unwrap().category, AxisCategory::Cognitive);
752
753        let missing = get_axis("nonexistent");
754        assert!(missing.is_none());
755    }
756
757    #[test]
758    fn test_categories_correct() {
759        let cognitive: Vec<_> = CANONICAL_AXES
760            .iter()
761            .filter(|a| a.category == AxisCategory::Cognitive)
762            .collect();
763        assert_eq!(cognitive.len(), 4);
764
765        let emotional: Vec<_> = CANONICAL_AXES
766            .iter()
767            .filter(|a| a.category == AxisCategory::Emotional)
768            .collect();
769        assert_eq!(emotional.len(), 4);
770
771        let social: Vec<_> = CANONICAL_AXES
772            .iter()
773            .filter(|a| a.category == AxisCategory::Social)
774            .collect();
775        assert_eq!(social.len(), 5);
776
777        let preferences: Vec<_> = CANONICAL_AXES
778            .iter()
779            .filter(|a| a.category == AxisCategory::Preferences)
780            .collect();
781        assert_eq!(preferences.len(), 4);
782
783        let control: Vec<_> = CANONICAL_AXES
784            .iter()
785            .filter(|a| a.category == AxisCategory::Control)
786            .collect();
787        assert_eq!(control.len(), 4);
788
789        let safety: Vec<_> = CANONICAL_AXES
790            .iter()
791            .filter(|a| a.category == AxisCategory::Safety)
792            .collect();
793        assert_eq!(safety.len(), 2);
794    }
795
796    #[test]
797    fn test_valid_axis_names() {
798        assert!(is_valid_axis_name("cognitive_load"));
799        assert!(is_valid_axis_name("warmth"));
800        assert!(is_valid_axis_name("axis123"));
801    }
802
803    #[test]
804    fn test_invalid_axis_names() {
805        assert!(!is_valid_axis_name(""));
806        assert!(!is_valid_axis_name("_starts_underscore"));
807        assert!(!is_valid_axis_name("ends_underscore_"));
808        assert!(!is_valid_axis_name("has spaces"));
809        assert!(!is_valid_axis_name("UPPERCASE"));
810        assert!(!is_valid_axis_name("has-dashes"));
811    }
812}