infiniloom_engine/embedding/
manifest.rs

1//! Manifest storage and diffing for incremental updates
2//!
3//! The manifest tracks all chunks generated for a repository, enabling:
4//! - Incremental updates (only re-embed changed chunks)
5//! - Change detection (added, modified, removed)
6//! - Integrity verification (detect tampering)
7//!
8//! # Storage Format
9//!
10//! Manifests are stored in bincode format (5-10x faster than JSON) with:
11//! - BLAKE3 integrity checksum
12//! - Version compatibility checking
13//! - Settings validation
14
15use std::collections::BTreeMap;
16use std::path::Path;
17
18use bincode::Options;
19use serde::{Deserialize, Serialize};
20
21use super::error::EmbedError;
22use super::hasher::IncrementalHasher;
23use super::types::{ChunkKind, EmbedChunk, EmbedSettings};
24use crate::bincode_safe::deserialize_with_limit;
25
26/// Current manifest format version
27pub const MANIFEST_VERSION: u32 = 2;
28
29/// Manifest tracking all chunks for incremental updates
30///
31/// # Determinism Note
32///
33/// The manifest binary file is **not byte-deterministic** across saves due to the
34/// `updated_at` timestamp. However, the **checksum is deterministic** because it
35/// excludes the timestamp from its calculation.
36///
37/// For comparing manifests:
38/// - **Wrong**: Compare raw binary files (will differ due to timestamp)
39/// - **Right**: Compare checksums via `manifest.checksum` (deterministic)
40///
41/// This design allows incremental updates while still detecting actual content changes.
42///
43/// # CI/CD Integration
44///
45/// If you need byte-deterministic manifests (e.g., for Docker layer caching):
46/// - Compare checksums instead of file hashes
47/// - Or set `updated_at = None` before saving in test environments
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct EmbedManifest {
50    /// Manifest format version
51    pub version: u32,
52
53    /// Relative repository path (from git root or CWD)
54    pub repo_path: String,
55
56    /// Git commit hash when manifest was created (for reference only)
57    /// Note: We always serialize Option fields for bincode compatibility
58    #[serde(default)]
59    pub commit_hash: Option<String>,
60
61    /// Timestamp of last update (Unix seconds)
62    ///
63    /// **Important**: This field is excluded from the integrity checksum calculation
64    /// to allow the checksum to remain stable across re-saves of unchanged content.
65    /// The binary file will differ byte-for-byte on each save, but the checksum will
66    /// only change if actual chunk content changes.
67    #[serde(default)]
68    pub updated_at: Option<u64>,
69
70    /// Settings used to generate chunks (part of integrity)
71    pub settings: EmbedSettings,
72
73    /// All chunks indexed by location key
74    /// Using BTreeMap for deterministic iteration order (critical for cross-platform consistency)
75    pub chunks: BTreeMap<String, ManifestEntry>,
76
77    /// Integrity checksum (BLAKE3 of settings + sorted chunk entries)
78    /// Excluded from serialization, computed on save, verified on load
79    #[serde(default)]
80    pub checksum: Option<String>,
81}
82
83/// Entry in the manifest for a single chunk
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct ManifestEntry {
86    /// Content-addressable chunk ID (128-bit)
87    pub chunk_id: String,
88
89    /// Full content hash for collision detection (256-bit)
90    pub full_hash: String,
91
92    /// Token count
93    pub tokens: u32,
94
95    /// Line range (1-indexed, inclusive)
96    pub lines: (u32, u32),
97}
98
99impl EmbedManifest {
100    /// Create a new empty manifest
101    pub fn new(repo_path: String, settings: EmbedSettings) -> Self {
102        Self {
103            version: MANIFEST_VERSION,
104            repo_path,
105            commit_hash: None,
106            updated_at: None,
107            settings,
108            chunks: BTreeMap::new(),
109            checksum: None,
110        }
111    }
112
113    /// Generate deterministic location key for a chunk
114    ///
115    /// Format: `file::symbol::kind`
116    /// Uses `::` as separator (unlikely in paths/symbols)
117    pub fn location_key(file: &str, symbol: &str, kind: ChunkKind) -> String {
118        format!("{}::{}::{}", file, symbol, kind.name())
119    }
120
121    /// Compute integrity checksum over settings and chunk entries
122    fn compute_checksum(&self) -> String {
123        let mut hasher = IncrementalHasher::new();
124
125        // Hash manifest version
126        hasher.update_u32(self.version);
127
128        // Hash settings (affects chunk generation)
129        let settings_json = serde_json::to_string(&self.settings).unwrap_or_default();
130        hasher.update_str(&settings_json);
131
132        // Hash chunks in deterministic order (sorted by key)
133        let mut keys: Vec<_> = self.chunks.keys().collect();
134        keys.sort();
135
136        for key in keys {
137            if let Some(entry) = self.chunks.get(key) {
138                hasher.update_str(key);
139                hasher.update_str(&entry.chunk_id);
140                hasher.update_str(&entry.full_hash);
141                hasher.update_u32(entry.tokens);
142                hasher.update_u32(entry.lines.0);
143                hasher.update_u32(entry.lines.1);
144            }
145        }
146
147        hasher.finalize_hex()
148    }
149
150    /// Save manifest to file with integrity checksum
151    ///
152    /// # Behavior
153    ///
154    /// This method:
155    /// 1. Updates `updated_at` to the current timestamp
156    /// 2. Computes a new checksum (excluding timestamp)
157    /// 3. Serializes to bincode format
158    ///
159    /// # Determinism
160    ///
161    /// The resulting binary file is **not byte-deterministic** because the timestamp
162    /// changes on every save. However, the checksum **is deterministic** - it only
163    /// changes when actual chunk content or settings change.
164    ///
165    /// For deterministic testing, set `self.updated_at = None` before saving.
166    ///
167    /// # Note
168    ///
169    /// This method mutates `self` to set checksum and timestamp.
170    /// This avoids cloning the entire manifest (which can be large).
171    pub fn save(&mut self, path: &Path) -> Result<(), EmbedError> {
172        // Create parent directories
173        if let Some(parent) = path.parent() {
174            std::fs::create_dir_all(parent)
175                .map_err(|e| EmbedError::IoError { path: path.to_path_buf(), source: e })?;
176        }
177
178        // Update timestamp
179        self.updated_at = Some(
180            std::time::SystemTime::now()
181                .duration_since(std::time::UNIX_EPOCH)
182                .map(|d| d.as_secs())
183                .unwrap_or(0),
184        );
185
186        // Compute checksum (excludes timestamp for deterministic checksums across saves)
187        self.checksum = Some(self.compute_checksum());
188
189        // Use bincode for faster I/O (5-10x faster than JSON for large manifests)
190        // Note: Must use bincode::options() to match deserialize_with_limit() in load()
191        let bytes = bincode::options()
192            .serialize(self)
193            .map_err(|e| EmbedError::SerializationError { reason: e.to_string() })?;
194
195        std::fs::write(path, bytes)
196            .map_err(|e| EmbedError::IoError { path: path.to_path_buf(), source: e })?;
197
198        Ok(())
199    }
200
201    /// Load manifest from file with integrity verification
202    pub fn load(path: &Path) -> Result<Self, EmbedError> {
203        let bytes = std::fs::read(path)
204            .map_err(|e| EmbedError::IoError { path: path.to_path_buf(), source: e })?;
205
206        let mut manifest: Self = deserialize_with_limit(&bytes)
207            .map_err(|e| EmbedError::DeserializationError { reason: e.to_string() })?;
208
209        // Version check
210        if manifest.version > MANIFEST_VERSION {
211            return Err(EmbedError::ManifestVersionTooNew {
212                found: manifest.version,
213                max_supported: MANIFEST_VERSION,
214            });
215        }
216
217        // Integrity verification using constant-time comparison to prevent timing attacks
218        if let Some(stored_checksum) = manifest.checksum.take() {
219            let computed = manifest.compute_checksum();
220            if !constant_time_eq(stored_checksum.as_bytes(), computed.as_bytes()) {
221                return Err(EmbedError::ManifestCorrupted {
222                    path: path.to_path_buf(),
223                    expected: stored_checksum,
224                    actual: computed,
225                });
226            }
227        }
228
229        // Validate settings
230        manifest.settings.validate()?;
231
232        Ok(manifest)
233    }
234
235    /// Load manifest if it exists, otherwise return None
236    pub fn load_if_exists(path: &Path) -> Result<Option<Self>, EmbedError> {
237        if path.exists() {
238            Ok(Some(Self::load(path)?))
239        } else {
240            Ok(None)
241        }
242    }
243
244    /// Update manifest with current chunks, detecting collisions
245    pub fn update(&mut self, chunks: &[EmbedChunk]) -> Result<(), EmbedError> {
246        // Collision detection: track id -> full_hash mappings
247        // Using BTreeMap for deterministic iteration (critical for cross-platform consistency)
248        let mut id_to_hash: BTreeMap<&str, &str> = BTreeMap::new();
249
250        self.chunks.clear();
251
252        for chunk in chunks {
253            // Check for hash collision
254            if let Some(&existing_hash) = id_to_hash.get(chunk.id.as_str()) {
255                if existing_hash != chunk.full_hash.as_str() {
256                    return Err(EmbedError::HashCollision {
257                        id: chunk.id.clone(),
258                        hash1: existing_hash.to_string(),
259                        hash2: chunk.full_hash.clone(),
260                    });
261                }
262            }
263            id_to_hash.insert(&chunk.id, &chunk.full_hash);
264
265            let key = Self::location_key(&chunk.source.file, &chunk.source.symbol, chunk.kind);
266
267            self.chunks.insert(
268                key,
269                ManifestEntry {
270                    chunk_id: chunk.id.clone(),
271                    full_hash: chunk.full_hash.clone(),
272                    tokens: chunk.tokens,
273                    lines: chunk.source.lines,
274                },
275            );
276        }
277
278        Ok(())
279    }
280
281    /// Compute diff between current chunks and manifest
282    pub fn diff(&self, current_chunks: &[EmbedChunk]) -> EmbedDiff {
283        let mut added = Vec::new();
284        let mut modified = Vec::new();
285        let mut removed = Vec::new();
286        let mut unchanged = Vec::new();
287
288        // Build map of current chunks by location key
289        // Using BTreeMap for deterministic iteration in "added" detection
290        let current_map: BTreeMap<String, &EmbedChunk> = current_chunks
291            .iter()
292            .map(|c| (Self::location_key(&c.source.file, &c.source.symbol, c.kind), c))
293            .collect();
294
295        // Find modified and unchanged (iterate manifest)
296        for (key, entry) in &self.chunks {
297            if let Some(current) = current_map.get(key) {
298                if current.id == entry.chunk_id {
299                    unchanged.push(current.id.clone());
300                } else {
301                    modified.push(ModifiedChunk {
302                        old_id: entry.chunk_id.clone(),
303                        new_id: current.id.clone(),
304                        chunk: (*current).clone(),
305                    });
306                }
307            } else {
308                // In manifest but not in current = removed
309                removed
310                    .push(RemovedChunk { id: entry.chunk_id.clone(), location_key: key.clone() });
311            }
312        }
313
314        // Find added (in current but not in manifest)
315        for (key, chunk) in &current_map {
316            if !self.chunks.contains_key(key) {
317                added.push((*chunk).clone());
318            }
319        }
320
321        let summary = DiffSummary {
322            added: added.len(),
323            modified: modified.len(),
324            removed: removed.len(),
325            unchanged: unchanged.len(),
326            total_chunks: current_chunks.len(),
327        };
328
329        EmbedDiff { summary, added, modified, removed, unchanged }
330    }
331
332    /// Check if settings match the manifest settings
333    pub fn settings_match(&self, settings: &EmbedSettings) -> bool {
334        &self.settings == settings
335    }
336
337    /// Get the number of chunks in the manifest
338    pub fn chunk_count(&self) -> usize {
339        self.chunks.len()
340    }
341}
342
343/// Result of diffing current state against manifest
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct EmbedDiff {
346    /// Summary statistics
347    pub summary: DiffSummary,
348
349    /// New chunks (not in manifest)
350    pub added: Vec<EmbedChunk>,
351
352    /// Changed chunks (different content)
353    pub modified: Vec<ModifiedChunk>,
354
355    /// Deleted chunks (in manifest but not current)
356    pub removed: Vec<RemovedChunk>,
357
358    /// Unchanged chunk IDs (same content)
359    pub unchanged: Vec<String>,
360}
361
362impl EmbedDiff {
363    /// Check if there are any changes
364    pub fn has_changes(&self) -> bool {
365        self.summary.added > 0 || self.summary.modified > 0 || self.summary.removed > 0
366    }
367
368    /// Get all chunks that need to be upserted (added + modified)
369    pub fn chunks_to_upsert(&self) -> Vec<&EmbedChunk> {
370        let mut chunks: Vec<&EmbedChunk> = self.added.iter().collect();
371        chunks.extend(self.modified.iter().map(|m| &m.chunk));
372        chunks
373    }
374
375    /// Get all IDs that need to be deleted
376    pub fn ids_to_delete(&self) -> Vec<&str> {
377        let mut ids: Vec<&str> = self.removed.iter().map(|r| r.id.as_str()).collect();
378        // Also delete old IDs for modified chunks
379        ids.extend(self.modified.iter().map(|m| m.old_id.as_str()));
380        ids
381    }
382
383    /// Split diff into batches for vector DB operations
384    pub fn batches(&self, batch_size: usize) -> Vec<DiffBatch> {
385        let mut batches = Vec::new();
386        let mut batch_num = 0;
387
388        // Batch added chunks
389        for chunk in self.added.chunks(batch_size) {
390            batches.push(DiffBatch {
391                batch_number: batch_num,
392                operation: BatchOperation::Upsert,
393                chunks: chunk.to_vec(),
394                ids: Vec::new(),
395            });
396            batch_num += 1;
397        }
398
399        // Batch modified chunks
400        for chunk in self.modified.chunks(batch_size) {
401            batches.push(DiffBatch {
402                batch_number: batch_num,
403                operation: BatchOperation::Upsert,
404                chunks: chunk.iter().map(|m| m.chunk.clone()).collect(),
405                ids: chunk.iter().map(|m| m.old_id.clone()).collect(), // Old IDs to delete
406            });
407            batch_num += 1;
408        }
409
410        // Batch removed IDs
411        for ids in self.removed.chunks(batch_size) {
412            batches.push(DiffBatch {
413                batch_number: batch_num,
414                operation: BatchOperation::Delete,
415                chunks: Vec::new(),
416                ids: ids.iter().map(|r| r.id.clone()).collect(),
417            });
418            batch_num += 1;
419        }
420
421        batches
422    }
423}
424
425/// Summary of changes between manifest and current state
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct DiffSummary {
428    /// Number of new chunks
429    pub added: usize,
430
431    /// Number of modified chunks
432    pub modified: usize,
433
434    /// Number of removed chunks
435    pub removed: usize,
436
437    /// Number of unchanged chunks
438    pub unchanged: usize,
439
440    /// Total chunks in current state
441    pub total_chunks: usize,
442}
443
444/// A chunk that was modified (content changed)
445#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct ModifiedChunk {
447    /// Previous chunk ID
448    pub old_id: String,
449
450    /// New chunk ID
451    pub new_id: String,
452
453    /// The updated chunk
454    pub chunk: EmbedChunk,
455}
456
457/// A chunk that was removed
458#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct RemovedChunk {
460    /// Chunk ID that was removed
461    pub id: String,
462
463    /// Location key for reference
464    pub location_key: String,
465}
466
467/// Batch of operations for vector DB
468#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct DiffBatch {
470    /// Batch number (0-indexed)
471    pub batch_number: usize,
472
473    /// Operation type
474    pub operation: BatchOperation,
475
476    /// Chunks to upsert (for Upsert operation)
477    pub chunks: Vec<EmbedChunk>,
478
479    /// IDs to delete (for Delete operation, or old IDs for Upsert)
480    pub ids: Vec<String>,
481}
482
483/// Type of batch operation
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
485#[serde(rename_all = "snake_case")]
486pub enum BatchOperation {
487    /// Insert or update chunks
488    Upsert,
489    /// Delete chunks by ID
490    Delete,
491}
492
493/// Constant-time byte comparison to prevent timing attacks
494///
495/// Returns true if both slices are equal, using constant-time comparison
496/// that doesn't short-circuit on first difference.
497#[inline]
498fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
499    if a.len() != b.len() {
500        return false;
501    }
502
503    // XOR all bytes and accumulate - runs in constant time regardless of content
504    let mut result = 0u8;
505    for (x, y) in a.iter().zip(b.iter()) {
506        result |= x ^ y;
507    }
508    result == 0
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514    use crate::embedding::types::{ChunkContext, ChunkSource, RepoIdentifier, Visibility};
515    use tempfile::TempDir;
516
517    fn create_test_chunk(id: &str, file: &str, symbol: &str) -> EmbedChunk {
518        EmbedChunk {
519            id: id.to_string(),
520            full_hash: format!("{}_full", id),
521            content: "fn test() {}".to_string(),
522            tokens: 10,
523            kind: ChunkKind::Function,
524            source: ChunkSource {
525                repo: RepoIdentifier::default(),
526                file: file.to_string(),
527                lines: (1, 5),
528                symbol: symbol.to_string(),
529                fqn: None,
530                language: "rust".to_string(),
531                parent: None,
532                visibility: Visibility::Public,
533                is_test: false,
534            },
535            context: ChunkContext::default(),
536            part: None,
537        }
538    }
539
540    #[test]
541    fn test_new_manifest() {
542        let manifest = EmbedManifest::new("my-repo".to_string(), EmbedSettings::default());
543
544        assert_eq!(manifest.version, MANIFEST_VERSION);
545        assert_eq!(manifest.repo_path, "my-repo");
546        assert!(manifest.chunks.is_empty());
547    }
548
549    #[test]
550    fn test_location_key() {
551        let key = EmbedManifest::location_key("src/auth.rs", "validate", ChunkKind::Function);
552        assert_eq!(key, "src/auth.rs::validate::function");
553    }
554
555    #[test]
556    fn test_save_and_load() {
557        let temp_dir = TempDir::new().unwrap();
558        let manifest_path = temp_dir.path().join("test.bin");
559
560        // Create and save manifest
561        let mut manifest = EmbedManifest::new("my-repo".to_string(), EmbedSettings::default());
562
563        let chunks = vec![
564            create_test_chunk("ec_123", "src/foo.rs", "foo"),
565            create_test_chunk("ec_456", "src/bar.rs", "bar"),
566        ];
567        manifest.update(&chunks).unwrap();
568        manifest.save(&manifest_path).unwrap();
569
570        // Load and verify
571        let loaded = EmbedManifest::load(&manifest_path).unwrap();
572        assert_eq!(loaded.repo_path, "my-repo");
573        assert_eq!(loaded.chunks.len(), 2);
574    }
575
576    #[test]
577    fn test_integrity_verification() {
578        let temp_dir = TempDir::new().unwrap();
579        let manifest_path = temp_dir.path().join("test.bin");
580
581        // Create and save manifest
582        let mut manifest = EmbedManifest::new("my-repo".to_string(), EmbedSettings::default());
583        manifest.save(&manifest_path).unwrap();
584
585        // Tamper with file
586        let mut bytes = std::fs::read(&manifest_path).unwrap();
587        if bytes.len() >= 10 {
588            let idx = bytes.len() - 10;
589            bytes[idx] ^= 0xFF;
590            std::fs::write(&manifest_path, bytes).unwrap();
591        }
592
593        // Should detect tampering
594        let result = EmbedManifest::load(&manifest_path);
595        assert!(matches!(
596            result,
597            Err(EmbedError::ManifestCorrupted { .. })
598                | Err(EmbedError::DeserializationError { .. })
599        ));
600    }
601
602    #[test]
603    fn test_diff_added() {
604        let manifest = EmbedManifest::new("my-repo".to_string(), EmbedSettings::default());
605
606        let chunks = vec![create_test_chunk("ec_123", "src/foo.rs", "foo")];
607
608        let diff = manifest.diff(&chunks);
609        assert_eq!(diff.summary.added, 1);
610        assert_eq!(diff.summary.modified, 0);
611        assert_eq!(diff.summary.removed, 0);
612    }
613
614    #[test]
615    fn test_diff_modified() {
616        let mut manifest = EmbedManifest::new("my-repo".to_string(), EmbedSettings::default());
617
618        let old_chunks = vec![create_test_chunk("ec_old", "src/foo.rs", "foo")];
619        manifest.update(&old_chunks).unwrap();
620
621        // Same location, different ID = modified
622        let new_chunks = vec![create_test_chunk("ec_new", "src/foo.rs", "foo")];
623
624        let diff = manifest.diff(&new_chunks);
625        assert_eq!(diff.summary.added, 0);
626        assert_eq!(diff.summary.modified, 1);
627        assert_eq!(diff.summary.removed, 0);
628        assert_eq!(diff.modified[0].old_id, "ec_old");
629        assert_eq!(diff.modified[0].new_id, "ec_new");
630    }
631
632    #[test]
633    fn test_diff_removed() {
634        let mut manifest = EmbedManifest::new("my-repo".to_string(), EmbedSettings::default());
635
636        let old_chunks = vec![create_test_chunk("ec_123", "src/foo.rs", "foo")];
637        manifest.update(&old_chunks).unwrap();
638
639        // Empty current = all removed
640        let diff = manifest.diff(&[]);
641        assert_eq!(diff.summary.added, 0);
642        assert_eq!(diff.summary.modified, 0);
643        assert_eq!(diff.summary.removed, 1);
644    }
645
646    #[test]
647    fn test_diff_unchanged() {
648        let mut manifest = EmbedManifest::new("my-repo".to_string(), EmbedSettings::default());
649
650        let chunks = vec![create_test_chunk("ec_123", "src/foo.rs", "foo")];
651        manifest.update(&chunks).unwrap();
652
653        // Same chunks = unchanged
654        let diff = manifest.diff(&chunks);
655        assert_eq!(diff.summary.unchanged, 1);
656        assert!(!diff.has_changes());
657    }
658
659    #[test]
660    fn test_batches() {
661        let manifest = EmbedManifest::new("my-repo".to_string(), EmbedSettings::default());
662
663        let chunks: Vec<_> = (0..5)
664            .map(|i| {
665                create_test_chunk(&format!("ec_{i}"), &format!("src/f{i}.rs"), &format!("f{i}"))
666            })
667            .collect();
668
669        let diff = manifest.diff(&chunks);
670        let batches = diff.batches(2);
671
672        // 5 chunks / batch size 2 = 3 batches
673        assert_eq!(batches.len(), 3);
674        assert_eq!(batches[0].chunks.len(), 2);
675        assert_eq!(batches[1].chunks.len(), 2);
676        assert_eq!(batches[2].chunks.len(), 1);
677    }
678
679    #[test]
680    fn test_load_if_exists() {
681        let temp_dir = TempDir::new().unwrap();
682        let manifest_path = temp_dir.path().join("nonexistent.bin");
683
684        // Non-existent returns None
685        let result = EmbedManifest::load_if_exists(&manifest_path).unwrap();
686        assert!(result.is_none());
687
688        // Existing returns Some
689        let mut manifest = EmbedManifest::new("test".to_string(), EmbedSettings::default());
690        manifest.save(&manifest_path).unwrap();
691
692        let result = EmbedManifest::load_if_exists(&manifest_path).unwrap();
693        assert!(result.is_some());
694    }
695
696    #[test]
697    fn test_collision_detection() {
698        let mut manifest = EmbedManifest::new("my-repo".to_string(), EmbedSettings::default());
699
700        // Create two chunks with same ID but different hashes
701        let mut chunk1 = create_test_chunk("ec_same", "src/foo.rs", "foo");
702        let mut chunk2 = create_test_chunk("ec_same", "src/bar.rs", "bar");
703        chunk1.full_hash = "hash1".to_string();
704        chunk2.full_hash = "hash2".to_string();
705
706        let result = manifest.update(&[chunk1, chunk2]);
707        assert!(matches!(result, Err(EmbedError::HashCollision { .. })));
708    }
709
710    #[test]
711    fn test_settings_match() {
712        let manifest = EmbedManifest::new("my-repo".to_string(), EmbedSettings::default());
713
714        assert!(manifest.settings_match(&EmbedSettings::default()));
715
716        let mut different = EmbedSettings::default();
717        different.max_tokens = 2000;
718        assert!(!manifest.settings_match(&different));
719    }
720}