@@ -54,6 +54,8 @@ class DayPlan:
5454 day_name : str
5555 day_type : str
5656 activities : List [Activity ]
57+ friction : float = 1.0
58+ target_minutes : Optional [Dict [str , int ]] = None
5759
5860
5961OUTDOOR_ACTIVITIES = {"outdoor_run" , "bike_ride" , "park_visit" , "hiking" , "outdoor_walk" }
@@ -357,6 +359,10 @@ def _generate_week_activities(
357359 special = self ._calendar_provider .get_special_period_effects (current_date )
358360 activities = self .apply_special_period_effects (activities , special )
359361
362+ target_minutes : Dict [str , int ] = {}
363+ for activity in activities :
364+ target_minutes [activity .name ] = target_minutes .get (activity .name , 0 ) + activity .base_duration_minutes
365+
360366 for activity in activities :
361367 self ._apply_friction (activity , daily_friction )
362368 if activity .name == "sleep" :
@@ -368,7 +374,16 @@ def _generate_week_activities(
368374 activity .actual_duration ,
369375 )
370376
371- week_schedule .append (DayPlan (current_date , day_name , day_type , activities ))
377+ week_schedule .append (
378+ DayPlan (
379+ current_date ,
380+ day_name ,
381+ day_type ,
382+ activities ,
383+ friction = daily_friction ,
384+ target_minutes = target_minutes ,
385+ )
386+ )
372387
373388 return week_schedule
374389
@@ -536,6 +551,7 @@ def generate_complete_week(
536551 week_seed : int ,
537552 templates : Optional [Dict [str , ActivityTemplate ]] = None ,
538553 yearly_budget : Optional [YearlyBudget ] = None ,
554+ debug : bool = False ,
539555 ) -> Dict [str , object ]:
540556 random .seed (week_seed )
541557 templates = templates or DEFAULT_TEMPLATES
@@ -545,11 +561,50 @@ def generate_complete_week(
545561 {f"{ plan .day_name } ({ plan .date .isoformat ()} )" : plan .activities for plan in week_plans }
546562 )
547563
564+ debug_trace : Optional [Dict [str , Any ]] = None
565+ debug_days : Dict [str , Dict [str , Any ]] = {}
566+ if debug :
567+ for plan in week_plans :
568+ debug_days [plan .day_name ] = {
569+ "date" : plan .date .isoformat (),
570+ "classification" : plan .day_type ,
571+ "friction" : plan .friction ,
572+ "target_minutes" : dict (plan .target_minutes or {}),
573+ "activities_before_compression" : [
574+ {
575+ "name" : activity .name ,
576+ "base" : activity .base_duration_minutes ,
577+ "waste_multiplier" : activity .waste_multiplier ,
578+ "optional" : activity .optional ,
579+ "priority" : activity .priority ,
580+ }
581+ for activity in plan .activities
582+ ],
583+ }
584+ debug_trace = {
585+ "profile" : profile .name ,
586+ "budget" : asdict (profile .budget ),
587+ "per_day" : debug_days ,
588+ }
589+
548590 compression_metadata : Dict [str , Dict [str , object ]] = {}
549591 for plan in week_plans :
550592 compressed , metadata = self ._compress_day_if_needed (plan .activities )
551593 plan .activities = compressed
552594 compression_metadata [plan .date .isoformat ()] = metadata
595+ if debug :
596+ day_debug = debug_days .get (plan .day_name )
597+ if day_debug is not None :
598+ day_debug ["activities_after_compression" ] = [
599+ {
600+ "name" : activity .name ,
601+ "base" : activity .base_duration_minutes ,
602+ "actual" : activity .actual_duration ,
603+ "optional" : activity .optional ,
604+ "priority" : activity .priority ,
605+ }
606+ for activity in plan .activities
607+ ]
553608
554609 normalized_inputs : List [Dict [str , Any ]] = []
555610 for plan in week_plans :
@@ -559,6 +614,20 @@ def generate_complete_week(
559614 event .day = plan .day_name
560615 events = self .fill_free_time (events )
561616 events = self .apply_micro_jitter (events )
617+ if debug :
618+ day_debug = debug_days .get (plan .day_name )
619+ if day_debug is not None :
620+ day_debug ["events" ] = [
621+ {
622+ "activity" : event .activity .name
623+ if isinstance (event .activity , Activity )
624+ else event .activity ,
625+ "start_minutes" : event .start_minutes ,
626+ "end_minutes" : event .end_minutes ,
627+ "duration" : max (0 , event .end_minutes - event .start_minutes ),
628+ }
629+ for event in events
630+ ]
562631 day_offset = (plan .date - start_date ).days
563632 weekday_index = plan .date .weekday ()
564633 for event in events :
@@ -609,7 +678,18 @@ def generate_complete_week(
609678 )
610679 summary_hours = self ._generate_summary (events_payload )
611680
612- return {
681+ weekly_totals_minutes : Dict [str , int ] = {}
682+ if debug :
683+ for event in events_payload :
684+ activity_name = str (event .get ("activity" ))
685+ duration = int (event .get ("duration_minutes" , 0 ) or 0 )
686+ weekly_totals_minutes [activity_name ] = (
687+ weekly_totals_minutes .get (activity_name , 0 ) + duration
688+ )
689+ if debug_trace is not None :
690+ debug_trace ["weekly_totals_from_events" ] = weekly_totals_minutes
691+
692+ result : Dict [str , Any ] = {
613693 "person" : profile .name ,
614694 "week_start" : start_date .isoformat (),
615695 "events" : events_payload ,
@@ -623,6 +703,11 @@ def generate_complete_week(
623703 },
624704 }
625705
706+ if debug and debug_trace is not None :
707+ result ["debug_trace" ] = debug_trace
708+
709+ return result
710+
626711 def select_profile (self , archetype : str ) -> Tuple [PersonProfile , Dict [str , ActivityTemplate ]]:
627712 archetype = archetype .lower ()
628713 if archetype not in self ._profile_factory :
0 commit comments