@@ -415,18 +415,19 @@ def analyze(self) -> Analysis:
415415 """
416416 cache_file = self .cache_dir / "analysis_cache.json"
417417
418- # Try to load existing cached analysis
419- cached_pyapplication = None
418+ # Try to load existing cached analysis
419+ cached = None
420420 if not self .rebuild_analysis and cache_file .exists ():
421421 try :
422- cached_pyapplication = self ._load_pyapplication_from_cache (cache_file )
423- logger .info ("Loaded cached analysis" )
422+ cached = self ._load_pyapplication_from_cache (cache_file )
423+ if cached is not None :
424+ logger .info ("Loaded cached analysis" )
424425 except Exception as e :
425426 logger .warning (f"Failed to load cache: { e } . Rebuilding analysis." )
426- cached_pyapplication = None
427+ cached = None
427428
428429 # Build symbol table from cached application if available (if no available, the build a new one)
429- symbol_table = self ._build_symbol_table (cached_pyapplication . symbol_table if cached_pyapplication else {})
430+ symbol_table = self ._build_symbol_table (cached . application . symbol_table if cached else {})
430431
431432 resolve_unresolved_constructors (symbol_table )
432433
@@ -463,40 +464,57 @@ def analyze(self) -> Analysis:
463464
464465 # L3/L4 dataflow emission rebuilt on the v2 tree in Stage 3+
465466
466- # Save to cache
467- self ._save_analysis_cache (app , cache_file )
468-
469- return Analysis (
467+ # Build the v2 envelope, then persist it (the cache stores the full
468+ # ``Analysis`` envelope so a reused cache round-trips schema_version).
469+ analysis = Analysis (
470470 max_level = self .analysis_level ,
471471 k_limit = self .options .graph_field_depth ,
472472 application = app ,
473473 )
474+ self ._save_analysis_cache (analysis , cache_file )
475+
476+ return analysis
477+
478+ def _load_pyapplication_from_cache (self , cache_file : Path ) -> Optional [Analysis ]:
479+ """Load a cached v2 ``Analysis`` envelope from file.
480+
481+ A cache written by an older (v1) analyzer stored a bare
482+ ``PyApplication`` with no ``schema_version``; such a payload no longer
483+ validates as an ``Analysis`` (or carries the wrong ``schema_version``).
484+ In that case we log and return ``None`` so the caller treats it as a
485+ cache miss and rebuilds from scratch — rather than crashing.
474486
475- def _load_pyapplication_from_cache (self , cache_file : Path ) -> PyApplication :
476- """Load cached analysis from file.
477-
478487 Args:
479488 cache_file: Path to the cache file
480-
489+
481490 Returns:
482- PyApplication: The cached application data
491+ Optional[Analysis]: The cached envelope, or ``None`` if the cache is
492+ stale/incompatible and should be rebuilt.
483493 """
484494 with cache_file .open ('r' ) as f :
485495 data = f .read ()
486- return model_validate_json (PyApplication , data )
487-
488- def _save_analysis_cache (self , app : PyApplication , cache_file : Path ) -> None :
489- """Save analysis to cache file.
490-
496+ try :
497+ cached = model_validate_json (Analysis , data )
498+ except Exception :
499+ logger .info ("stale/incompatible analysis cache — rebuilding" )
500+ return None
501+ if getattr (cached , "schema_version" , None ) != "2.0.0" :
502+ logger .info ("stale/incompatible analysis cache (schema_version) — rebuilding" )
503+ return None
504+ return cached
505+
506+ def _save_analysis_cache (self , analysis : Analysis , cache_file : Path ) -> None :
507+ """Save the v2 ``Analysis`` envelope to the cache file.
508+
491509 Args:
492- app : The PyApplication to cache
510+ analysis : The Analysis envelope to cache
493511 cache_file: Path to save the cache file
494512 """
495513 # Ensure cache directory exists
496514 cache_file .parent .mkdir (parents = True , exist_ok = True )
497-
515+
498516 with cache_file .open ('w' ) as f :
499- f .write (model_dump_json (app , indent = 2 ))
517+ f .write (model_dump_json (analysis , indent = 2 ))
500518
501519 logger .info (f"Analysis cached to { cache_file } " )
502520
0 commit comments