mecha10_cli/types/
project.rs

1//! Project configuration types
2//!
3//! Types for loading and managing mecha10.json project configuration files.
4
5use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9#[derive(Debug, Serialize, Deserialize)]
10pub struct ProjectConfig {
11    pub name: String,
12    pub version: String,
13    pub robot: RobotConfig,
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub simulation: Option<ProjectSimulationConfig>,
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub lifecycle: Option<LifecycleConfig>,
18    #[serde(default)]
19    pub nodes: NodesConfig,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub behaviors: Option<BehaviorsConfig>,
22    #[serde(default)]
23    pub services: ServicesConfig,
24    #[serde(default)]
25    pub docker: DockerServicesConfig,
26    #[serde(default)]
27    pub redis: RedisConfig,
28    #[serde(default)]
29    pub dashboard: DashboardConfig,
30    #[serde(default)]
31    pub networking: NetworkingConfig,
32    #[serde(default)]
33    pub environments: EnvironmentsConfig,
34}
35
36// ============================================================================
37// Node Configuration Types
38// ============================================================================
39
40/// Node source type based on namespace prefix
41#[derive(Debug, Clone, PartialEq)]
42pub enum NodeSource {
43    /// Framework node bundled in CLI (@mecha10/*)
44    Framework,
45    /// Local project node (@local/*)
46    Project,
47    /// Third-party registry node (@<org>/*)
48    Registry(String),
49}
50
51/// Parsed node specification from namespaced string format
52///
53/// Node identifiers follow this format:
54/// - `@mecha10/listener` - Framework node (bundled in CLI)
55/// - `@local/my-custom` - Project node (built from local workspace)
56/// - `@someorg/cool-node` - Registry node (future, from npm-like registry)
57///
58/// Config paths follow the identifier:
59/// - `configs/nodes/@mecha10/listener/config.json`
60/// - `configs/nodes/@local/my-custom/config.json`
61#[derive(Debug, Clone)]
62pub struct NodeSpec {
63    /// Node name (derived from identifier, used in lifecycle modes)
64    pub name: String,
65    /// Full namespaced identifier
66    #[allow(dead_code)] // API for future use (registry lookups)
67    pub identifier: String,
68    /// Node source type
69    pub source: NodeSource,
70}
71
72impl NodeSpec {
73    /// Parse a node identifier string into a NodeSpec
74    ///
75    /// # Examples
76    ///
77    /// ```
78    /// let spec = NodeSpec::parse("@mecha10/listener").unwrap();
79    /// assert_eq!(spec.name, "listener");
80    /// assert_eq!(spec.source, NodeSource::Framework);
81    ///
82    /// let spec = NodeSpec::parse("@local/my-custom").unwrap();
83    /// assert_eq!(spec.name, "my-custom");
84    /// assert_eq!(spec.source, NodeSource::Project);
85    /// ```
86    pub fn parse(identifier: &str) -> Result<Self> {
87        let identifier = identifier.trim();
88
89        if !identifier.starts_with('@') {
90            anyhow::bail!(
91                "Invalid node identifier '{}'. Expected @mecha10/<name>, @local/<name>, or @<org>/<name>",
92                identifier
93            );
94        }
95
96        let without_at = identifier.strip_prefix('@').unwrap();
97        let parts: Vec<&str> = without_at.splitn(2, '/').collect();
98
99        if parts.len() != 2 {
100            anyhow::bail!(
101                "Invalid node identifier '{}'. Expected @<scope>/<name> format",
102                identifier
103            );
104        }
105
106        let scope = parts[0];
107        let name = parts[1].to_string();
108
109        if scope.is_empty() || name.is_empty() {
110            anyhow::bail!("Empty scope or name in identifier: {}", identifier);
111        }
112
113        let source = match scope {
114            "mecha10" => NodeSource::Framework,
115            "local" => NodeSource::Project,
116            org => NodeSource::Registry(org.to_string()),
117        };
118
119        Ok(Self {
120            name,
121            identifier: identifier.to_string(),
122            source,
123        })
124    }
125
126    /// Check if this is a framework node
127    pub fn is_framework(&self) -> bool {
128        matches!(self.source, NodeSource::Framework)
129    }
130
131    /// Check if this is a project node
132    #[allow(dead_code)] // API for future use
133    pub fn is_project(&self) -> bool {
134        matches!(self.source, NodeSource::Project)
135    }
136
137    /// Get the package/crate path for this node
138    ///
139    /// - Framework: mecha10-nodes-{name}
140    /// - Project: nodes/{name}
141    /// - Registry: node_modules/@{org}/{name} (future)
142    pub fn package_path(&self) -> String {
143        match &self.source {
144            NodeSource::Framework => format!("mecha10-nodes-{}", self.name),
145            NodeSource::Project => format!("nodes/{}", self.name),
146            NodeSource::Registry(org) => format!("node_modules/@{}/{}", org, self.name),
147        }
148    }
149
150    /// Get the config directory path for this node (relative to project root)
151    ///
152    /// Config path follows the identifier structure:
153    /// - `@mecha10/listener` → `configs/nodes/@mecha10/listener/`
154    /// - `@local/my-custom` → `configs/nodes/@local/my-custom/`
155    /// - `@someorg/node` → `configs/nodes/@someorg/node/`
156    #[allow(dead_code)] // API for future use
157    pub fn config_dir(&self) -> String {
158        format!("configs/nodes/{}", self.identifier)
159    }
160}
161
162/// Nodes configuration - array of namespaced node identifiers
163///
164/// Node identifiers follow this format:
165/// - `@mecha10/listener` - Framework node (bundled in CLI)
166/// - `@local/my-custom` - Project node (built from local workspace)
167/// - `@someorg/cool-node` - Registry node (future, from npm-like registry)
168///
169/// Example:
170/// ```json
171/// {
172///   "nodes": [
173///     "@mecha10/listener",
174///     "@mecha10/speaker",
175///     "@local/my-custom"
176///   ]
177/// }
178/// ```
179#[derive(Debug, Serialize, Deserialize, Default, Clone)]
180#[serde(transparent)]
181pub struct NodesConfig(pub Vec<String>);
182
183impl NodesConfig {
184    /// Create a new empty nodes config
185    #[allow(dead_code)] // API for future use
186    pub fn new() -> Self {
187        Self(Vec::new())
188    }
189
190    /// Get all node specs from the configuration
191    pub fn get_node_specs(&self) -> Vec<NodeSpec> {
192        self.0.iter().filter_map(|id| NodeSpec::parse(id).ok()).collect()
193    }
194
195    /// Get all node names (for lifecycle mode validation)
196    pub fn get_node_names(&self) -> Vec<String> {
197        self.get_node_specs().iter().map(|s| s.name.clone()).collect()
198    }
199
200    /// Find a node by name
201    pub fn find_by_name(&self, name: &str) -> Option<NodeSpec> {
202        self.get_node_specs().into_iter().find(|s| s.name == name)
203    }
204
205    /// Check if a node exists
206    pub fn contains(&self, name: &str) -> bool {
207        self.find_by_name(name).is_some()
208    }
209
210    /// Add a node to the configuration
211    pub fn add_node(&mut self, identifier: &str) {
212        if !self.0.contains(&identifier.to_string()) {
213            self.0.push(identifier.to_string());
214        }
215    }
216
217    /// Remove a node from the configuration
218    #[allow(dead_code)] // API for future use
219    pub fn remove_node(&mut self, name: &str) {
220        self.0
221            .retain(|id| NodeSpec::parse(id).map(|s| s.name != name).unwrap_or(true));
222    }
223
224    /// Get the raw list of identifiers
225    #[allow(dead_code)] // API for future use
226    pub fn identifiers(&self) -> &[String] {
227        &self.0
228    }
229
230    /// Check if empty
231    #[allow(dead_code)] // API for future use
232    pub fn is_empty(&self) -> bool {
233        self.0.is_empty()
234    }
235
236    /// Get count of nodes
237    #[allow(dead_code)] // API for future use
238    pub fn len(&self) -> usize {
239        self.0.len()
240    }
241}
242
243/// Behavior tree configuration
244///
245/// Controls behavior tree execution for autonomous robot behaviors.
246#[derive(Debug, Serialize, Deserialize, Clone)]
247pub struct BehaviorsConfig {
248    /// Name of the active behavior to run (e.g., "idle_wander")
249    pub active: String,
250
251    /// Directory containing behavior tree templates (relative to project root)
252    #[serde(default = "default_behaviors_templates_dir")]
253    pub templates_dir: String,
254
255    /// Directory containing behavior configuration files (relative to project root)
256    #[serde(default = "default_behaviors_configs_dir")]
257    pub configs_dir: String,
258
259    /// Executor configuration
260    #[serde(default)]
261    pub executor: BehaviorExecutorConfig,
262}
263
264/// Behavior executor configuration
265///
266/// Controls how the behavior tree executor runs.
267#[derive(Debug, Serialize, Deserialize, Clone)]
268pub struct BehaviorExecutorConfig {
269    /// Tick rate in Hz (how many times per second to tick the behavior tree)
270    #[serde(default = "default_tick_rate_hz")]
271    pub tick_rate_hz: f32,
272
273    /// Maximum number of ticks before stopping (None = run forever)
274    #[serde(default)]
275    pub max_ticks: Option<usize>,
276
277    /// Whether to log execution statistics
278    #[serde(default = "default_log_stats")]
279    pub log_stats: bool,
280}
281
282impl Default for BehaviorExecutorConfig {
283    fn default() -> Self {
284        Self {
285            tick_rate_hz: default_tick_rate_hz(),
286            max_ticks: None,
287            log_stats: default_log_stats(),
288        }
289    }
290}
291
292fn default_behaviors_templates_dir() -> String {
293    "behaviors".to_string()
294}
295
296fn default_behaviors_configs_dir() -> String {
297    "configs".to_string()
298}
299
300fn default_tick_rate_hz() -> f32 {
301    10.0
302}
303
304fn default_log_stats() -> bool {
305    true
306}
307
308#[derive(Debug, Serialize, Deserialize, Default)]
309pub struct ServicesConfig {
310    #[serde(default)]
311    pub http_api: Option<HttpApiServiceConfig>,
312    #[serde(default)]
313    pub database: Option<DatabaseServiceConfig>,
314    #[serde(default)]
315    pub job_processor: Option<JobProcessorServiceConfig>,
316    #[serde(default)]
317    pub scheduler: Option<SchedulerServiceConfig>,
318}
319
320#[derive(Debug, Serialize, Deserialize, Clone)]
321pub struct HttpApiServiceConfig {
322    pub host: String,
323    pub port: u16,
324    #[serde(default = "default_enable_cors")]
325    pub _enable_cors: bool,
326}
327
328#[derive(Debug, Serialize, Deserialize, Clone)]
329pub struct DatabaseServiceConfig {
330    pub url: String,
331    #[serde(default = "default_max_connections")]
332    pub max_connections: u32,
333    #[serde(default = "default_timeout_seconds")]
334    pub timeout_seconds: u64,
335}
336
337#[derive(Debug, Serialize, Deserialize, Clone)]
338pub struct JobProcessorServiceConfig {
339    #[serde(default = "default_worker_count")]
340    pub worker_count: usize,
341    #[serde(default = "default_max_queue_size")]
342    pub max_queue_size: usize,
343}
344
345#[derive(Debug, Serialize, Deserialize, Clone)]
346pub struct SchedulerServiceConfig {
347    pub tasks: Vec<ScheduledTaskConfig>,
348}
349
350#[derive(Debug, Serialize, Deserialize, Clone)]
351#[allow(dead_code)]
352pub struct ScheduledTaskConfig {
353    pub name: String,
354    pub cron: String,
355    pub topic: String,
356    pub payload: String,
357}
358
359#[derive(Debug, Serialize, Deserialize)]
360pub struct RobotConfig {
361    pub id: String,
362    #[serde(default)]
363    pub platform: Option<String>,
364    #[serde(default)]
365    pub description: Option<String>,
366}
367
368/// Project-level simulation configuration stored in mecha10.json
369///
370/// This controls simulation settings for the project, distinct from
371/// the runtime simulation config loaded from configs/{profile}/simulation/.
372#[derive(Debug, Serialize, Deserialize, Clone)]
373pub struct ProjectSimulationConfig {
374    /// Whether simulation is enabled for this project
375    #[serde(default = "default_simulation_enabled")]
376    pub enabled: bool,
377
378    /// Config profile to use (dev, production, staging, etc.)
379    #[serde(default = "default_config_profile")]
380    pub config_profile: String,
381
382    /// Optional scenario name (loads configs/{profile}/simulation/{scenario}.json)
383    /// If not specified, loads configs/{profile}/simulation/config.json
384    #[serde(default, skip_serializing_if = "Option::is_none")]
385    pub scenario: Option<String>,
386
387    // DEPRECATED: Legacy fields for backwards compatibility
388    // These are ignored if config_profile is set
389    #[serde(default, skip_serializing_if = "Option::is_none")]
390    pub model: Option<String>,
391    #[serde(default, skip_serializing_if = "Option::is_none")]
392    pub model_config: Option<String>,
393    #[serde(default, skip_serializing_if = "Option::is_none")]
394    pub environment: Option<String>,
395    #[serde(default, skip_serializing_if = "Option::is_none")]
396    pub environment_config: Option<String>,
397}
398
399fn default_simulation_enabled() -> bool {
400    true
401}
402
403fn default_config_profile() -> String {
404    "dev".to_string()
405}
406
407/// Lifecycle configuration for mode-based node management
408///
409/// Enables dynamic node lifecycle based on operational modes.
410/// Each mode defines which nodes should run in that context.
411#[derive(Debug, Serialize, Deserialize, Clone)]
412pub struct LifecycleConfig {
413    /// Available operational modes
414    pub modes: std::collections::HashMap<String, ModeConfig>,
415
416    /// Default mode on startup
417    #[serde(default = "default_lifecycle_mode")]
418    pub default_mode: String,
419}
420
421/// Configuration for a single operational mode
422///
423/// Defines which nodes should run in this mode and which should stop.
424#[derive(Debug, Serialize, Deserialize, Clone)]
425pub struct ModeConfig {
426    /// Human-readable description of this mode
427    #[serde(default, skip_serializing_if = "Option::is_none")]
428    pub description: Option<String>,
429
430    /// Nodes that should run in this mode
431    pub nodes: Vec<String>,
432
433    /// Nodes that should be stopped when entering this mode (optional)
434    /// This is useful for explicitly stopping nodes that conflict with this mode.
435    #[serde(default)]
436    pub stop_nodes: Vec<String>,
437}
438
439fn default_lifecycle_mode() -> String {
440    "startup".to_string()
441}
442
443// Validation for lifecycle configuration
444impl LifecycleConfig {
445    /// Validate lifecycle configuration against available nodes
446    ///
447    /// # Arguments
448    ///
449    /// * `available_nodes` - List of node names defined in project config
450    ///
451    /// # Errors
452    ///
453    /// Returns error if:
454    /// - Default mode doesn't exist in modes
455    /// - Any mode references undefined nodes
456    #[allow(dead_code)] // Will be used in Phase 3
457    pub fn validate(&self, available_nodes: &[String]) -> Result<()> {
458        // Check that default mode exists
459        if !self.modes.contains_key(&self.default_mode) {
460            return Err(anyhow::anyhow!(
461                "Default mode '{}' not found in modes. Available modes: {}",
462                self.default_mode,
463                self.modes.keys().map(|k| k.as_str()).collect::<Vec<_>>().join(", ")
464            ));
465        }
466
467        // Validate each mode
468        for (mode_name, mode_config) in &self.modes {
469            mode_config.validate(mode_name, available_nodes)?;
470        }
471
472        Ok(())
473    }
474}
475
476impl ModeConfig {
477    /// Validate mode configuration against available nodes
478    ///
479    /// # Arguments
480    ///
481    /// * `mode_name` - Name of this mode (for error messages)
482    /// * `available_nodes` - List of node names defined in project config
483    ///
484    /// # Errors
485    ///
486    /// Returns error if any referenced node doesn't exist in project config
487    #[allow(dead_code)] // Will be used in Phase 3
488    pub fn validate(&self, mode_name: &str, available_nodes: &[String]) -> Result<()> {
489        // Check all nodes exist
490        for node in &self.nodes {
491            if !available_nodes.contains(node) {
492                return Err(anyhow::anyhow!(
493                    "Mode '{}': node '{}' not found in project configuration. Available nodes: {}",
494                    mode_name,
495                    node,
496                    available_nodes.join(", ")
497                ));
498            }
499        }
500
501        // Check stop_nodes exist
502        for node in &self.stop_nodes {
503            if !available_nodes.contains(node) {
504                return Err(anyhow::anyhow!(
505                    "Mode '{}': stop_node '{}' not found in project configuration. Available nodes: {}",
506                    mode_name,
507                    node,
508                    available_nodes.join(", ")
509                ));
510            }
511        }
512
513        Ok(())
514    }
515}
516
517#[derive(Debug, Serialize, Deserialize)]
518pub struct RedisConfig {
519    #[serde(default = "default_redis_url")]
520    pub url: String,
521}
522
523/// Docker services configuration for project-level Docker Compose management
524#[derive(Debug, Serialize, Deserialize, Default, Clone)]
525pub struct DockerServicesConfig {
526    /// Services needed for on-robot execution (typically just Redis for message passing)
527    #[serde(default)]
528    pub robot: Vec<String>,
529
530    /// Services needed for edge/remote execution (Redis + databases, etc.)
531    #[serde(default)]
532    pub edge: Vec<String>,
533
534    /// Path to docker-compose.yml relative to project root
535    #[serde(default = "default_compose_file")]
536    pub compose_file: String,
537
538    /// Whether to auto-start services in dev mode
539    #[serde(default = "default_auto_start")]
540    pub auto_start: bool,
541}
542
543impl Default for RedisConfig {
544    fn default() -> Self {
545        Self {
546            url: default_redis_url(),
547        }
548    }
549}
550
551fn default_redis_url() -> String {
552    "redis://localhost:6379".to_string()
553}
554
555/// Dashboard configuration
556///
557/// Controls the dashboard URL for the project.
558/// Can be overridden via MECHA10_DASHBOARD_URL environment variable.
559#[derive(Debug, Serialize, Deserialize, Clone)]
560pub struct DashboardConfig {
561    /// Dashboard URL
562    /// Default: https://dashboard.mecha.industries
563    /// Override with MECHA10_DASHBOARD_URL env var for local development
564    #[serde(default = "default_dashboard_url")]
565    pub url: String,
566}
567
568impl Default for DashboardConfig {
569    fn default() -> Self {
570        Self {
571            url: default_dashboard_url(),
572        }
573    }
574}
575
576impl DashboardConfig {
577    /// Get the effective dashboard URL
578    ///
579    /// Priority:
580    /// 1. MECHA10_DASHBOARD_URL environment variable
581    /// 2. Config file value
582    /// 3. Default (https://dashboard.mecha.industries)
583    pub fn effective_url(&self) -> String {
584        std::env::var("MECHA10_DASHBOARD_URL").unwrap_or_else(|_| self.url.clone())
585    }
586}
587
588fn default_dashboard_url() -> String {
589    "https://dashboard.mecha.industries".to_string()
590}
591
592/// Networking configuration for WebRTC relay and signaling
593///
594/// Centralizes relay URL configuration for simulation-bridge and other nodes.
595#[derive(Debug, Serialize, Deserialize, Clone, Default)]
596pub struct NetworkingConfig {
597    /// WebRTC relay configuration
598    #[serde(default)]
599    pub relay: RelayConfig,
600}
601
602/// WebRTC relay configuration
603///
604/// The relay URL is used by simulation-bridge in client mode to connect
605/// to a remote signaling server for WebRTC camera streaming.
606#[derive(Debug, Serialize, Deserialize, Clone)]
607pub struct RelayConfig {
608    /// WebRTC relay URL for signaling
609    /// Default: wss://api.mecha.industries/webrtc-relay
610    /// Override with WEBRTC_RELAY_URL env var
611    #[serde(default = "default_relay_url")]
612    pub url: String,
613}
614
615impl Default for RelayConfig {
616    fn default() -> Self {
617        Self {
618            url: default_relay_url(),
619        }
620    }
621}
622
623impl RelayConfig {
624    /// Get the effective relay URL
625    ///
626    /// Priority:
627    /// 1. WEBRTC_RELAY_URL environment variable
628    /// 2. Config file value
629    /// 3. Default (wss://api.mecha.industries/webrtc-relay)
630    pub fn effective_url(&self) -> String {
631        std::env::var("WEBRTC_RELAY_URL").unwrap_or_else(|_| self.url.clone())
632    }
633}
634
635fn default_relay_url() -> String {
636    "wss://api.mecha.industries/webrtc-relay".to_string()
637}
638
639/// Environment-specific configuration
640///
641/// Allows configuring different URLs for dev, staging, and production environments.
642/// The active environment is determined by:
643/// 1. `MECHA10_ENV` environment variable
644/// 2. `--env` CLI flag
645/// 3. Default: "dev" when running `mecha10 dev`, "prod" otherwise
646#[derive(Debug, Serialize, Deserialize, Clone)]
647pub struct EnvironmentsConfig {
648    /// Development environment (local control plane)
649    #[serde(default = "default_dev_environment")]
650    pub dev: EnvironmentConfig,
651
652    /// Staging environment (optional)
653    #[serde(default, skip_serializing_if = "Option::is_none")]
654    pub staging: Option<EnvironmentConfig>,
655
656    /// Production environment (cloud control plane)
657    #[serde(default = "default_prod_environment")]
658    pub prod: EnvironmentConfig,
659}
660
661impl Default for EnvironmentsConfig {
662    fn default() -> Self {
663        Self {
664            dev: default_dev_environment(),
665            staging: None,
666            prod: default_prod_environment(),
667        }
668    }
669}
670
671impl EnvironmentsConfig {
672    /// Get the environment config for the given environment name
673    pub fn get(&self, env: &str) -> Option<&EnvironmentConfig> {
674        match env {
675            "dev" | "development" => Some(&self.dev),
676            "staging" => self.staging.as_ref(),
677            "prod" | "production" => Some(&self.prod),
678            _ => None,
679        }
680    }
681
682    /// Get the current environment config based on MECHA10_ENV
683    pub fn current(&self) -> &EnvironmentConfig {
684        let env = std::env::var("MECHA10_ENV").unwrap_or_else(|_| "dev".to_string());
685        self.get(&env).unwrap_or(&self.dev)
686    }
687
688    /// Get the control plane URL for the current environment
689    pub fn control_plane_url(&self) -> String {
690        std::env::var("MECHA10_CONTROL_PLANE_URL").unwrap_or_else(|_| self.current().control_plane.clone())
691    }
692}
693
694/// Configuration for a specific environment
695#[derive(Debug, Serialize, Deserialize, Clone)]
696pub struct EnvironmentConfig {
697    /// Control plane base URL (e.g., "ws://localhost:8100" or "wss://api.mecha.industries")
698    pub control_plane: String,
699}
700
701fn default_dev_environment() -> EnvironmentConfig {
702    EnvironmentConfig {
703        control_plane: "ws://localhost:8000".to_string(),
704    }
705}
706
707fn default_prod_environment() -> EnvironmentConfig {
708    EnvironmentConfig {
709        control_plane: "wss://api.mecha.industries".to_string(),
710    }
711}
712
713// Default value functions
714fn default_enable_cors() -> bool {
715    true
716}
717
718fn default_max_connections() -> u32 {
719    10
720}
721
722fn default_timeout_seconds() -> u64 {
723    30
724}
725
726fn default_worker_count() -> usize {
727    4
728}
729
730fn default_max_queue_size() -> usize {
731    100
732}
733
734fn default_compose_file() -> String {
735    "docker-compose.yml".to_string()
736}
737
738fn default_auto_start() -> bool {
739    true
740}
741
742/// Load robot_id from mecha10.json
743#[allow(dead_code)]
744pub async fn load_robot_id(config_path: &PathBuf) -> Result<String> {
745    let content = tokio::fs::read_to_string(config_path)
746        .await
747        .context("Failed to read mecha10.json")?;
748
749    let config: ProjectConfig = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
750
751    Ok(config.robot.id)
752}
753
754/// Load full project config from mecha10.json
755pub async fn load_project_config(config_path: &PathBuf) -> Result<ProjectConfig> {
756    let content = tokio::fs::read_to_string(config_path)
757        .await
758        .context("Failed to read mecha10.json")?;
759
760    let config: ProjectConfig = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
761
762    Ok(config)
763}