Skip to main content

driven/format/
encoder.rs

1//! Binary encoder for .drv format
2
3use super::{
4    SectionType,
5    schema::{
6        ContextSection, DrvHeader, PersonaSection, RuleEntry, StandardsSection, WorkflowSection,
7    },
8};
9use crate::{DrivenError, Result, parser::UnifiedRule};
10use bytes::{BufMut, BytesMut};
11use std::collections::HashMap;
12
13/// Encoder for creating .drv binary files
14#[derive(Debug)]
15pub struct DrvEncoder {
16    /// String deduplication table
17    string_table: Vec<String>,
18    /// String to index mapping
19    string_map: HashMap<String, u32>,
20    /// Output buffer
21    buffer: BytesMut,
22}
23
24impl DrvEncoder {
25    /// Create a new encoder
26    pub fn new() -> Self {
27        Self {
28            string_table: Vec::new(),
29            string_map: HashMap::new(),
30            buffer: BytesMut::with_capacity(4096),
31        }
32    }
33
34    /// Intern a string and return its index
35    fn intern(&mut self, s: &str) -> u32 {
36        if let Some(&idx) = self.string_map.get(s) {
37            return idx;
38        }
39        let idx = self.string_table.len() as u32;
40        self.string_table.push(s.to_string());
41        self.string_map.insert(s.to_string(), idx);
42        idx
43    }
44
45    /// Encode rules to binary format
46    pub fn encode(&self, rules: &[UnifiedRule]) -> Result<Vec<u8>> {
47        let mut encoder = DrvEncoder::new();
48        encoder.encode_rules(rules)
49    }
50
51    /// Internal encoding implementation
52    fn encode_rules(&mut self, rules: &[UnifiedRule]) -> Result<Vec<u8>> {
53        // First pass: build string table and sections
54        let mut persona: Option<PersonaSection> = None;
55        let mut standards = StandardsSection::default();
56        let mut context = ContextSection::default();
57        let mut workflow: Option<WorkflowSection> = None;
58
59        for rule in rules {
60            match rule {
61                UnifiedRule::Persona {
62                    name,
63                    role,
64                    identity,
65                    style,
66                    traits,
67                    principles,
68                } => {
69                    persona = Some(PersonaSection {
70                        name_idx: self.intern(name),
71                        role_idx: self.intern(role),
72                        identity_idx: identity.as_ref().map(|s| self.intern(s)).unwrap_or(0),
73                        style_idx: style.as_ref().map(|s| self.intern(s)).unwrap_or(0),
74                        traits: traits.iter().map(|t| self.intern(t)).collect(),
75                        principles: principles.iter().map(|p| self.intern(p)).collect(),
76                    });
77                }
78                UnifiedRule::Standard {
79                    category,
80                    priority,
81                    description,
82                    pattern,
83                } => {
84                    standards.rules.push(RuleEntry {
85                        category: *category,
86                        priority: *priority,
87                        description_idx: self.intern(description),
88                        pattern_idx: pattern.as_ref().map(|p| self.intern(p)).unwrap_or(0),
89                    });
90                }
91                UnifiedRule::Context {
92                    includes,
93                    excludes,
94                    focus,
95                } => {
96                    context.include_patterns = includes.iter().map(|s| self.intern(s)).collect();
97                    context.exclude_patterns = excludes.iter().map(|s| self.intern(s)).collect();
98                    context.focus_areas = focus.iter().map(|s| self.intern(s)).collect();
99                }
100                UnifiedRule::Workflow { name, steps } => {
101                    let mut ws = WorkflowSection {
102                        name_idx: self.intern(name),
103                        steps: Vec::new(),
104                    };
105                    for step in steps {
106                        ws.steps.push(super::schema::WorkflowStep {
107                            name_idx: self.intern(&step.name),
108                            description_idx: self.intern(&step.description),
109                            condition_idx: step
110                                .condition
111                                .as_ref()
112                                .map(|c| self.intern(c))
113                                .unwrap_or(0),
114                            actions: step.actions.iter().map(|a| self.intern(a)).collect(),
115                        });
116                    }
117                    workflow = Some(ws);
118                }
119                UnifiedRule::Raw { content } => {
120                    // Store raw content as a standard rule
121                    standards.rules.push(RuleEntry {
122                        category: super::schema::RuleCategory::Other,
123                        priority: 100,
124                        description_idx: self.intern(content),
125                        pattern_idx: 0,
126                    });
127                }
128            }
129        }
130
131        // Count sections
132        let mut section_count = 1; // String table always present
133        if persona.is_some() {
134            section_count += 1;
135        }
136        if !standards.rules.is_empty() {
137            section_count += 1;
138        }
139        if !context.include_patterns.is_empty() || !context.exclude_patterns.is_empty() {
140            section_count += 1;
141        }
142        if workflow.is_some() {
143            section_count += 1;
144        }
145
146        // Write header (will update checksum at end)
147        let header = DrvHeader::new(section_count);
148        self.buffer.put_slice(bytemuck::bytes_of(&header));
149
150        // Write string table
151        self.write_string_table()?;
152
153        // Write sections
154        if let Some(ref p) = persona {
155            self.write_persona_section(p)?;
156        }
157        if !standards.rules.is_empty() {
158            self.write_standards_section(&standards)?;
159        }
160        if !context.include_patterns.is_empty() || !context.exclude_patterns.is_empty() {
161            self.write_context_section(&context)?;
162        }
163        if let Some(ref w) = workflow {
164            self.write_workflow_section(w)?;
165        }
166
167        // Calculate and update checksum
168        let checksum = self.calculate_checksum();
169        let header_bytes = &mut self.buffer[..16];
170        header_bytes[12..16].copy_from_slice(&checksum.to_le_bytes());
171
172        Ok(self.buffer.to_vec())
173    }
174
175    fn write_string_table(&mut self) -> Result<()> {
176        self.buffer.put_u8(SectionType::StringTable as u8);
177        self.buffer.put_u32_le(self.string_table.len() as u32);
178
179        for s in &self.string_table {
180            let bytes = s.as_bytes();
181            if bytes.len() > u16::MAX as usize {
182                return Err(DrivenError::Format(format!(
183                    "String too long: {} bytes",
184                    bytes.len()
185                )));
186            }
187            self.buffer.put_u16_le(bytes.len() as u16);
188            self.buffer.put_slice(bytes);
189        }
190
191        Ok(())
192    }
193
194    fn write_persona_section(&mut self, persona: &PersonaSection) -> Result<()> {
195        self.buffer.put_u8(SectionType::Persona as u8);
196        self.buffer.put_u32_le(persona.name_idx);
197        self.buffer.put_u32_le(persona.role_idx);
198        self.buffer.put_u32_le(persona.identity_idx);
199        self.buffer.put_u32_le(persona.style_idx);
200
201        self.buffer.put_u16_le(persona.traits.len() as u16);
202        for &idx in &persona.traits {
203            self.buffer.put_u32_le(idx);
204        }
205
206        self.buffer.put_u16_le(persona.principles.len() as u16);
207        for &idx in &persona.principles {
208            self.buffer.put_u32_le(idx);
209        }
210
211        Ok(())
212    }
213
214    fn write_standards_section(&mut self, standards: &StandardsSection) -> Result<()> {
215        self.buffer.put_u8(SectionType::Standards as u8);
216        self.buffer.put_u32_le(standards.rules.len() as u32);
217
218        for rule in &standards.rules {
219            self.buffer.put_u8(rule.category as u8);
220            self.buffer.put_u8(rule.priority);
221            self.buffer.put_u32_le(rule.description_idx);
222            self.buffer.put_u32_le(rule.pattern_idx);
223        }
224
225        Ok(())
226    }
227
228    fn write_context_section(&mut self, context: &ContextSection) -> Result<()> {
229        self.buffer.put_u8(SectionType::Context as u8);
230
231        self.buffer
232            .put_u16_le(context.include_patterns.len() as u16);
233        for &idx in &context.include_patterns {
234            self.buffer.put_u32_le(idx);
235        }
236
237        self.buffer
238            .put_u16_le(context.exclude_patterns.len() as u16);
239        for &idx in &context.exclude_patterns {
240            self.buffer.put_u32_le(idx);
241        }
242
243        self.buffer.put_u16_le(context.focus_areas.len() as u16);
244        for &idx in &context.focus_areas {
245            self.buffer.put_u32_le(idx);
246        }
247
248        self.buffer.put_u16_le(context.dependencies.len() as u16);
249        for &(name, version) in &context.dependencies {
250            self.buffer.put_u32_le(name);
251            self.buffer.put_u32_le(version);
252        }
253
254        Ok(())
255    }
256
257    fn write_workflow_section(&mut self, workflow: &WorkflowSection) -> Result<()> {
258        self.buffer.put_u8(SectionType::Workflow as u8);
259        self.buffer.put_u32_le(workflow.name_idx);
260        self.buffer.put_u16_le(workflow.steps.len() as u16);
261
262        for step in &workflow.steps {
263            self.buffer.put_u32_le(step.name_idx);
264            self.buffer.put_u32_le(step.description_idx);
265            self.buffer.put_u32_le(step.condition_idx);
266            self.buffer.put_u16_le(step.actions.len() as u16);
267            for &idx in &step.actions {
268                self.buffer.put_u32_le(idx);
269            }
270        }
271
272        Ok(())
273    }
274
275    fn calculate_checksum(&self) -> u32 {
276        let hash = blake3::hash(&self.buffer[16..]);
277        let bytes = hash.as_bytes();
278        u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
279    }
280}
281
282impl Default for DrvEncoder {
283    fn default() -> Self {
284        Self::new()
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_encoder_new() {
294        let encoder = DrvEncoder::new();
295        assert!(encoder.string_table.is_empty());
296        assert!(encoder.string_map.is_empty());
297    }
298
299    #[test]
300    fn test_string_interning() {
301        let mut encoder = DrvEncoder::new();
302        let idx1 = encoder.intern("hello");
303        let idx2 = encoder.intern("world");
304        let idx3 = encoder.intern("hello"); // duplicate
305
306        assert_eq!(idx1, 0);
307        assert_eq!(idx2, 1);
308        assert_eq!(idx3, 0); // Same as first
309        assert_eq!(encoder.string_table.len(), 2);
310    }
311
312    #[test]
313    fn test_encode_empty() {
314        let encoder = DrvEncoder::new();
315        let result = encoder.encode(&[]).unwrap();
316
317        // Should have at least header + string table section
318        assert!(result.len() >= 16);
319        // Check magic bytes
320        assert_eq!(&result[0..4], b"DRV\0");
321    }
322}
323
324#[cfg(test)]
325mod prop_tests {
326    use super::*;
327    use crate::format::DrvDecoder;
328    use crate::format::schema::RuleCategory;
329    use proptest::prelude::*;
330
331    /// Generate arbitrary RuleCategory values
332    fn arb_rule_category() -> impl Strategy<Value = RuleCategory> {
333        prop_oneof![
334            Just(RuleCategory::Style),
335            Just(RuleCategory::Naming),
336            Just(RuleCategory::ErrorHandling),
337            Just(RuleCategory::Testing),
338            Just(RuleCategory::Documentation),
339            Just(RuleCategory::Security),
340            Just(RuleCategory::Performance),
341            Just(RuleCategory::Architecture),
342            Just(RuleCategory::Imports),
343            Just(RuleCategory::Git),
344            Just(RuleCategory::Api),
345            Just(RuleCategory::Other),
346        ]
347    }
348
349    /// Generate arbitrary WorkflowStepData
350    fn arb_workflow_step() -> impl Strategy<Value = crate::parser::WorkflowStepData> {
351        (
352            "[a-zA-Z][a-zA-Z0-9_]{0,20}",                              // name
353            "[a-zA-Z0-9 .,!?]{1,100}",                                 // description
354            proptest::option::of("[a-zA-Z0-9 .,!?]{1,50}"),            // condition
355            proptest::collection::vec("[a-zA-Z0-9 .,!?]{1,50}", 0..5), // actions
356        )
357            .prop_map(|(name, description, condition, actions)| {
358                crate::parser::WorkflowStepData {
359                    name,
360                    description,
361                    condition,
362                    actions,
363                }
364            })
365    }
366
367    /// Generate arbitrary UnifiedRule values
368    fn arb_unified_rule() -> impl Strategy<Value = UnifiedRule> {
369        prop_oneof![
370            // Persona rule
371            (
372                "[a-zA-Z][a-zA-Z0-9_]{0,20}",                              // name
373                "[a-zA-Z0-9 .,!?]{1,100}",                                 // role
374                proptest::option::of("[a-zA-Z0-9 .,!?]{1,100}"),           // identity
375                proptest::option::of("[a-zA-Z0-9 .,!?]{1,50}"),            // style
376                proptest::collection::vec("[a-zA-Z0-9 .,!?]{1,30}", 0..5), // traits
377                proptest::collection::vec("[a-zA-Z0-9 .,!?]{1,50}", 0..5), // principles
378            )
379                .prop_map(|(name, role, identity, style, traits, principles)| {
380                    UnifiedRule::Persona {
381                        name,
382                        role,
383                        identity,
384                        style,
385                        traits,
386                        principles,
387                    }
388                }),
389            // Standard rule
390            (
391                arb_rule_category(),
392                0u8..=255u8,                                        // priority
393                "[a-zA-Z0-9 .,!?]{1,200}",                          // description
394                proptest::option::of("[a-zA-Z0-9 .,!?*_/]{1,100}"), // pattern
395            )
396                .prop_map(|(category, priority, description, pattern)| {
397                    UnifiedRule::Standard {
398                        category,
399                        priority,
400                        description,
401                        pattern,
402                    }
403                }),
404            // Context rule
405            (
406                proptest::collection::vec("[a-zA-Z0-9*_./]{1,50}", 0..5), // includes
407                proptest::collection::vec("[a-zA-Z0-9*_./]{1,50}", 0..5), // excludes
408                proptest::collection::vec("[a-zA-Z0-9 ]{1,30}", 0..5),    // focus
409            )
410                .prop_map(|(includes, excludes, focus)| {
411                    UnifiedRule::Context {
412                        includes,
413                        excludes,
414                        focus,
415                    }
416                }),
417            // Workflow rule
418            (
419                "[a-zA-Z][a-zA-Z0-9_-]{0,30}",                        // name
420                proptest::collection::vec(arb_workflow_step(), 0..5), // steps
421            )
422                .prop_map(|(name, steps)| { UnifiedRule::Workflow { name, steps } }),
423            // Raw rule
424            "[a-zA-Z0-9 .,!?\n]{1,500}".prop_map(|content| { UnifiedRule::Raw { content } }),
425        ]
426    }
427
428    /// Helper to compare UnifiedRule values for equality (since it doesn't derive PartialEq)
429    fn rules_equal(a: &UnifiedRule, b: &UnifiedRule) -> bool {
430        match (a, b) {
431            (
432                UnifiedRule::Persona {
433                    name: n1,
434                    role: r1,
435                    identity: i1,
436                    style: s1,
437                    traits: t1,
438                    principles: p1,
439                },
440                UnifiedRule::Persona {
441                    name: n2,
442                    role: r2,
443                    identity: i2,
444                    style: s2,
445                    traits: t2,
446                    principles: p2,
447                },
448            ) => n1 == n2 && r1 == r2 && i1 == i2 && s1 == s2 && t1 == t2 && p1 == p2,
449            (
450                UnifiedRule::Standard {
451                    category: c1,
452                    priority: p1,
453                    description: d1,
454                    pattern: pt1,
455                },
456                UnifiedRule::Standard {
457                    category: c2,
458                    priority: p2,
459                    description: d2,
460                    pattern: pt2,
461                },
462            ) => c1 == c2 && p1 == p2 && d1 == d2 && pt1 == pt2,
463            (
464                UnifiedRule::Context {
465                    includes: i1,
466                    excludes: e1,
467                    focus: f1,
468                },
469                UnifiedRule::Context {
470                    includes: i2,
471                    excludes: e2,
472                    focus: f2,
473                },
474            ) => i1 == i2 && e1 == e2 && f1 == f2,
475            (
476                UnifiedRule::Workflow {
477                    name: n1,
478                    steps: s1,
479                },
480                UnifiedRule::Workflow {
481                    name: n2,
482                    steps: s2,
483                },
484            ) => {
485                if n1 != n2 || s1.len() != s2.len() {
486                    return false;
487                }
488                s1.iter().zip(s2.iter()).all(|(a, b)| {
489                    a.name == b.name
490                        && a.description == b.description
491                        && a.condition == b.condition
492                        && a.actions == b.actions
493                })
494            }
495            (UnifiedRule::Raw { content: c1 }, UnifiedRule::Raw { content: c2 }) => c1 == c2,
496            _ => false,
497        }
498    }
499
500    proptest! {
501        /// Property 2: DX Machine Format Round-Trip Consistency
502        /// *For any* valid binary rule set, encoding to DX Machine format and
503        /// decoding back SHALL produce an equivalent rule set.
504        /// **Validates: Requirements 1.2**
505        ///
506        /// Note: The current binary format has these limitations:
507        /// - Only one Persona per file (last one wins)
508        /// - Only one Workflow per file (last one wins)
509        /// - Only one Context per file (last one wins, and only written if non-empty)
510        /// - Empty Context (no include/exclude patterns) is not written
511        #[test]
512        fn prop_drv_binary_roundtrip(rules in proptest::collection::vec(arb_unified_rule(), 0..10)) {
513            let encoder = DrvEncoder::new();
514            let encoded = encoder.encode(&rules).expect("Encoding should succeed");
515
516            let decoder = DrvDecoder::new(&encoded).expect("Decoding should succeed");
517            let decoded = decoder.decode_all().expect("Decode all should succeed");
518
519            // Count expected rules after encoding (accounting for format limitations)
520            // - Only one Persona is kept (last one)
521            // - Only one Workflow is kept (last one)
522            // - Only one Context is kept (last one, and only written if non-empty)
523            // - All Standards are kept
524            // - Raw rules become Standards
525            let has_persona = rules.iter().any(|r| matches!(r, UnifiedRule::Persona { .. }));
526            let has_workflow = rules.iter().any(|r| matches!(r, UnifiedRule::Workflow { .. }));
527
528            // The last Context wins, and it's only written if it has patterns
529            let last_context = rules.iter().rev().find(|r| matches!(r, UnifiedRule::Context { .. }));
530            let has_written_context = if let Some(UnifiedRule::Context { includes, excludes, .. }) = last_context {
531                !includes.is_empty() || !excludes.is_empty()
532            } else {
533                false
534            };
535
536            let standard_count = rules.iter().filter(|r| {
537                matches!(r, UnifiedRule::Standard { .. } | UnifiedRule::Raw { .. })
538            }).count();
539
540            let expected_count = (if has_persona { 1 } else { 0 })
541                + (if has_workflow { 1 } else { 0 })
542                + (if has_written_context { 1 } else { 0 })
543                + standard_count;
544
545            prop_assert_eq!(decoded.len(), expected_count, "Rule count mismatch");
546
547            // Verify the last Persona matches
548            if has_persona {
549                let last_persona = rules.iter().rev().find(|r| matches!(r, UnifiedRule::Persona { .. }));
550                let decoded_persona = decoded.iter().find(|r| matches!(r, UnifiedRule::Persona { .. }));
551                if let (Some(orig), Some(dec)) = (last_persona, decoded_persona) {
552                    prop_assert!(rules_equal(orig, dec), "Persona mismatch");
553                }
554            }
555
556            // Verify the last Workflow matches
557            if has_workflow {
558                let last_workflow = rules.iter().rev().find(|r| matches!(r, UnifiedRule::Workflow { .. }));
559                let decoded_workflow = decoded.iter().find(|r| matches!(r, UnifiedRule::Workflow { .. }));
560                if let (Some(orig), Some(dec)) = (last_workflow, decoded_workflow) {
561                    prop_assert!(rules_equal(orig, dec), "Workflow mismatch");
562                }
563            }
564
565            // Verify Standards (including converted Raw rules)
566            let original_standards: Vec<_> = rules.iter().filter(|r| {
567                matches!(r, UnifiedRule::Standard { .. } | UnifiedRule::Raw { .. })
568            }).collect();
569            let decoded_standards: Vec<_> = decoded.iter().filter(|r| {
570                matches!(r, UnifiedRule::Standard { .. })
571            }).collect();
572
573            prop_assert_eq!(original_standards.len(), decoded_standards.len(), "Standard count mismatch");
574        }
575
576        /// Property test for string interning consistency
577        #[test]
578        fn prop_string_interning_idempotent(s in "[a-zA-Z0-9 .,!?]{1,100}") {
579            let mut encoder = DrvEncoder::new();
580            let idx1 = encoder.intern(&s);
581            let idx2 = encoder.intern(&s);
582            prop_assert_eq!(idx1, idx2, "Same string should return same index");
583        }
584
585        /// Property test for encoding determinism
586        #[test]
587        fn prop_encoding_deterministic(rules in proptest::collection::vec(arb_unified_rule(), 0..5)) {
588            let encoder1 = DrvEncoder::new();
589            let encoder2 = DrvEncoder::new();
590
591            let encoded1 = encoder1.encode(&rules).expect("First encoding should succeed");
592            let encoded2 = encoder2.encode(&rules).expect("Second encoding should succeed");
593
594            prop_assert_eq!(encoded1, encoded2, "Same rules should produce same binary");
595        }
596    }
597}