Skip to main content

driven/integration/
mod.rs

1//! Cross-Crate Integration Module
2//!
3//! This module provides integration between Driven, Generator, and DCP crates,
4//! enabling seamless cross-crate operations and shared binary format compatibility.
5//!
6//! ## Features
7//!
8//! - Driven tool registration in DCP
9//! - Spec scaffolding with Generator templates
10//! - Hook message passing via DCP
11//! - DX ∞ binary format compatibility
12//!
13//! ## Usage
14//!
15//! ```rust,ignore
16//! use driven::integration::{DrivenDcpBridge, SpecScaffolder, HookMessenger};
17//!
18//! // Register driven tools in DCP
19//! let mut bridge = DrivenDcpBridge::new(dcp_client);
20//! bridge.register_driven_tools()?;
21//!
22//! // Generate spec scaffolding
23//! let scaffolder = SpecScaffolder::new(generator_bridge);
24//! scaffolder.create_spec("feature-001", &params)?;
25//!
26//! // Send hook messages via DCP
27//! let messenger = HookMessenger::new(dcp_client);
28//! messenger.send_hook_message(&hook, &context)?;
29//! ```
30
31use crate::dcp::{DcpClient, DcpMessage, MessageType, ZctiBuilder};
32use crate::generator_integration::{GenerateParams, GeneratorBridge};
33use crate::hooks::{AgentHook, HookContext};
34use crate::{DrivenError, Result};
35use std::collections::HashMap;
36use std::path::{Path, PathBuf};
37
38// ==================== Driven-DCP Bridge ====================
39
40/// Bridge between Driven and DCP for tool registration and communication
41///
42/// Provides APIs for registering Driven tools in DCP and invoking them
43/// via the DCP protocol.
44#[derive(Debug)]
45pub struct DrivenDcpBridge {
46    /// Registered tool IDs mapped by name
47    tool_ids: HashMap<String, u32>,
48    /// Whether the bridge is initialized
49    initialized: bool,
50}
51
52/// Driven tool definitions for DCP registration
53#[derive(Debug, Clone)]
54pub struct DrivenTool {
55    /// Tool name
56    pub name: String,
57    /// Tool description
58    pub description: String,
59    /// Tool category
60    pub category: DrivenToolCategory,
61    /// Required capabilities (bitset)
62    pub capabilities: u64,
63}
64
65/// Categories of Driven tools
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum DrivenToolCategory {
68    /// Rule management tools
69    Rules,
70    /// Spec-driven development tools
71    Specs,
72    /// Hook management tools
73    Hooks,
74    /// Steering file tools
75    Steering,
76    /// Template tools
77    Templates,
78    /// Sync tools
79    Sync,
80}
81
82impl DrivenDcpBridge {
83    /// Create a new Driven-DCP bridge
84    pub fn new() -> Self {
85        Self {
86            tool_ids: HashMap::new(),
87            initialized: false,
88        }
89    }
90
91    /// Register all Driven tools in the DCP client
92    pub fn register_driven_tools(&mut self, client: &mut DcpClient) -> Result<()> {
93        let tools = self.get_driven_tools();
94
95        for tool in tools {
96            let schema_hash = self.compute_schema_hash(&tool);
97            let tool_id = client.register_tool(
98                &tool.name,
99                &tool.description,
100                schema_hash,
101                tool.capabilities,
102            );
103            self.tool_ids.insert(tool.name.clone(), tool_id);
104
105            // Also register with MCP adapter for compatibility
106            client.register_tool_mcp(&tool.name, tool_id);
107        }
108
109        self.initialized = true;
110        Ok(())
111    }
112
113    /// Get all Driven tool definitions
114    fn get_driven_tools(&self) -> Vec<DrivenTool> {
115        vec![
116            // Rule management tools
117            DrivenTool {
118                name: "driven.rules.sync".to_string(),
119                description: "Sync rules to all configured editors".to_string(),
120                category: DrivenToolCategory::Rules,
121                capabilities: 0x0001,
122            },
123            DrivenTool {
124                name: "driven.rules.convert".to_string(),
125                description: "Convert rules between formats".to_string(),
126                category: DrivenToolCategory::Rules,
127                capabilities: 0x0001,
128            },
129            DrivenTool {
130                name: "driven.rules.validate".to_string(),
131                description: "Validate rule syntax and semantics".to_string(),
132                category: DrivenToolCategory::Rules,
133                capabilities: 0x0001,
134            },
135            // Spec tools
136            DrivenTool {
137                name: "driven.spec.init".to_string(),
138                description: "Initialize a new specification".to_string(),
139                category: DrivenToolCategory::Specs,
140                capabilities: 0x0002,
141            },
142            DrivenTool {
143                name: "driven.spec.generate".to_string(),
144                description: "Generate spec artifacts from description".to_string(),
145                category: DrivenToolCategory::Specs,
146                capabilities: 0x0002,
147            },
148            DrivenTool {
149                name: "driven.spec.analyze".to_string(),
150                description: "Analyze spec for consistency".to_string(),
151                category: DrivenToolCategory::Specs,
152                capabilities: 0x0002,
153            },
154            // Hook tools
155            DrivenTool {
156                name: "driven.hooks.list".to_string(),
157                description: "List all registered hooks".to_string(),
158                category: DrivenToolCategory::Hooks,
159                capabilities: 0x0004,
160            },
161            DrivenTool {
162                name: "driven.hooks.trigger".to_string(),
163                description: "Manually trigger a hook".to_string(),
164                category: DrivenToolCategory::Hooks,
165                capabilities: 0x0004,
166            },
167            DrivenTool {
168                name: "driven.hooks.enable".to_string(),
169                description: "Enable a hook".to_string(),
170                category: DrivenToolCategory::Hooks,
171                capabilities: 0x0004,
172            },
173            DrivenTool {
174                name: "driven.hooks.disable".to_string(),
175                description: "Disable a hook".to_string(),
176                category: DrivenToolCategory::Hooks,
177                capabilities: 0x0004,
178            },
179            // Steering tools
180            DrivenTool {
181                name: "driven.steering.list".to_string(),
182                description: "List steering files".to_string(),
183                category: DrivenToolCategory::Steering,
184                capabilities: 0x0008,
185            },
186            DrivenTool {
187                name: "driven.steering.get".to_string(),
188                description: "Get steering content for context".to_string(),
189                category: DrivenToolCategory::Steering,
190                capabilities: 0x0008,
191            },
192            // Template tools
193            DrivenTool {
194                name: "driven.templates.list".to_string(),
195                description: "List available templates".to_string(),
196                category: DrivenToolCategory::Templates,
197                capabilities: 0x0010,
198            },
199            DrivenTool {
200                name: "driven.templates.apply".to_string(),
201                description: "Apply a template".to_string(),
202                category: DrivenToolCategory::Templates,
203                capabilities: 0x0010,
204            },
205        ]
206    }
207
208    /// Compute a schema hash for a tool (using Blake3)
209    fn compute_schema_hash(&self, tool: &DrivenTool) -> [u8; 32] {
210        use blake3::Hasher;
211        let mut hasher = Hasher::new();
212        hasher.update(tool.name.as_bytes());
213        hasher.update(tool.description.as_bytes());
214        hasher.update(&[tool.category as u8]);
215        *hasher.finalize().as_bytes()
216    }
217
218    /// Get tool ID by name
219    pub fn get_tool_id(&self, name: &str) -> Option<u32> {
220        self.tool_ids.get(name).copied()
221    }
222
223    /// Check if a tool is registered
224    pub fn has_tool(&self, name: &str) -> bool {
225        self.tool_ids.contains_key(name)
226    }
227
228    /// List all registered tool names
229    pub fn list_tools(&self) -> Vec<&str> {
230        self.tool_ids.keys().map(|s| s.as_str()).collect()
231    }
232
233    /// Create a ZCTI builder for a Driven tool
234    pub fn create_invocation(&self, tool_name: &str, client: &DcpClient) -> Result<ZctiBuilder> {
235        let tool_id = self
236            .get_tool_id(tool_name)
237            .ok_or_else(|| DrivenError::Config(format!("Tool not found: {}", tool_name)))?;
238        client.create_zcti_builder(tool_id)
239    }
240}
241
242impl Default for DrivenDcpBridge {
243    fn default() -> Self {
244        Self::new()
245    }
246}
247
248// ==================== Spec Scaffolder ====================
249
250/// Spec scaffolder that integrates with Generator for template-based scaffolding
251#[derive(Debug)]
252pub struct SpecScaffolder {
253    /// Generator bridge for template rendering
254    generator: GeneratorBridge,
255    /// Base directory for specs
256    spec_dir: PathBuf,
257    /// Auto-numbering counter
258    next_spec_number: u32,
259}
260
261impl SpecScaffolder {
262    /// Create a new spec scaffolder
263    pub fn new() -> Result<Self> {
264        Ok(Self {
265            generator: GeneratorBridge::new()?,
266            spec_dir: PathBuf::from(".driven/specs"),
267            next_spec_number: 1,
268        })
269    }
270
271    /// Create with custom spec directory
272    pub fn with_spec_dir(spec_dir: impl AsRef<Path>) -> Result<Self> {
273        Ok(Self {
274            generator: GeneratorBridge::new()?,
275            spec_dir: spec_dir.as_ref().to_path_buf(),
276            next_spec_number: 1,
277        })
278    }
279
280    /// Set the generator bridge
281    pub fn with_generator(mut self, generator: GeneratorBridge) -> Self {
282        self.generator = generator;
283        self
284    }
285
286    /// Initialize the scaffolder by scanning existing specs
287    pub fn initialize(&mut self) -> Result<()> {
288        self.generator.initialize()?;
289
290        // Scan for existing specs to determine next number
291        if self.spec_dir.exists() {
292            let mut max_num = 0u32;
293            for entry in std::fs::read_dir(&self.spec_dir).map_err(DrivenError::Io)? {
294                let entry = entry.map_err(DrivenError::Io)?;
295                if entry.path().is_dir() {
296                    if let Some(name) = entry.file_name().to_str() {
297                        // Try to parse leading number (e.g., "001-feature" -> 1)
298                        if let Some(num_str) = name.split('-').next() {
299                            if let Ok(num) = num_str.parse::<u32>() {
300                                max_num = max_num.max(num);
301                            }
302                        }
303                    }
304                }
305            }
306            self.next_spec_number = max_num + 1;
307        }
308
309        Ok(())
310    }
311
312    /// Create a new spec with auto-numbering
313    pub fn create_spec(
314        &mut self,
315        name: &str,
316        params: &GenerateParams,
317    ) -> Result<SpecScaffoldResult> {
318        let spec_id = format!("{:03}-{}", self.next_spec_number, name);
319        self.next_spec_number += 1;
320
321        self.create_spec_with_id(&spec_id, params)
322    }
323
324    /// Create a spec with a specific ID
325    pub fn create_spec_with_id(
326        &self,
327        spec_id: &str,
328        params: &GenerateParams,
329    ) -> Result<SpecScaffoldResult> {
330        // Generate scaffold files
331        let files = self.generator.generate_spec_scaffold(spec_id, params)?;
332
333        // Write files to disk
334        let written = self.generator.write_files(&files)?;
335        let written_count = written.len();
336        let files_skipped = files.len() - written_count;
337
338        Ok(SpecScaffoldResult {
339            spec_id: spec_id.to_string(),
340            spec_dir: self.spec_dir.join(spec_id),
341            files_created: written,
342            files_skipped,
343        })
344    }
345
346    /// Get the next spec number
347    pub fn next_spec_number(&self) -> u32 {
348        self.next_spec_number
349    }
350
351    /// Get the spec directory
352    pub fn spec_dir(&self) -> &Path {
353        &self.spec_dir
354    }
355}
356
357impl Default for SpecScaffolder {
358    fn default() -> Self {
359        Self::new().unwrap_or_else(|_| Self {
360            generator: GeneratorBridge::default(),
361            spec_dir: PathBuf::from(".driven/specs"),
362            next_spec_number: 1,
363        })
364    }
365}
366
367/// Result of spec scaffolding
368#[derive(Debug, Clone)]
369pub struct SpecScaffoldResult {
370    /// Spec ID
371    pub spec_id: String,
372    /// Spec directory path
373    pub spec_dir: PathBuf,
374    /// Files that were created
375    pub files_created: Vec<PathBuf>,
376    /// Number of files skipped (already existed)
377    pub files_skipped: usize,
378}
379
380// ==================== Hook Messenger ====================
381
382/// Hook messenger for sending hook messages via DCP
383#[derive(Debug)]
384pub struct HookMessenger {
385    /// Message queue for pending messages
386    message_queue: Vec<HookMessage>,
387    /// Whether to use DCP binary protocol
388    use_dcp: bool,
389}
390
391/// A hook message to be sent via DCP
392#[derive(Debug, Clone)]
393pub struct HookMessage {
394    /// Hook ID that triggered this message
395    pub hook_id: String,
396    /// Message content
397    pub content: String,
398    /// Context data
399    pub context: HashMap<String, String>,
400    /// Priority (lower = higher priority)
401    pub priority: u8,
402    /// Timestamp when the message was created
403    pub timestamp: u64,
404}
405
406impl HookMessenger {
407    /// Create a new hook messenger
408    pub fn new() -> Self {
409        Self {
410            message_queue: Vec::new(),
411            use_dcp: true,
412        }
413    }
414
415    /// Set whether to use DCP binary protocol
416    pub fn with_dcp(mut self, use_dcp: bool) -> Self {
417        self.use_dcp = use_dcp;
418        self
419    }
420
421    /// Create a message from a hook and context
422    pub fn create_message(&self, hook: &AgentHook, context: &HookContext) -> HookMessage {
423        let mut ctx_map = HashMap::new();
424
425        // Add context data
426        if let Some(file) = &context.file_path {
427            ctx_map.insert("file".to_string(), file.to_string_lossy().to_string());
428        }
429        if let Some(name) = &context.file_name {
430            ctx_map.insert("file_name".to_string(), name.clone());
431        }
432        if let Some(ext) = &context.file_ext {
433            ctx_map.insert("file_ext".to_string(), ext.clone());
434        }
435
436        // Add custom variables
437        for (k, v) in &context.variables {
438            ctx_map.insert(k.clone(), v.clone());
439        }
440
441        HookMessage {
442            hook_id: hook.id.clone(),
443            content: hook.action.message.clone(),
444            context: ctx_map,
445            priority: hook.priority,
446            timestamp: std::time::SystemTime::now()
447                .duration_since(std::time::UNIX_EPOCH)
448                .map(|d| d.as_secs())
449                .unwrap_or(0),
450        }
451    }
452
453    /// Queue a message for sending
454    pub fn queue_message(&mut self, message: HookMessage) {
455        self.message_queue.push(message);
456        // Sort by priority (lower = higher priority)
457        self.message_queue.sort_by_key(|m| m.priority);
458    }
459
460    /// Send a hook message via DCP
461    pub fn send_message(&self, _client: &DcpClient, message: &HookMessage) -> Result<DcpMessage> {
462        if self.use_dcp {
463            self.send_dcp_message(message)
464        } else {
465            self.send_mcp_message(message)
466        }
467    }
468
469    /// Send message using DCP binary protocol
470    fn send_dcp_message(&self, message: &HookMessage) -> Result<DcpMessage> {
471        // Serialize message to binary format
472        let payload = self.serialize_message(message)?;
473
474        // Create DCP message
475        Ok(DcpMessage::new(MessageType::Prompt, 0, payload))
476    }
477
478    /// Send message using MCP JSON-RPC protocol
479    fn send_mcp_message(&self, message: &HookMessage) -> Result<DcpMessage> {
480        // Create JSON payload
481        let json = serde_json::json!({
482            "hook_id": message.hook_id,
483            "content": message.content,
484            "context": message.context,
485            "priority": message.priority,
486            "timestamp": message.timestamp,
487        });
488
489        let payload = serde_json::to_vec(&json)
490            .map_err(|e| DrivenError::Format(format!("Failed to serialize: {}", e)))?;
491
492        Ok(DcpMessage::new(MessageType::Prompt, 0, payload))
493    }
494
495    /// Serialize a message to binary format
496    fn serialize_message(&self, message: &HookMessage) -> Result<Vec<u8>> {
497        // Simple binary format:
498        // - 4 bytes: hook_id length
499        // - N bytes: hook_id
500        // - 4 bytes: content length
501        // - N bytes: content
502        // - 4 bytes: context count
503        // - For each context entry:
504        //   - 4 bytes: key length
505        //   - N bytes: key
506        //   - 4 bytes: value length
507        //   - N bytes: value
508        // - 1 byte: priority
509        // - 8 bytes: timestamp
510
511        let mut buf = Vec::new();
512
513        // Hook ID
514        buf.extend_from_slice(&(message.hook_id.len() as u32).to_le_bytes());
515        buf.extend_from_slice(message.hook_id.as_bytes());
516
517        // Content
518        buf.extend_from_slice(&(message.content.len() as u32).to_le_bytes());
519        buf.extend_from_slice(message.content.as_bytes());
520
521        // Context
522        buf.extend_from_slice(&(message.context.len() as u32).to_le_bytes());
523        for (k, v) in &message.context {
524            buf.extend_from_slice(&(k.len() as u32).to_le_bytes());
525            buf.extend_from_slice(k.as_bytes());
526            buf.extend_from_slice(&(v.len() as u32).to_le_bytes());
527            buf.extend_from_slice(v.as_bytes());
528        }
529
530        // Priority and timestamp
531        buf.push(message.priority);
532        buf.extend_from_slice(&message.timestamp.to_le_bytes());
533
534        Ok(buf)
535    }
536
537    /// Deserialize a message from binary format
538    pub fn deserialize_message(&self, data: &[u8]) -> Result<HookMessage> {
539        let mut offset = 0;
540
541        // Helper to read u32
542        let read_u32 = |data: &[u8], offset: &mut usize| -> Result<u32> {
543            if *offset + 4 > data.len() {
544                return Err(DrivenError::InvalidBinary("Insufficient data".to_string()));
545            }
546            let bytes: [u8; 4] = data[*offset..*offset + 4].try_into().unwrap();
547            *offset += 4;
548            Ok(u32::from_le_bytes(bytes))
549        };
550
551        // Helper to read string
552        let read_string = |data: &[u8], offset: &mut usize| -> Result<String> {
553            let len = read_u32(data, offset)? as usize;
554            if *offset + len > data.len() {
555                return Err(DrivenError::InvalidBinary("Insufficient data".to_string()));
556            }
557            let s = std::str::from_utf8(&data[*offset..*offset + len])
558                .map_err(|e| DrivenError::InvalidBinary(format!("Invalid UTF-8: {}", e)))?
559                .to_string();
560            *offset += len;
561            Ok(s)
562        };
563
564        // Read hook_id
565        let hook_id = read_string(data, &mut offset)?;
566
567        // Read content
568        let content = read_string(data, &mut offset)?;
569
570        // Read context
571        let ctx_count = read_u32(data, &mut offset)? as usize;
572        let mut context = HashMap::new();
573        for _ in 0..ctx_count {
574            let key = read_string(data, &mut offset)?;
575            let value = read_string(data, &mut offset)?;
576            context.insert(key, value);
577        }
578
579        // Read priority
580        if offset >= data.len() {
581            return Err(DrivenError::InvalidBinary(
582                "Insufficient data for priority".to_string(),
583            ));
584        }
585        let priority = data[offset];
586        offset += 1;
587
588        // Read timestamp
589        if offset + 8 > data.len() {
590            return Err(DrivenError::InvalidBinary(
591                "Insufficient data for timestamp".to_string(),
592            ));
593        }
594        let timestamp_bytes: [u8; 8] = data[offset..offset + 8].try_into().unwrap();
595        let timestamp = u64::from_le_bytes(timestamp_bytes);
596
597        Ok(HookMessage {
598            hook_id,
599            content,
600            context,
601            priority,
602            timestamp,
603        })
604    }
605
606    /// Flush all queued messages
607    pub fn flush(&mut self, client: &DcpClient) -> Result<Vec<DcpMessage>> {
608        let messages: Vec<_> = self.message_queue.drain(..).collect();
609        let mut results = Vec::new();
610
611        for message in messages {
612            results.push(self.send_message(client, &message)?);
613        }
614
615        Ok(results)
616    }
617
618    /// Get the number of queued messages
619    pub fn queue_len(&self) -> usize {
620        self.message_queue.len()
621    }
622
623    /// Clear the message queue
624    pub fn clear_queue(&mut self) {
625        self.message_queue.clear();
626    }
627}
628
629impl Default for HookMessenger {
630    fn default() -> Self {
631        Self::new()
632    }
633}
634
635// ==================== Binary Format Compatibility ====================
636
637/// Binary format compatibility checker for DX ∞ format
638///
639/// Ensures that binary data can be correctly serialized and deserialized
640/// across different crates (Driven, Generator, DCP).
641#[derive(Debug)]
642pub struct BinaryFormatChecker {
643    /// Supported format versions
644    supported_versions: Vec<u16>,
645}
646
647/// Binary format header for cross-crate compatibility
648#[repr(C)]
649#[derive(Debug, Clone, Copy)]
650pub struct CrossCrateHeader {
651    /// Magic bytes: "DX∞C" (DX Infinity Cross-crate)
652    pub magic: [u8; 4],
653    /// Format version
654    pub version: u16,
655    /// Source crate identifier
656    pub source_crate: u8,
657    /// Flags
658    pub flags: u8,
659    /// Payload length
660    pub payload_len: u32,
661    /// Checksum (Blake3, first 8 bytes)
662    pub checksum: [u8; 8],
663}
664
665/// Source crate identifiers
666#[repr(u8)]
667#[derive(Debug, Clone, Copy, PartialEq, Eq)]
668pub enum SourceCrate {
669    /// dx-driven crate
670    Driven = 1,
671    /// dx-generator crate
672    Generator = 2,
673    /// dx-dcp crate
674    Dcp = 3,
675    /// Unknown source
676    Unknown = 0,
677}
678
679impl CrossCrateHeader {
680    /// Magic bytes for cross-crate format
681    pub const MAGIC: [u8; 4] = [0x44, 0x58, 0xE2, 0x43]; // "DX∞C" (with infinity symbol byte)
682
683    /// Header size in bytes
684    pub const SIZE: usize = std::mem::size_of::<Self>();
685
686    /// Current format version
687    pub const CURRENT_VERSION: u16 = 1;
688
689    /// Create a new header
690    pub fn new(source: SourceCrate, payload_len: u32) -> Self {
691        Self {
692            magic: Self::MAGIC,
693            version: Self::CURRENT_VERSION,
694            source_crate: source as u8,
695            flags: 0,
696            payload_len,
697            checksum: [0; 8],
698        }
699    }
700
701    /// Create header with checksum
702    pub fn with_checksum(source: SourceCrate, payload: &[u8]) -> Self {
703        let mut header = Self::new(source, payload.len() as u32);
704        header.checksum = Self::compute_checksum(payload);
705        header
706    }
707
708    /// Compute checksum for payload
709    pub fn compute_checksum(payload: &[u8]) -> [u8; 8] {
710        let hash = blake3::hash(payload);
711        let mut checksum = [0u8; 8];
712        checksum.copy_from_slice(&hash.as_bytes()[..8]);
713        checksum
714    }
715
716    /// Verify the checksum
717    pub fn verify_checksum(&self, payload: &[u8]) -> bool {
718        let expected = Self::compute_checksum(payload);
719        self.checksum == expected
720    }
721
722    /// Check if magic bytes are valid
723    pub fn is_valid_magic(&self) -> bool {
724        self.magic == Self::MAGIC
725    }
726
727    /// Get the source crate
728    pub fn source(&self) -> SourceCrate {
729        match self.source_crate {
730            1 => SourceCrate::Driven,
731            2 => SourceCrate::Generator,
732            3 => SourceCrate::Dcp,
733            _ => SourceCrate::Unknown,
734        }
735    }
736
737    /// Convert to bytes
738    pub fn as_bytes(&self) -> &[u8] {
739        unsafe { std::slice::from_raw_parts(self as *const Self as *const u8, Self::SIZE) }
740    }
741
742    /// Parse from bytes
743    pub fn from_bytes(bytes: &[u8]) -> Result<&Self> {
744        if bytes.len() < Self::SIZE {
745            return Err(DrivenError::InvalidBinary(format!(
746                "Insufficient data for header: {} < {}",
747                bytes.len(),
748                Self::SIZE
749            )));
750        }
751
752        let header = unsafe { &*(bytes.as_ptr() as *const Self) };
753
754        if !header.is_valid_magic() {
755            return Err(DrivenError::InvalidBinary(
756                "Invalid magic bytes".to_string(),
757            ));
758        }
759
760        Ok(header)
761    }
762}
763
764impl BinaryFormatChecker {
765    /// Create a new binary format checker
766    pub fn new() -> Self {
767        Self {
768            supported_versions: vec![1],
769        }
770    }
771
772    /// Check if a version is supported
773    pub fn is_version_supported(&self, version: u16) -> bool {
774        self.supported_versions.contains(&version)
775    }
776
777    /// Validate a cross-crate binary blob
778    pub fn validate(&self, data: &[u8]) -> Result<ValidationResult> {
779        // Parse header
780        let header = CrossCrateHeader::from_bytes(data)?;
781
782        // Check version
783        if !self.is_version_supported(header.version) {
784            return Ok(ValidationResult {
785                valid: false,
786                source: header.source(),
787                version: header.version,
788                payload_len: header.payload_len,
789                checksum_valid: false,
790                error: Some(format!("Unsupported version: {}", header.version)),
791            });
792        }
793
794        // Check payload length
795        let expected_len = CrossCrateHeader::SIZE + header.payload_len as usize;
796        if data.len() < expected_len {
797            return Ok(ValidationResult {
798                valid: false,
799                source: header.source(),
800                version: header.version,
801                payload_len: header.payload_len,
802                checksum_valid: false,
803                error: Some(format!(
804                    "Insufficient data: expected {}, got {}",
805                    expected_len,
806                    data.len()
807                )),
808            });
809        }
810
811        // Verify checksum
812        let payload = &data[CrossCrateHeader::SIZE..expected_len];
813        let checksum_valid = header.verify_checksum(payload);
814
815        Ok(ValidationResult {
816            valid: checksum_valid,
817            source: header.source(),
818            version: header.version,
819            payload_len: header.payload_len,
820            checksum_valid,
821            error: if checksum_valid {
822                None
823            } else {
824                Some("Checksum mismatch".to_string())
825            },
826        })
827    }
828
829    /// Wrap payload with cross-crate header
830    pub fn wrap(&self, source: SourceCrate, payload: &[u8]) -> Vec<u8> {
831        let header = CrossCrateHeader::with_checksum(source, payload);
832        let mut result = Vec::with_capacity(CrossCrateHeader::SIZE + payload.len());
833        result.extend_from_slice(header.as_bytes());
834        result.extend_from_slice(payload);
835        result
836    }
837
838    /// Unwrap payload from cross-crate format
839    pub fn unwrap(&self, data: &[u8]) -> Result<(SourceCrate, Vec<u8>)> {
840        let result = self.validate(data)?;
841
842        if !result.valid {
843            return Err(DrivenError::InvalidBinary(
844                result.error.unwrap_or_else(|| "Unknown error".to_string()),
845            ));
846        }
847
848        let payload = data
849            [CrossCrateHeader::SIZE..CrossCrateHeader::SIZE + result.payload_len as usize]
850            .to_vec();
851        Ok((result.source, payload))
852    }
853}
854
855impl Default for BinaryFormatChecker {
856    fn default() -> Self {
857        Self::new()
858    }
859}
860
861/// Result of binary format validation
862#[derive(Debug, Clone)]
863pub struct ValidationResult {
864    /// Whether the data is valid
865    pub valid: bool,
866    /// Source crate
867    pub source: SourceCrate,
868    /// Format version
869    pub version: u16,
870    /// Payload length
871    pub payload_len: u32,
872    /// Whether checksum is valid
873    pub checksum_valid: bool,
874    /// Error message if invalid
875    pub error: Option<String>,
876}
877
878// ==================== Integration Tests ====================
879
880#[cfg(test)]
881mod tests {
882    use super::*;
883
884    // ==================== DrivenDcpBridge Tests ====================
885
886    #[test]
887    fn test_driven_dcp_bridge_creation() {
888        let bridge = DrivenDcpBridge::new();
889        assert!(!bridge.initialized);
890        assert!(bridge.tool_ids.is_empty());
891    }
892
893    #[test]
894    fn test_driven_tools_list() {
895        let bridge = DrivenDcpBridge::new();
896        let tools = bridge.get_driven_tools();
897
898        // Should have tools for each category
899        assert!(
900            tools
901                .iter()
902                .any(|t| t.category == DrivenToolCategory::Rules)
903        );
904        assert!(
905            tools
906                .iter()
907                .any(|t| t.category == DrivenToolCategory::Specs)
908        );
909        assert!(
910            tools
911                .iter()
912                .any(|t| t.category == DrivenToolCategory::Hooks)
913        );
914        assert!(
915            tools
916                .iter()
917                .any(|t| t.category == DrivenToolCategory::Steering)
918        );
919        assert!(
920            tools
921                .iter()
922                .any(|t| t.category == DrivenToolCategory::Templates)
923        );
924    }
925
926    #[test]
927    fn test_schema_hash_determinism() {
928        let bridge = DrivenDcpBridge::new();
929        let tool = DrivenTool {
930            name: "test.tool".to_string(),
931            description: "A test tool".to_string(),
932            category: DrivenToolCategory::Rules,
933            capabilities: 0x1234,
934        };
935
936        let hash1 = bridge.compute_schema_hash(&tool);
937        let hash2 = bridge.compute_schema_hash(&tool);
938
939        assert_eq!(hash1, hash2);
940    }
941
942    // ==================== SpecScaffolder Tests ====================
943
944    #[test]
945    fn test_spec_scaffolder_creation() {
946        let scaffolder = SpecScaffolder::new().unwrap();
947        assert_eq!(scaffolder.next_spec_number(), 1);
948    }
949
950    #[test]
951    fn test_spec_scaffolder_custom_dir() {
952        let scaffolder = SpecScaffolder::with_spec_dir("custom/specs").unwrap();
953        assert_eq!(scaffolder.spec_dir(), Path::new("custom/specs"));
954    }
955
956    // ==================== HookMessenger Tests ====================
957
958    #[test]
959    fn test_hook_messenger_creation() {
960        let messenger = HookMessenger::new();
961        assert!(messenger.use_dcp);
962        assert_eq!(messenger.queue_len(), 0);
963    }
964
965    #[test]
966    fn test_hook_message_serialization_roundtrip() {
967        let messenger = HookMessenger::new();
968
969        let mut context = HashMap::new();
970        context.insert("file".to_string(), "test.rs".to_string());
971        context.insert("branch".to_string(), "main".to_string());
972
973        let message = HookMessage {
974            hook_id: "test-hook".to_string(),
975            content: "Test message content".to_string(),
976            context,
977            priority: 50,
978            timestamp: 1234567890,
979        };
980
981        let serialized = messenger.serialize_message(&message).unwrap();
982        let deserialized = messenger.deserialize_message(&serialized).unwrap();
983
984        assert_eq!(deserialized.hook_id, message.hook_id);
985        assert_eq!(deserialized.content, message.content);
986        assert_eq!(deserialized.priority, message.priority);
987        assert_eq!(deserialized.timestamp, message.timestamp);
988        assert_eq!(deserialized.context.len(), message.context.len());
989    }
990
991    #[test]
992    fn test_hook_message_queue() {
993        let mut messenger = HookMessenger::new();
994
995        // Queue messages with different priorities
996        messenger.queue_message(HookMessage {
997            hook_id: "low".to_string(),
998            content: "Low priority".to_string(),
999            context: HashMap::new(),
1000            priority: 100,
1001            timestamp: 0,
1002        });
1003
1004        messenger.queue_message(HookMessage {
1005            hook_id: "high".to_string(),
1006            content: "High priority".to_string(),
1007            context: HashMap::new(),
1008            priority: 10,
1009            timestamp: 0,
1010        });
1011
1012        messenger.queue_message(HookMessage {
1013            hook_id: "medium".to_string(),
1014            content: "Medium priority".to_string(),
1015            context: HashMap::new(),
1016            priority: 50,
1017            timestamp: 0,
1018        });
1019
1020        assert_eq!(messenger.queue_len(), 3);
1021
1022        // Messages should be sorted by priority
1023        assert_eq!(messenger.message_queue[0].hook_id, "high");
1024        assert_eq!(messenger.message_queue[1].hook_id, "medium");
1025        assert_eq!(messenger.message_queue[2].hook_id, "low");
1026    }
1027
1028    // ==================== BinaryFormatChecker Tests ====================
1029
1030    #[test]
1031    fn test_cross_crate_header_creation() {
1032        let header = CrossCrateHeader::new(SourceCrate::Driven, 100);
1033
1034        assert!(header.is_valid_magic());
1035        assert_eq!(header.version, CrossCrateHeader::CURRENT_VERSION);
1036        assert_eq!(header.source(), SourceCrate::Driven);
1037        assert_eq!(header.payload_len, 100);
1038    }
1039
1040    #[test]
1041    fn test_cross_crate_header_checksum() {
1042        let payload = b"test payload data";
1043        let header = CrossCrateHeader::with_checksum(SourceCrate::Generator, payload);
1044
1045        assert!(header.verify_checksum(payload));
1046        assert!(!header.verify_checksum(b"different data"));
1047    }
1048
1049    #[test]
1050    fn test_binary_format_wrap_unwrap() {
1051        let checker = BinaryFormatChecker::new();
1052        let payload = b"test payload for cross-crate transfer";
1053
1054        let wrapped = checker.wrap(SourceCrate::Dcp, payload);
1055        let (source, unwrapped) = checker.unwrap(&wrapped).unwrap();
1056
1057        assert_eq!(source, SourceCrate::Dcp);
1058        assert_eq!(unwrapped, payload);
1059    }
1060
1061    #[test]
1062    fn test_binary_format_validation() {
1063        let checker = BinaryFormatChecker::new();
1064        let payload = b"validation test payload";
1065
1066        let wrapped = checker.wrap(SourceCrate::Driven, payload);
1067        let result = checker.validate(&wrapped).unwrap();
1068
1069        assert!(result.valid);
1070        assert!(result.checksum_valid);
1071        assert_eq!(result.source, SourceCrate::Driven);
1072        assert_eq!(result.payload_len, payload.len() as u32);
1073        assert!(result.error.is_none());
1074    }
1075
1076    #[test]
1077    fn test_binary_format_invalid_magic() {
1078        let checker = BinaryFormatChecker::new();
1079        let invalid_data = vec![0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00];
1080
1081        let result = checker.validate(&invalid_data);
1082        assert!(result.is_err());
1083    }
1084
1085    #[test]
1086    fn test_binary_format_corrupted_checksum() {
1087        let checker = BinaryFormatChecker::new();
1088        let payload = b"test payload";
1089
1090        let mut wrapped = checker.wrap(SourceCrate::Driven, payload);
1091        // Corrupt the payload
1092        if wrapped.len() > CrossCrateHeader::SIZE {
1093            wrapped[CrossCrateHeader::SIZE] ^= 0xFF;
1094        }
1095
1096        let result = checker.validate(&wrapped).unwrap();
1097        assert!(!result.valid);
1098        assert!(!result.checksum_valid);
1099    }
1100}
1101
1102// ==================== Property-Based Tests ====================
1103
1104#[cfg(test)]
1105mod property_tests {
1106    use super::*;
1107    use proptest::prelude::*;
1108
1109    // Property 13: Cross-Crate Integration
1110    // For any operation that spans multiple crates, the operation SHALL complete
1111    // successfully with correct data passing between crates.
1112
1113    proptest! {
1114        /// Property: Hook message serialization is lossless
1115        #[test]
1116        fn prop_hook_message_roundtrip(
1117            hook_id in "[a-z][a-z0-9-]{0,30}",
1118            content in ".{0,1000}",
1119            priority in 0u8..=255u8,
1120            timestamp in 0u64..=u64::MAX,
1121        ) {
1122            let messenger = HookMessenger::new();
1123
1124            let message = HookMessage {
1125                hook_id: hook_id.clone(),
1126                content: content.clone(),
1127                context: HashMap::new(),
1128                priority,
1129                timestamp,
1130            };
1131
1132            let serialized = messenger.serialize_message(&message).unwrap();
1133            let deserialized = messenger.deserialize_message(&serialized).unwrap();
1134
1135            prop_assert_eq!(deserialized.hook_id, hook_id);
1136            prop_assert_eq!(deserialized.content, content);
1137            prop_assert_eq!(deserialized.priority, priority);
1138            prop_assert_eq!(deserialized.timestamp, timestamp);
1139        }
1140
1141        /// Property: Binary format wrap/unwrap is lossless
1142        #[test]
1143        fn prop_binary_format_roundtrip(
1144            payload in prop::collection::vec(any::<u8>(), 0..10000),
1145            source in prop_oneof![
1146                Just(SourceCrate::Driven),
1147                Just(SourceCrate::Generator),
1148                Just(SourceCrate::Dcp),
1149            ],
1150        ) {
1151            let checker = BinaryFormatChecker::new();
1152
1153            let wrapped = checker.wrap(source, &payload);
1154            let (unwrapped_source, unwrapped_payload) = checker.unwrap(&wrapped).unwrap();
1155
1156            prop_assert_eq!(unwrapped_source, source);
1157            prop_assert_eq!(unwrapped_payload, payload);
1158        }
1159
1160        /// Property: Cross-crate header checksum is deterministic
1161        #[test]
1162        fn prop_checksum_deterministic(
1163            payload in prop::collection::vec(any::<u8>(), 0..10000),
1164        ) {
1165            let checksum1 = CrossCrateHeader::compute_checksum(&payload);
1166            let checksum2 = CrossCrateHeader::compute_checksum(&payload);
1167
1168            prop_assert_eq!(checksum1, checksum2);
1169        }
1170
1171        /// Property: Schema hash is deterministic for same tool
1172        #[test]
1173        fn prop_schema_hash_deterministic(
1174            name in "[a-z][a-z0-9.]{0,50}",
1175            description in ".{0,200}",
1176            capabilities in 0u64..=u64::MAX,
1177        ) {
1178            let bridge = DrivenDcpBridge::new();
1179
1180            let tool = DrivenTool {
1181                name: name.clone(),
1182                description: description.clone(),
1183                category: DrivenToolCategory::Rules,
1184                capabilities,
1185            };
1186
1187            let hash1 = bridge.compute_schema_hash(&tool);
1188            let hash2 = bridge.compute_schema_hash(&tool);
1189
1190            prop_assert_eq!(hash1, hash2);
1191        }
1192
1193        /// Property: Message queue maintains priority ordering
1194        #[test]
1195        fn prop_message_queue_priority_order(
1196            priorities in prop::collection::vec(0u8..=255u8, 1..20),
1197        ) {
1198            let mut messenger = HookMessenger::new();
1199
1200            for (i, priority) in priorities.iter().enumerate() {
1201                messenger.queue_message(HookMessage {
1202                    hook_id: format!("hook-{}", i),
1203                    content: String::new(),
1204                    context: HashMap::new(),
1205                    priority: *priority,
1206                    timestamp: 0,
1207                });
1208            }
1209
1210            // Verify messages are sorted by priority
1211            for i in 1..messenger.message_queue.len() {
1212                prop_assert!(
1213                    messenger.message_queue[i - 1].priority <= messenger.message_queue[i].priority,
1214                    "Messages not sorted by priority"
1215                );
1216            }
1217        }
1218
1219        /// Property: Binary format validation detects corruption
1220        #[test]
1221        fn prop_corruption_detection(
1222            payload in prop::collection::vec(any::<u8>(), 1..1000),
1223            corrupt_offset in 0usize..1000usize,
1224        ) {
1225            let checker = BinaryFormatChecker::new();
1226            let mut wrapped = checker.wrap(SourceCrate::Driven, &payload);
1227
1228            // Only corrupt if offset is within payload area
1229            let payload_start = CrossCrateHeader::SIZE;
1230            if corrupt_offset < payload.len() {
1231                let actual_offset = payload_start + corrupt_offset;
1232                if actual_offset < wrapped.len() {
1233                    wrapped[actual_offset] ^= 0xFF;
1234
1235                    let result = checker.validate(&wrapped).unwrap();
1236                    prop_assert!(!result.checksum_valid, "Corruption not detected");
1237                }
1238            }
1239        }
1240    }
1241}