1use std::collections::HashSet;
22use std::time::{SystemTime, UNIX_EPOCH};
23
24use anyhow::Result;
25
26use crate::analysis::walker::{detect_language, WalkedFile};
27use crate::analysis::{parse_file, public_api_symbols, StaticFileAnalysis};
28use crate::health::staleness::{
29 apply_reparse_staleness, cascade_staleness_to_gotchas, ReparseDiff,
30};
31use crate::store::record::{
32 Category, ConfidenceScore, FileRecord, QualityScore, Record, RecordLifecycle, RecordSource,
33 RecordVersion, StalenessScore, StalenessSignal, StalenessTier,
34};
35use crate::store::Store;
36
37fn now_secs() -> u64 {
38 SystemTime::now()
39 .duration_since(UNIX_EPOCH)
40 .unwrap_or_default()
41 .as_secs()
42}
43
44pub async fn reparse_impl(
54 store: &Store,
55 repo_root: &std::path::Path,
56 rel_path: &str,
57) -> Result<()> {
58 let abs_path = repo_root.join(rel_path);
59 let file_key = format!("file:{rel_path}");
60 let now = now_secs();
61
62 if !abs_path.exists() {
64 if let Some(mut record) = store.get(&file_key).await? {
66 record.staleness.value = 1.0;
67 record.staleness.tier = StalenessTier::Tombstone;
68 record.staleness.signals.push(StalenessSignal::FileDeleted);
69 record.staleness.computed_at = now;
70 record.updated_at = now;
71 record.version.logical_clock += 1;
72 record.version.wall_clock = now;
73 store.put(&file_key, &record).await?;
74 }
75 return Ok(());
76 }
77
78 let language = detect_language(&abs_path);
80 let size_bytes = std::fs::metadata(&abs_path).map(|m| m.len()).unwrap_or(0);
81
82 let walked = WalkedFile {
83 abs_path: abs_path.clone(),
84 rel_path: rel_path.to_string(),
85 language,
86 size_bytes,
87 mtime_secs: 0, };
89
90 let analysis = match parse_file(&walked) {
92 Ok(a) => a,
93 Err(e) => {
94 tracing::warn!("reparse: parse failed for {rel_path}: {e}");
95 return Ok(());
96 }
97 };
98
99 let existing = store.get(&file_key).await?;
101
102 let Some(mut record) = existing else {
104 let file_record = build_file_record_from_analysis(rel_path, &analysis, &walked, now);
105 let new_record = Record {
106 key: file_key.clone(),
107 value: file_record.purpose.clone(),
108 payload: serde_json::to_value(&file_record).ok(),
109 category: Category::File,
110 priority: crate::store::record::Priority::Normal,
111 tags: vec![],
112 created_at: now,
113 updated_at: now,
114 ref_url: None,
115 staleness: StalenessScore::fresh(),
116 lifecycle: RecordLifecycle::Active,
117 version: RecordVersion {
118 device_id: crate::store::stable_device_id(),
119 logical_clock: 1,
120 wall_clock: now,
121 },
122 quality: QualityScore::layer0_default(),
123 access_count: 0,
124 last_accessed: 0,
125 source: RecordSource::StaticAnalysis,
126 confidence: ConfidenceScore::for_new_record(&RecordSource::StaticAnalysis),
127 gap_analysis_score: 0.0,
128 };
129 store.put(&file_key, &new_record).await?;
130 return Ok(());
131 };
132
133 let old_fr: FileRecord = match record.payload_as::<FileRecord>() {
135 Some(fr) => fr,
136 None => {
137 let file_record = build_file_record_from_analysis(rel_path, &analysis, &walked, now);
139 record.value = file_record.purpose.clone();
140 record.payload = serde_json::to_value(&file_record).ok();
141 record.updated_at = now;
142 record.version.logical_clock += 1;
143 record.version.wall_clock = now;
144 store.put(&file_key, &record).await?;
145 return Ok(());
146 }
147 };
148
149 let diff = compute_diff(&old_fr, &analysis);
151 if diff.is_empty() {
152 return Ok(());
153 }
154
155 let merged = FileRecord {
157 path: rel_path.to_string(),
158 purpose: old_fr.purpose,
159 entry_points: public_api_symbols(&analysis),
160 imports: analysis.imports.iter().map(|i| i.path.clone()).collect(),
161 gotcha_keys: old_fr.gotcha_keys.clone(),
162 decision_keys: old_fr.decision_keys,
163 todos: analysis.todos,
164 unsafe_count: analysis.unsafe_count,
165 unwrap_count: analysis.unwrap_count,
166 change_frequency: old_fr.change_frequency,
167 last_author: old_fr.last_author,
168 is_hotspot: old_fr.is_hotspot,
169 token_cost_estimate: (walked.size_bytes / 4).min(u32::MAX as u64) as u32,
170 last_modified_session: now,
171 content_hash: analysis.content_hash.clone(),
172 line_count: analysis.line_count,
173 blast_radius: old_fr.blast_radius,
174 propagated_staleness: old_fr.propagated_staleness,
175 };
176
177 record.value = merged.purpose.clone();
178 record.payload = serde_json::to_value(&merged).ok();
179
180 let signals = apply_reparse_staleness(&mut record, &diff);
182
183 record.updated_at = now;
185 record.version.logical_clock += 1;
186 record.version.wall_clock = now;
187
188 if !signals.is_empty() {
190 if let Err(e) = cascade_staleness_to_gotchas(store, &merged).await {
191 tracing::warn!("reparse: cascade to gotchas failed for {rel_path}: {e}");
192 }
193 }
194
195 store.put(&file_key, &record).await?;
197
198 Ok(())
199}
200
201pub async fn reparse_staged(
211 store: &Store,
212 repo_root: &std::path::Path,
213 rel_path: &str,
214) -> Result<Option<(String, Record)>> {
215 let abs_path = repo_root.join(rel_path);
216 let file_key = format!("file:{rel_path}");
217 let now = now_secs();
218
219 if !abs_path.exists() {
221 if let Some(mut record) = store.get(&file_key).await? {
222 record.staleness.value = 1.0;
223 record.staleness.tier = StalenessTier::Tombstone;
224 record.staleness.signals.push(StalenessSignal::FileDeleted);
225 record.staleness.computed_at = now;
226 record.updated_at = now;
227 record.version.logical_clock += 1;
228 record.version.wall_clock = now;
229 return Ok(Some((file_key, record)));
230 }
231 return Ok(None);
232 }
233
234 let language = detect_language(&abs_path);
236 let size_bytes = std::fs::metadata(&abs_path).map(|m| m.len()).unwrap_or(0);
237 let walked = WalkedFile {
238 abs_path: abs_path.clone(),
239 rel_path: rel_path.to_string(),
240 language,
241 size_bytes,
242 mtime_secs: 0,
243 };
244 let analysis = match parse_file(&walked) {
245 Ok(a) => a,
246 Err(e) => {
247 tracing::warn!("reparse_staged: parse failed for {rel_path}: {e}");
248 return Ok(None);
249 }
250 };
251
252 let existing = store.get(&file_key).await?;
254
255 let Some(mut record) = existing else {
257 let file_record = build_file_record_from_analysis(rel_path, &analysis, &walked, now);
258 let new_record = Record {
259 key: file_key.clone(),
260 value: file_record.purpose.clone(),
261 payload: serde_json::to_value(&file_record).ok(),
262 category: Category::File,
263 priority: crate::store::record::Priority::Normal,
264 tags: vec![],
265 created_at: now,
266 updated_at: now,
267 ref_url: None,
268 staleness: StalenessScore::fresh(),
269 lifecycle: RecordLifecycle::Active,
270 version: RecordVersion {
271 device_id: crate::store::stable_device_id(),
272 logical_clock: 1,
273 wall_clock: now,
274 },
275 quality: QualityScore::layer0_default(),
276 access_count: 0,
277 last_accessed: 0,
278 source: RecordSource::StaticAnalysis,
279 confidence: ConfidenceScore::for_new_record(&RecordSource::StaticAnalysis),
280 gap_analysis_score: 0.0,
281 };
282 return Ok(Some((file_key, new_record)));
283 };
284
285 let old_fr: FileRecord = match record.payload_as::<FileRecord>() {
287 Some(fr) => fr,
288 None => {
289 let file_record = build_file_record_from_analysis(rel_path, &analysis, &walked, now);
290 record.value = file_record.purpose.clone();
291 record.payload = serde_json::to_value(&file_record).ok();
292 record.updated_at = now;
293 record.version.logical_clock += 1;
294 record.version.wall_clock = now;
295 return Ok(Some((file_key, record)));
296 }
297 };
298
299 let diff = compute_diff(&old_fr, &analysis);
301 if diff.is_empty() {
302 return Ok(None);
303 }
304
305 let merged = FileRecord {
307 path: rel_path.to_string(),
308 purpose: old_fr.purpose,
309 entry_points: public_api_symbols(&analysis),
310 imports: analysis.imports.iter().map(|i| i.path.clone()).collect(),
311 gotcha_keys: old_fr.gotcha_keys.clone(),
312 decision_keys: old_fr.decision_keys,
313 todos: analysis.todos,
314 unsafe_count: analysis.unsafe_count,
315 unwrap_count: analysis.unwrap_count,
316 change_frequency: old_fr.change_frequency,
317 last_author: old_fr.last_author,
318 is_hotspot: old_fr.is_hotspot,
319 token_cost_estimate: (walked.size_bytes / 4).min(u32::MAX as u64) as u32,
320 last_modified_session: now,
321 content_hash: analysis.content_hash.clone(),
322 line_count: analysis.line_count,
323 blast_radius: old_fr.blast_radius,
324 propagated_staleness: old_fr.propagated_staleness,
325 };
326
327 record.value = merged.purpose.clone();
328 record.payload = serde_json::to_value(&merged).ok();
329
330 let _signals = apply_reparse_staleness(&mut record, &diff);
332 record.updated_at = now;
333 record.version.logical_clock += 1;
334 record.version.wall_clock = now;
335
336 Ok(Some((file_key, record)))
340}
341
342fn build_file_record_from_analysis(
344 rel_path: &str,
345 analysis: &StaticFileAnalysis,
346 walked: &WalkedFile,
347 now: u64,
348) -> FileRecord {
349 FileRecord {
350 path: rel_path.to_string(),
351 purpose: String::new(),
352 entry_points: public_api_symbols(analysis),
353 imports: analysis.imports.iter().map(|i| i.path.clone()).collect(),
354 gotcha_keys: vec![],
355 decision_keys: vec![],
356 todos: analysis.todos.clone(),
357 unsafe_count: analysis.unsafe_count,
358 unwrap_count: analysis.unwrap_count,
359 change_frequency: 0,
360 last_author: None,
361 is_hotspot: false,
362 token_cost_estimate: (walked.size_bytes / 4).min(u32::MAX as u64) as u32,
363 last_modified_session: now,
364 content_hash: analysis.content_hash.clone(),
365 line_count: analysis.line_count,
366 blast_radius: None,
367 propagated_staleness: None,
368 }
369}
370
371pub fn compute_diff(old: &FileRecord, new: &StaticFileAnalysis) -> ReparseDiff {
373 let new_public_api = public_api_symbols(new);
374 let old_eps: HashSet<&str> = old.entry_points.iter().map(|s| s.as_str()).collect();
375 let new_eps: HashSet<&str> = new_public_api.iter().map(|s| s.as_str()).collect();
376
377 let entry_points_added: Vec<String> = new_eps
378 .difference(&old_eps)
379 .map(|s| s.to_string())
380 .collect();
381 let entry_points_removed: Vec<String> = old_eps
382 .difference(&new_eps)
383 .map(|s| s.to_string())
384 .collect();
385
386 let old_imports: HashSet<&str> = old.imports.iter().map(|s| s.as_str()).collect();
387 let new_imports: HashSet<&str> = new.imports.iter().map(|s| s.path.as_str()).collect();
388
389 let imports_added: Vec<String> = new_imports
390 .difference(&old_imports)
391 .map(|s| s.to_string())
392 .collect();
393 let imports_removed: Vec<String> = old_imports
394 .difference(&new_imports)
395 .map(|s| s.to_string())
396 .collect();
397
398 let todos_changed = old.todos.len() != new.todos.len()
399 || old
400 .todos
401 .iter()
402 .zip(new.todos.iter())
403 .any(|(a, b)| a.text != b.text || a.line != b.line);
404
405 let unsafe_delta = new.unsafe_count as i32 - old.unsafe_count as i32;
406 let unwrap_delta = new.unwrap_count as i32 - old.unwrap_count as i32;
407
408 ReparseDiff {
409 entry_points_added,
410 entry_points_removed,
411 imports_added,
412 imports_removed,
413 todos_changed,
414 unsafe_delta,
415 unwrap_delta,
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use crate::analysis::parser::{ImportKind, ImportStatement};
423 use crate::analysis::walker::Language;
424 use tempfile::TempDir;
425
426 fn make_old_file_record() -> FileRecord {
427 FileRecord {
428 path: "src/main.rs".into(),
429 purpose: "Main entry point".into(),
430 entry_points: vec!["main".into(), "old_fn".into()],
431 imports: vec!["std::io".into()],
432 gotcha_keys: vec!["gotcha:test".into()],
433 decision_keys: vec![],
434 todos: vec![],
435 unsafe_count: 0,
436 unwrap_count: 1,
437 change_frequency: 5,
438 last_author: Some("dev".into()),
439 is_hotspot: true,
440 token_cost_estimate: 100,
441 last_modified_session: 1_000_000,
442 content_hash: None,
443 line_count: 0,
444 blast_radius: None,
445 propagated_staleness: None,
446 }
447 }
448
449 fn make_new_analysis() -> StaticFileAnalysis {
450 StaticFileAnalysis {
451 path: "src/main.rs".into(),
452 language: Language::Rust,
453 entry_points: vec!["main".into(), "new_fn".into()],
454 exported_types: vec![],
455 imports: vec![
456 ImportStatement::new("std::io", ImportKind::Normal, 1),
457 ImportStatement::new("anyhow", ImportKind::Normal, 2),
458 ],
459 todos: vec![],
460 unsafe_count: 0,
461 unwrap_count: 0,
462 panic_count: 0,
463 branch_count: 0,
464 module_doc: None,
465 content_hash: None,
466 line_count: 0,
467 }
468 }
469
470 #[test]
471 fn compute_diff_detects_entry_point_changes() {
472 let old = make_old_file_record();
473 let new = make_new_analysis();
474 let diff = compute_diff(&old, &new);
475
476 assert!(diff.entry_points_added.contains(&"new_fn".to_string()));
477 assert!(diff.entry_points_removed.contains(&"old_fn".to_string()));
478 }
479
480 #[test]
481 fn compute_diff_detects_import_changes() {
482 let old = make_old_file_record();
483 let new = make_new_analysis();
484 let diff = compute_diff(&old, &new);
485
486 assert!(diff.imports_added.contains(&"anyhow".to_string()));
487 assert!(diff.imports_removed.is_empty());
488 }
489
490 #[test]
491 fn compute_diff_detects_unwrap_delta() {
492 let old = make_old_file_record();
493 let new = make_new_analysis();
494 let diff = compute_diff(&old, &new);
495 assert_eq!(diff.unwrap_delta, -1);
496 }
497
498 #[test]
499 fn compute_diff_empty_when_identical() {
500 let old = FileRecord {
501 path: "src/main.rs".into(),
502 purpose: "test".into(),
503 entry_points: vec!["main".into()],
504 imports: vec!["std::io".into()],
505 gotcha_keys: vec![],
506 decision_keys: vec![],
507 todos: vec![],
508 unsafe_count: 0,
509 unwrap_count: 0,
510 change_frequency: 0,
511 last_author: None,
512 is_hotspot: false,
513 token_cost_estimate: 0,
514 last_modified_session: 0,
515 content_hash: None,
516 line_count: 0,
517 blast_radius: None,
518 propagated_staleness: None,
519 };
520 let new = StaticFileAnalysis {
521 path: "src/main.rs".into(),
522 language: Language::Rust,
523 entry_points: vec!["main".into()],
524 exported_types: vec![],
525 imports: vec![ImportStatement::new("std::io", ImportKind::Normal, 1)],
526 todos: vec![],
527 unsafe_count: 0,
528 unwrap_count: 0,
529 panic_count: 0,
530 branch_count: 0,
531 module_doc: None,
532 content_hash: None,
533 line_count: 0,
534 };
535 let diff = compute_diff(&old, &new);
536 assert!(diff.is_empty());
537 }
538
539 #[tokio::test]
540 async fn reparse_creates_stub_for_unknown_file() {
541 let dir = TempDir::new().unwrap();
542 let repo = dir.path();
543 std::fs::write(repo.join("new_file.rs"), "pub fn hello() {}").unwrap();
544
545 let store = Store::open(repo).await.unwrap();
546 reparse_impl(&store, repo, "new_file.rs").await.unwrap();
547
548 let record = store.get("file:new_file.rs").await.unwrap();
549 assert!(record.is_some());
550 let r = record.unwrap();
551 assert_eq!(r.category, Category::File);
552
553 let fr: FileRecord = r.payload_as::<FileRecord>().unwrap();
554 assert!(fr.purpose.is_empty());
555 assert!(fr.entry_points.contains(&"hello".to_string()));
556
557 store.close().await.unwrap();
558 }
559
560 #[tokio::test]
561 async fn reparse_marks_deleted_file_as_tombstone() {
562 let dir = TempDir::new().unwrap();
563 let repo = dir.path();
564
565 let store = Store::open(repo).await.unwrap();
566
567 let fr = FileRecord {
568 path: "gone.rs".into(),
569 purpose: String::new(),
570 entry_points: vec![],
571 imports: vec![],
572 gotcha_keys: vec![],
573 decision_keys: vec![],
574 todos: vec![],
575 unsafe_count: 0,
576 unwrap_count: 0,
577 change_frequency: 0,
578 last_author: None,
579 is_hotspot: false,
580 token_cost_estimate: 0,
581 last_modified_session: 0,
582 content_hash: None,
583 line_count: 0,
584 blast_radius: None,
585 propagated_staleness: None,
586 };
587 let record = Record {
588 key: "file:gone.rs".into(),
589 value: serde_json::to_string(&fr).unwrap(),
590 category: Category::File,
591 priority: crate::store::record::Priority::Normal,
592 tags: vec![],
593 created_at: 1_000_000,
594 updated_at: 1_000_000,
595 ref_url: None,
596 staleness: StalenessScore::fresh(),
597 lifecycle: RecordLifecycle::Active,
598 version: RecordVersion {
599 device_id: uuid::Uuid::new_v4(),
600 logical_clock: 1,
601 wall_clock: 1_000_000,
602 },
603 quality: QualityScore::layer0_default(),
604 access_count: 0,
605 last_accessed: 0,
606 source: RecordSource::StaticAnalysis,
607 confidence: ConfidenceScore::for_new_record(&RecordSource::StaticAnalysis),
608 gap_analysis_score: 0.0,
609 payload: None,
610 };
611 store.put("file:gone.rs", &record).await.unwrap();
612
613 reparse_impl(&store, repo, "gone.rs").await.unwrap();
614
615 let updated = store.get("file:gone.rs").await.unwrap().unwrap();
616 assert_eq!(updated.staleness.tier, StalenessTier::Tombstone);
617 assert!(updated.staleness.value >= 1.0 - f32::EPSILON);
618
619 store.close().await.unwrap();
620 }
621
622 #[tokio::test]
623 async fn reparse_preserves_enrichment_fields_and_bumps_staleness() {
624 let dir = TempDir::new().unwrap();
625 let repo = dir.path();
626 std::fs::write(repo.join("lib.rs"), "pub fn new_fn() {}\npub fn kept() {}").unwrap();
627
628 let store = Store::open(repo).await.unwrap();
629
630 let fr = FileRecord {
631 path: "lib.rs".into(),
632 purpose: "Core library".into(),
633 entry_points: vec!["old_fn".into(), "kept".into()],
634 imports: vec![],
635 gotcha_keys: vec!["gotcha:important".into()],
636 decision_keys: vec!["decision:arch".into()],
637 todos: vec![],
638 unsafe_count: 0,
639 unwrap_count: 0,
640 change_frequency: 10,
641 last_author: Some("ioni".into()),
642 is_hotspot: true,
643 token_cost_estimate: 50,
644 last_modified_session: 1_000_000,
645 content_hash: None,
646 line_count: 0,
647 blast_radius: None,
648 propagated_staleness: None,
649 };
650 let record = Record {
651 key: "file:lib.rs".into(),
652 value: fr.purpose.clone(),
653 payload: serde_json::to_value(&fr).ok(),
654 category: Category::File,
655 priority: crate::store::record::Priority::Normal,
656 tags: vec![],
657 created_at: 1_000_000,
658 updated_at: 1_000_000,
659 ref_url: None,
660 staleness: StalenessScore::fresh(),
661 lifecycle: RecordLifecycle::Active,
662 version: RecordVersion {
663 device_id: uuid::Uuid::new_v4(),
664 logical_clock: 1,
665 wall_clock: 1_000_000,
666 },
667 quality: QualityScore::layer0_default(),
668 access_count: 3,
669 last_accessed: 1_000_000,
670 source: RecordSource::StaticAnalysis,
671 confidence: ConfidenceScore::for_new_record(&RecordSource::StaticAnalysis),
672 gap_analysis_score: 0.0,
673 };
674 store.put("file:lib.rs", &record).await.unwrap();
675
676 reparse_impl(&store, repo, "lib.rs").await.unwrap();
677
678 let updated = store.get("file:lib.rs").await.unwrap().unwrap();
679 let updated_fr: FileRecord = updated.payload_as::<FileRecord>().unwrap();
680
681 assert_eq!(updated_fr.purpose, "Core library");
683 assert_eq!(updated_fr.gotcha_keys, vec!["gotcha:important"]);
684 assert_eq!(updated_fr.decision_keys, vec!["decision:arch"]);
685 assert_eq!(updated_fr.change_frequency, 10);
686 assert_eq!(updated_fr.last_author.as_deref(), Some("ioni"));
687 assert!(updated_fr.is_hotspot);
688
689 assert!(updated_fr.entry_points.contains(&"new_fn".to_string()));
691 assert!(updated_fr.entry_points.contains(&"kept".to_string()));
692 assert!(!updated_fr.entry_points.contains(&"old_fn".to_string()));
693 assert!(updated_fr.content_hash.is_some());
694 assert!(updated_fr.line_count > 0);
695
696 assert!(updated.staleness.value > 0.0);
698
699 store.close().await.unwrap();
700 }
701
702 #[tokio::test]
703 async fn reparse_noop_when_no_structural_changes() {
704 let dir = TempDir::new().unwrap();
705 let repo = dir.path();
706 std::fs::write(repo.join("stable.rs"), "pub fn run() {}").unwrap();
707
708 let store = Store::open(repo).await.unwrap();
709
710 let fr = FileRecord {
711 path: "stable.rs".into(),
712 purpose: "Stable module".into(),
713 entry_points: vec!["run".into()],
714 imports: vec![],
715 gotcha_keys: vec![],
716 decision_keys: vec![],
717 todos: vec![],
718 unsafe_count: 0,
719 unwrap_count: 0,
720 change_frequency: 0,
721 last_author: None,
722 is_hotspot: false,
723 token_cost_estimate: 50,
724 last_modified_session: 1_000_000,
725 content_hash: None,
726 line_count: 0,
727 blast_radius: None,
728 propagated_staleness: None,
729 };
730 let record = Record {
731 key: "file:stable.rs".into(),
732 value: fr.purpose.clone(),
733 payload: serde_json::to_value(&fr).ok(),
734 category: Category::File,
735 priority: crate::store::record::Priority::Normal,
736 tags: vec![],
737 created_at: 1_000_000,
738 updated_at: 1_000_000,
739 ref_url: None,
740 staleness: StalenessScore::fresh(),
741 lifecycle: RecordLifecycle::Active,
742 version: RecordVersion {
743 device_id: uuid::Uuid::new_v4(),
744 logical_clock: 1,
745 wall_clock: 1_000_000,
746 },
747 quality: QualityScore::layer0_default(),
748 access_count: 0,
749 last_accessed: 0,
750 source: RecordSource::StaticAnalysis,
751 confidence: ConfidenceScore::for_new_record(&RecordSource::StaticAnalysis),
752 gap_analysis_score: 0.0,
753 };
754 store.put("file:stable.rs", &record).await.unwrap();
755
756 reparse_impl(&store, repo, "stable.rs").await.unwrap();
757
758 let after = store.get("file:stable.rs").await.unwrap().unwrap();
760 assert_eq!(after.version.logical_clock, 1);
761 assert_eq!(after.updated_at, 1_000_000);
762
763 store.close().await.unwrap();
764 }
765
766 #[tokio::test]
767 async fn reparse_preserves_exported_types_in_entry_points() {
768 let dir = TempDir::new().unwrap();
769 let repo = dir.path();
770 std::fs::write(repo.join("models.rs"), "pub struct Widget;\n").unwrap();
771
772 let store = Store::open(repo).await.unwrap();
773 reparse_impl(&store, repo, "models.rs").await.unwrap();
774
775 let record = store.get("file:models.rs").await.unwrap().unwrap();
776 let fr: FileRecord = record.payload_as::<FileRecord>().unwrap();
777 assert!(fr.entry_points.contains(&"Widget".to_string()));
778 assert!(fr.content_hash.is_some());
779 assert!(fr.line_count > 0);
780
781 store.close().await.unwrap();
782 }
783}