From 44d873f8effbf0a49bd0ff096d4866b88a737250 Mon Sep 17 00:00:00 2001 From: Lennart van der Molen Date: Thu, 13 Nov 2025 22:21:42 +0100 Subject: [PATCH] Add optional debug trace output to MK2 scheduling --- engines/engine_mk2.py | 89 +++++++++++++++++++++++++++++++++++++++++- engines/web_adapter.py | 13 ++++-- rigs/workforce_rig.py | 3 +- 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/engines/engine_mk2.py b/engines/engine_mk2.py index 5b35023..808ae4c 100644 --- a/engines/engine_mk2.py +++ b/engines/engine_mk2.py @@ -54,6 +54,8 @@ class DayPlan: day_name: str day_type: str activities: List[Activity] + friction: float = 1.0 + target_minutes: Optional[Dict[str, int]] = None OUTDOOR_ACTIVITIES = {"outdoor_run", "bike_ride", "park_visit", "hiking", "outdoor_walk"} @@ -357,6 +359,10 @@ def _generate_week_activities( special = self._calendar_provider.get_special_period_effects(current_date) activities = self.apply_special_period_effects(activities, special) + target_minutes: Dict[str, int] = {} + for activity in activities: + target_minutes[activity.name] = target_minutes.get(activity.name, 0) + activity.base_duration_minutes + for activity in activities: self._apply_friction(activity, daily_friction) if activity.name == "sleep": @@ -368,7 +374,16 @@ def _generate_week_activities( activity.actual_duration, ) - week_schedule.append(DayPlan(current_date, day_name, day_type, activities)) + week_schedule.append( + DayPlan( + current_date, + day_name, + day_type, + activities, + friction=daily_friction, + target_minutes=target_minutes, + ) + ) return week_schedule @@ -536,6 +551,7 @@ def generate_complete_week( week_seed: int, templates: Optional[Dict[str, ActivityTemplate]] = None, yearly_budget: Optional[YearlyBudget] = None, + debug: bool = False, ) -> Dict[str, object]: random.seed(week_seed) templates = templates or DEFAULT_TEMPLATES @@ -545,11 +561,50 @@ def generate_complete_week( {f"{plan.day_name} ({plan.date.isoformat()})": plan.activities for plan in week_plans} ) + debug_trace: Optional[Dict[str, Any]] = None + debug_days: Dict[str, Dict[str, Any]] = {} + if debug: + for plan in week_plans: + debug_days[plan.day_name] = { + "date": plan.date.isoformat(), + "classification": plan.day_type, + "friction": plan.friction, + "target_minutes": dict(plan.target_minutes or {}), + "activities_before_compression": [ + { + "name": activity.name, + "base": activity.base_duration_minutes, + "waste_multiplier": activity.waste_multiplier, + "optional": activity.optional, + "priority": activity.priority, + } + for activity in plan.activities + ], + } + debug_trace = { + "profile": profile.name, + "budget": asdict(profile.budget), + "per_day": debug_days, + } + compression_metadata: Dict[str, Dict[str, object]] = {} for plan in week_plans: compressed, metadata = self._compress_day_if_needed(plan.activities) plan.activities = compressed compression_metadata[plan.date.isoformat()] = metadata + if debug: + day_debug = debug_days.get(plan.day_name) + if day_debug is not None: + day_debug["activities_after_compression"] = [ + { + "name": activity.name, + "base": activity.base_duration_minutes, + "actual": activity.actual_duration, + "optional": activity.optional, + "priority": activity.priority, + } + for activity in plan.activities + ] normalized_inputs: List[Dict[str, Any]] = [] for plan in week_plans: @@ -559,6 +614,20 @@ def generate_complete_week( event.day = plan.day_name events = self.fill_free_time(events) events = self.apply_micro_jitter(events) + if debug: + day_debug = debug_days.get(plan.day_name) + if day_debug is not None: + day_debug["events"] = [ + { + "activity": event.activity.name + if isinstance(event.activity, Activity) + else event.activity, + "start_minutes": event.start_minutes, + "end_minutes": event.end_minutes, + "duration": max(0, event.end_minutes - event.start_minutes), + } + for event in events + ] day_offset = (plan.date - start_date).days weekday_index = plan.date.weekday() for event in events: @@ -609,7 +678,18 @@ def generate_complete_week( ) summary_hours = self._generate_summary(events_payload) - return { + weekly_totals_minutes: Dict[str, int] = {} + if debug: + for event in events_payload: + activity_name = str(event.get("activity")) + duration = int(event.get("duration_minutes", 0) or 0) + weekly_totals_minutes[activity_name] = ( + weekly_totals_minutes.get(activity_name, 0) + duration + ) + if debug_trace is not None: + debug_trace["weekly_totals_from_events"] = weekly_totals_minutes + + result: Dict[str, Any] = { "person": profile.name, "week_start": start_date.isoformat(), "events": events_payload, @@ -623,6 +703,11 @@ def generate_complete_week( }, } + if debug and debug_trace is not None: + result["debug_trace"] = debug_trace + + return result + def select_profile(self, archetype: str) -> Tuple[PersonProfile, Dict[str, ActivityTemplate]]: archetype = archetype.lower() if archetype not in self._profile_factory: diff --git a/engines/web_adapter.py b/engines/web_adapter.py index 39139c1..1d221d1 100644 --- a/engines/web_adapter.py +++ b/engines/web_adapter.py @@ -281,13 +281,17 @@ def mk1_run_web(archetype: str, week_start: Optional[str], seed: Any) -> SchemaP return _ensure_schema(payload, rig="default", seed=seed_value, archetype=archetype_key or "office") -def mk2_run_calendar_web(archetype: str, week_start: Optional[str], seed: Any) -> SchemaPayload: +def mk2_run_calendar_web( + archetype: str, week_start: Optional[str], seed: Any, debug: bool = False +) -> SchemaPayload: archetype_key = str(archetype or "office").strip().lower() seed_value = _coerce_seed(seed) start_date = _coerce_start_date(week_start) or date.today() profile, templates = _MK2_RIG.select_profile(archetype_key) - result = _MK2_RIG.generate_complete_week(profile, start_date, seed_value, templates, None) + result = _MK2_RIG.generate_complete_week( + profile, start_date, seed_value, templates, None, debug=debug + ) payload: MutableMapping[str, Any] = dict(result) payload.setdefault("issues", []) @@ -307,6 +311,7 @@ def mk2_run_workforce_web( week_start: Optional[str], seed: Any, yearly_budget: Optional[Mapping[str, Any]], + debug: bool = False, ) -> SchemaPayload: archetype_key = str(archetype or "office").strip().lower() seed_value = _coerce_seed(seed) @@ -315,7 +320,9 @@ def mk2_run_workforce_web( profile, templates = _MK2_RIG.select_profile(archetype_key) budget = _build_yearly_budget(yearly_budget) - result = _MK2_RIG.generate_complete_week(profile, start_date, seed_value, templates, budget) + result = _MK2_RIG.generate_complete_week( + profile, start_date, seed_value, templates, budget, debug=debug + ) payload: MutableMapping[str, Any] = dict(result) payload.setdefault("issues", []) diff --git a/rigs/workforce_rig.py b/rigs/workforce_rig.py index 19734b5..286ee2a 100644 --- a/rigs/workforce_rig.py +++ b/rigs/workforce_rig.py @@ -101,9 +101,10 @@ def generate_complete_week( week_seed: int, templates: Optional[Dict[str, ActivityTemplate]] = None, yearly_budget: Optional[YearlyBudget] = None, + debug: bool = False, ) -> Dict[str, object]: """Delegate generation to the configured engine.""" return self._engine.generate_complete_week( - profile, start_date, week_seed, templates, yearly_budget + profile, start_date, week_seed, templates, yearly_budget, debug=debug )