diff --git a/SESSION.md b/SESSION.md index dad1f9a..2332fcf 100644 --- a/SESSION.md +++ b/SESSION.md @@ -2,7 +2,7 @@ ## Current Goal -Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase is a dense `Decision Assistant` operator workspace with grouped recall, broader replay baselines, note templates, quick-save shortcuts, and concrete OKX announcements/system-status read-only sources. This run closed H18/F19/G18/D20 by adding handoff cleanup-action readiness / resume backlog grouping, digest proof-SLA backlog routing / freshness-debt summary, template recovery shortcuts / audit lane grouping, and setup-family promotion evidence queue / backlog trend handoff. Local validation passed, the current working tree was deployed, and remote smoke confirmed the latest Decision Assistant fields on the live host. +Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase is a dense `Decision Assistant` operator workspace with grouped recall, broader replay baselines, note templates, quick-save shortcuts, and concrete OKX announcements/system-status read-only sources. This run closed H19/F20/G19/D21 by adding handoff cleanup execution logs / grouped resume summaries, digest freshness-debt trend / SLA recovery recommendations, template audit-lane recommendations / shortcut telemetry, and setup-family promotion evidence aging / backlog comparison summaries. Local validation passed, the current working tree was deployed, and remote smoke confirmed the latest Decision Assistant fields on the live host. ## Current Branch @@ -100,6 +100,10 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. - latest_decision_assistant_digest_backlog_slice: proof_sla_backlog_freshness_debt_summary - latest_decision_assistant_template_audit_lane_slice: recovery_shortcuts_audit_lane_grouping - latest_decision_assistant_replay_evidence_backlog_slice: promotion_evidence_queue_backlog_trend_handoff +- latest_decision_assistant_handoff_execution_slice: cleanup_execution_log_grouped_resume_handoff_summary +- latest_decision_assistant_digest_debt_trend_slice: freshness_debt_trend_sla_recovery_recommendations +- latest_decision_assistant_template_shortcut_telemetry_slice: audit_lane_recommendations_shortcut_outcome_telemetry +- latest_decision_assistant_replay_evidence_aging_slice: promotion_evidence_aging_backlog_comparison_summary - latest_decision_assistant_summary_status: implemented - latest_decision_assistant_change_summary_status: implemented - latest_decision_assistant_evidence_status: implemented @@ -184,10 +188,14 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. - latest_decision_assistant_digest_backlog_status: implemented - latest_decision_assistant_template_audit_lane_status: implemented - latest_decision_assistant_replay_evidence_backlog_status: implemented +- latest_decision_assistant_handoff_execution_status: implemented +- latest_decision_assistant_digest_debt_trend_status: implemented +- latest_decision_assistant_template_shortcut_telemetry_status: implemented +- latest_decision_assistant_replay_evidence_aging_status: implemented - latest_decision_assistant_workbench_status: visible_local_smoke - latest_remote_decision_assistant_deploy_status: deployed_current_head - latest_remote_decision_assistant_smoke_status: passed -- latest_remote_decision_assistant_review_packet_status: cleanup_backlog_proof_debt_template_lanes_promotion_evidence_visible +- latest_remote_decision_assistant_review_packet_status: cleanup_execution_debt_recovery_shortcut_telemetry_evidence_aging_visible - latest_p7_selector_choice: continue_periodic_analysis_decision_monitoring_with_maturity_rollup - latest_p14_slice: webhook_transport_retry - latest_p15_pxx_tasklist_status: ready @@ -266,6 +274,12 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. ## Completed In This Run +- Added handoff cleanup execution log / grouped resume handoff summary fields (`cleanup_execution_log`, `grouped_resume_handoff_summary`, and `next_cleanup_execution_prompt`) plus cockpit visibility. +- Added digest freshness-debt trend / SLA recovery recommendation fields (`freshness_debt_trend`, `freshness_debt_trend_counts`, `sla_recovery_recommendations`, and `next_sla_recovery_prompt`) plus cockpit visibility. +- Added template audit-lane recommendation / shortcut outcome telemetry fields (`audit_lane_recommendation`, `shortcut_outcome_telemetry`, `template_audit_lane_recommendations`, and `shortcut_outcome_status_counts`) plus cockpit visibility. +- Added setup-family promotion evidence aging / backlog comparison fields (`promotion_evidence_aging_status`, `promotion_evidence_aging_queue`, `promotion_backlog_comparison_status`, and `promotion_backlog_comparison_summary`) plus cockpit visibility. +- Passed focused validation: `unittest tests.unit.test_operator_workspace_service`, `node --check src/web/app.js`, `unittest tests.unit.test_operator_workspace_service tests.unit.test_api_static_server tests.unit.test_api_server`, full `unittest discover -s tests/unit` (713 tests), and `scripts/run_script_static_checks.py`. +- Deployed the current working tree to `http://192.168.3.90:8000`; deploy smoke passed 11 endpoints, `run_remote_ops_smoke.py` passed 9 checks, and remote field smoke confirmed cleanup execution, freshness-debt trend, shortcut telemetry, and promotion evidence aging fields. - Added handoff cleanup-action readiness / resume backlog grouping fields (`cleanup_action_readiness`, `cleanup_action_ready_count`, `resume_backlog_group`, `resume_backlog_groups`, and `next_cleanup_action_prompt`) plus cockpit visibility. - Added digest proof-SLA backlog routing / freshness-debt summary fields (`proof_sla_backlog_route`, `proof_sla_backlog_priority`, `freshness_debt_score`, `proof_sla_backlog_queue`, and `freshness_debt_summary`) plus cockpit visibility. - Added template recovery shortcuts / audit lane grouping fields (`recovery_action_shortcuts`, `audit_lane_group`, `audit_lane_rank`, `template_audit_lane_groups`, and `next_recovery_action_shortcut`) plus cockpit visibility. @@ -576,11 +590,12 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. - Latest H16/F17/G16/D18 validation passed: 713 unit tests, 73 script static checks, `node --check src/web/app.js`, deploy smoke passed 11 endpoints, remote ops smoke passed 9 checks, and remote field smoke returned `status=passed`. - Latest H18/F19/G18/D20 validation passed: 713 unit tests, 73 script static checks, `node --check src/web/app.js`, deploy smoke passed 11 endpoints, remote ops smoke passed 9 checks, and remote field smoke returned `status=passed`. - Latest H17/F18/G17/D19 validation passed: 713 unit tests, 73 script static checks, `node --check src/web/app.js`, deploy smoke passed 11 endpoints, remote ops smoke passed 9 checks, and remote field smoke returned `status=passed`. +- Latest H19/F20/G19/D21 validation passed: 713 unit tests, 73 script static checks, `node --check src/web/app.js`, deploy smoke passed 11 endpoints, remote ops smoke passed 9 checks, and remote field smoke confirmed cleanup execution, debt trend, shortcut telemetry, and evidence aging fields. - Real-live order submission remains intentionally disabled because the OKX order adapter is still not a real-live submitter. ## Exact Next Step -Continue the Decision Assistant dense operator workspace with the next bounded slice: H19 handoff cleanup execution log / grouped resume handoff summary, F20 digest freshness-debt trend / SLA recovery recommendations, G19 template audit-lane recommendations / shortcut outcome telemetry, and D21 setup-family promotion evidence aging / backlog comparison summary. PXX remains closed; do not create P25/P26 automatically, and do not enable real trading. +Continue the Decision Assistant dense operator workspace with the next bounded product slice from the remaining non-trading assistant gaps: E5 external read-only macro/news calendar and event-window summary, W1 pre/intraday/post daily workflow rhythm checklist, D22 replay failure/missed-opportunity attribution rollup, and F21 personalized digest high-value filter / importance templates. PXX remains closed; do not create P25/P26 automatically, and do not enable real trading. ## Validation Commands Used Recently diff --git a/TASKS.md b/TASKS.md index d96db55..f42182b 100644 --- a/TASKS.md +++ b/TASKS.md @@ -131,10 +131,14 @@ - [x] F19 deepen digest recall with proof SLA backlog routing / freshness-debt summary - [x] G18 continue review template ergonomics with template recovery action shortcuts / audit lane grouping - [x] D20 extend replay comparison with setup-family promotion evidence queue / backlog trend handoff -- [ ] H19 continue compact operator workspace polish with handoff cleanup execution log / grouped resume handoff summary -- [ ] F20 deepen digest recall with freshness-debt trend / SLA recovery recommendations -- [ ] G19 continue review template ergonomics with template audit-lane recommendations / shortcut outcome telemetry -- [ ] D21 extend replay comparison with setup-family promotion evidence aging / backlog comparison summary +- [x] H19 continue compact operator workspace polish with handoff cleanup execution log / grouped resume handoff summary +- [x] F20 deepen digest recall with freshness-debt trend / SLA recovery recommendations +- [x] G19 continue review template ergonomics with template audit-lane recommendations / shortcut outcome telemetry +- [x] D21 extend replay comparison with setup-family promotion evidence aging / backlog comparison summary +- [ ] E5 external read-only macro/news calendar and event-window summary +- [ ] W1 pre/intraday/post workflow rhythm checklist +- [ ] D22 replay learning failure/missed-opportunity attribution rollup +- [ ] F21 personalized digest high-value filter / importance template - [x] P8 local closure complete - [x] P9 local closure complete - [x] P10 local closure complete diff --git a/src/services/operator/operator_workspace_service.py b/src/services/operator/operator_workspace_service.py index c7c667f..7c7712d 100644 --- a/src/services/operator/operator_workspace_service.py +++ b/src/services/operator/operator_workspace_service.py @@ -1098,6 +1098,9 @@ def _build_decision_assistant_review_entrypoints( item["recovery_action_shortcuts"] = outcome.get("recovery_action_shortcuts") or [] item["audit_lane_group"] = outcome.get("audit_lane_group") item["audit_lane_rank"] = outcome.get("audit_lane_rank") + item["audit_lane_recommendation"] = outcome.get("audit_lane_recommendation") + item["shortcut_outcome_telemetry"] = outcome.get("shortcut_outcome_telemetry") or [] + item["shortcut_outcome_telemetry_count"] = outcome.get("shortcut_outcome_telemetry_count") or 0 item["guardrail_audit_filter"] = outcome.get("guardrail_audit_filter") item["guardrail_audit_filter_reason"] = outcome.get("guardrail_audit_filter_reason") item["guardrail_audit_filtered_trail"] = outcome.get("guardrail_audit_filtered_trail") or [] @@ -1444,6 +1447,22 @@ def _score_template_outcome(*, template_id: str, usage: dict, notes: list[dict]) preference_recovery_status=preference_recovery_status, outcome_status=outcome_status, ) + recovery_action_shortcuts = _template_recovery_action_shortcuts( + template_id=template_id, + recommendation=preference_recovery_recommendation, + audit_lane_group=audit_lane_group, + ) + audit_lane_recommendation = _template_audit_lane_recommendation( + template_id=template_id, + audit_lane_group=audit_lane_group, + recommendation=preference_recovery_recommendation, + ) + shortcut_outcome_telemetry = _template_shortcut_outcome_telemetry( + template_id=template_id, + shortcuts=recovery_action_shortcuts, + audit_lane_group=audit_lane_group, + outcome_status=outcome_status, + ) return { "template_id": template_id, "reuse_count": reuse_count, @@ -1486,13 +1505,12 @@ def _score_template_outcome(*, template_id: str, usage: dict, notes: list[dict]) "guardrail_audit_trail": guardrail_audit_trail[:5], "guardrail_audit_trail_count": len(guardrail_audit_trail), "preference_recovery_recommendation": preference_recovery_recommendation, - "recovery_action_shortcuts": _template_recovery_action_shortcuts( - template_id=template_id, - recommendation=preference_recovery_recommendation, - audit_lane_group=audit_lane_group, - ), + "recovery_action_shortcuts": recovery_action_shortcuts, "audit_lane_group": audit_lane_group, "audit_lane_rank": _template_audit_lane_rank(audit_lane_group), + "audit_lane_recommendation": audit_lane_recommendation, + "shortcut_outcome_telemetry": shortcut_outcome_telemetry, + "shortcut_outcome_telemetry_count": len(shortcut_outcome_telemetry), "guardrail_audit_filter": guardrail_audit_filter, "guardrail_audit_filter_reason": _template_guardrail_audit_filter_reason(guardrail_audit_filter), "guardrail_audit_filtered_trail": guardrail_audit_filtered_trail[:5], @@ -1737,6 +1755,71 @@ def _template_recovery_shortcut_label(action: str) -> str: return "Keep Stable" +def _template_audit_lane_recommendation(*, template_id: str, audit_lane_group: str, recommendation: dict) -> dict: + action = str(recommendation.get("action") or "capture_first_outcome") + priority = str(recommendation.get("priority") or "low") + template_label = template_id.replace("_", " ") or "template" + lane = str(audit_lane_group or "stable") + if lane == "guardrail": + lane_action = "resolve_guardrail" + prompt = f"Keep {template_label} in the guardrail lane until recovery evidence is captured." + elif lane == "recovery": + lane_action = "review_recovery" + prompt = f"Review recovery evidence for {template_label} before restoring preference." + elif lane == "capture": + lane_action = "capture_outcome" + prompt = f"Capture one outcome note for {template_label} before ranking it again." + else: + lane_action = "keep_stable" + prompt = f"{template_label} is stable; keep telemetry on future shortcut use." + return { + "template_id": template_id, + "audit_lane_group": lane, + "audit_lane_rank": _template_audit_lane_rank(lane), + "action": lane_action, + "source_action": action, + "priority": priority, + "prompt": prompt, + } + + +def _template_shortcut_outcome_telemetry( + *, + template_id: str, + shortcuts: list[dict], + audit_lane_group: str, + outcome_status: str, +) -> list[dict]: + telemetry = [] + for index, shortcut in enumerate(shortcuts[:4], start=1): + action_id = str(shortcut.get("action_id") or f"{template_id}:shortcut:{index}") + telemetry.append( + { + "template_id": template_id, + "action_id": action_id, + "label": shortcut.get("label"), + "audit_lane_group": audit_lane_group, + "outcome_status": outcome_status, + "telemetry_status": _template_shortcut_telemetry_status( + audit_lane_group=audit_lane_group, + outcome_status=outcome_status, + ), + "rank": index, + } + ) + return telemetry + + +def _template_shortcut_telemetry_status(*, audit_lane_group: str, outcome_status: str) -> str: + if audit_lane_group == "guardrail" or outcome_status == "review_before_reuse": + return "guardrail_shortcut_watch" + if audit_lane_group == "recovery": + return "recovery_shortcut_watch" + if audit_lane_group == "capture": + return "capture_shortcut_needed" + return "stable_shortcut" + + def _build_template_feedback_loop(*, template_id: str, outcome_status: str, reuse_count: int) -> dict: template_label = template_id.replace("_", " ") or "template" if outcome_status == "review_before_reuse": @@ -1856,6 +1939,27 @@ def _build_template_feedback_aggregation( str(row.get("label") or ""), ) ) + template_audit_lane_recommendations = [ + dict(item.get("audit_lane_recommendation") or {}) + for item in template_outcomes.values() + if dict(item.get("audit_lane_recommendation") or {}) + ] + template_audit_lane_recommendations.sort( + key=lambda row: ( + int(row.get("audit_lane_rank") or 9), + {"high": 1, "medium": 2, "low": 3, "none": 4}.get(str(row.get("priority") or "low"), 3), + str(row.get("template_id") or ""), + ) + ) + shortcut_outcome_telemetry = [ + dict(row) + for item in template_outcomes.values() + for row in list(item.get("shortcut_outcome_telemetry") or []) + ] + shortcut_outcome_status_counts = {} + for row in shortcut_outcome_telemetry: + status = str(row.get("telemetry_status") or "unknown") + shortcut_outcome_status_counts[status] = int(shortcut_outcome_status_counts.get(status) or 0) + 1 guardrail_audit_filter_counts = {} for item in template_outcomes.values(): audit_filter = str(item.get("guardrail_audit_filter") or "unknown") @@ -1939,6 +2043,14 @@ def _build_template_feedback_aggregation( "recovery_action_shortcuts": recovery_action_shortcuts[:6], "recovery_action_shortcut_count": len(recovery_action_shortcuts), "next_recovery_action_shortcut": recovery_action_shortcuts[0] if recovery_action_shortcuts else None, + "template_audit_lane_recommendations": template_audit_lane_recommendations[:6], + "template_audit_lane_recommendation_count": len(template_audit_lane_recommendations), + "shortcut_outcome_telemetry": shortcut_outcome_telemetry[:8], + "shortcut_outcome_telemetry_count": len(shortcut_outcome_telemetry), + "shortcut_outcome_status_counts": shortcut_outcome_status_counts, + "next_shortcut_outcome_prompt": ( + template_audit_lane_recommendations[0].get("prompt") if template_audit_lane_recommendations else None + ), } @@ -2285,6 +2397,11 @@ def _build_decision_assistant_digest_history_summary(history: list[dict]) -> dic ) ) freshness_debt_summary = _build_digest_history_freshness_debt_summary(groups) + freshness_debt_trend_counts = {} + for group in groups: + trend = str(group.get("freshness_debt_trend") or "unknown") + freshness_debt_trend_counts[trend] = int(freshness_debt_trend_counts.get(trend) or 0) + 1 + sla_recovery_recommendations = _build_digest_history_sla_recovery_recommendations(groups) repeated_stale_suppression_count = sum( 1 for group in groups @@ -2366,6 +2483,15 @@ def _build_decision_assistant_digest_history_summary(history: list[dict]) -> dic "proof_sla_backlog_count": len(proof_sla_backlog_queue), "freshness_debt_summary": freshness_debt_summary, "next_freshness_debt_prompt": freshness_debt_summary.get("next_prompt"), + "freshness_debt_trend_counts": freshness_debt_trend_counts, + "sla_recovery_recommendations": sla_recovery_recommendations[:5], + "sla_recovery_recommendation_count": len(sla_recovery_recommendations), + "next_sla_recovery_recommendation": ( + sla_recovery_recommendations[0] if sla_recovery_recommendations else None + ), + "next_sla_recovery_prompt": ( + sla_recovery_recommendations[0].get("prompt") if sla_recovery_recommendations else None + ), } @@ -2441,6 +2567,18 @@ def _build_decision_assistant_digest_history_groups(history: list[dict], *, reco proof_resolution_sla_status=proof_resolution_sla_status, freshness_debt_score=freshness_debt_score, ) + freshness_debt_trend = _digest_history_freshness_debt_trend( + freshness_debt_score=freshness_debt_score, + proof_sla_backlog_route=proof_sla_backlog_route, + proof_resolution_sla_status=proof_resolution_sla_status, + ) + sla_recovery_recommendation = _digest_history_sla_recovery_recommendation( + kind=kind, + proof_sla_backlog_route=proof_sla_backlog_route, + proof_sla_backlog_priority=proof_sla_backlog_priority, + freshness_debt_trend=freshness_debt_trend, + freshness_debt_score=freshness_debt_score, + ) repeated_stale_proof_suppression = _digest_history_repeated_stale_proof_suppression( item_count=len(items), proof_follow_up_stale_queue=proof_follow_up_stale_queue, @@ -2506,6 +2644,8 @@ def _build_decision_assistant_digest_history_groups(history: list[dict], *, reco "proof_sla_backlog_route": proof_sla_backlog_route, "proof_sla_backlog_priority": proof_sla_backlog_priority, "freshness_debt_score": freshness_debt_score, + "freshness_debt_trend": freshness_debt_trend, + "sla_recovery_recommendation": sla_recovery_recommendation, "repeated_stale_proof_suppression": repeated_stale_proof_suppression, "refresh_confirmation_trail": refresh_confirmation_trail, "latest_confirmation_trail_item": refresh_confirmation_trail[0] if refresh_confirmation_trail else None, @@ -2955,6 +3095,84 @@ def _digest_history_freshness_debt_prompt(group: dict) -> str: return f"{kind} freshness debt is clear." +def _digest_history_freshness_debt_trend( + *, + freshness_debt_score: int, + proof_sla_backlog_route: str, + proof_resolution_sla_status: str, +) -> str: + score = int(freshness_debt_score or 0) + route = str(proof_sla_backlog_route or "resolved") + sla_status = str(proof_resolution_sla_status or "resolved") + if score >= 80 or route == "resolve_now" or sla_status == "breached": + return "debt_expanding" + if route == "timestamp_proof" or sla_status == "missing_timestamp_sla": + return "timestamp_gap" + if score >= 40 or route == "watch_queue": + return "debt_watch" + return "debt_clear" + + +def _digest_history_sla_recovery_recommendation( + *, + kind: str, + proof_sla_backlog_route: str, + proof_sla_backlog_priority: str, + freshness_debt_trend: str, + freshness_debt_score: int, +) -> dict: + route = str(proof_sla_backlog_route or "resolved") + priority = str(proof_sla_backlog_priority or "none") + trend = str(freshness_debt_trend or "debt_clear") + if route == "resolve_now": + action = "resolve_now" + label = "Resolve SLA" + prompt = f"Resolve {kind} proof SLA before this digest group is reused." + elif route == "timestamp_proof": + action = "attach_timestamp_proof" + label = "Attach Timestamp" + prompt = f"Attach timestamp proof for {kind} to reduce freshness debt." + elif route == "watch_queue": + action = "refresh_watch_queue" + label = "Refresh Watch" + prompt = f"Refresh or recheck {kind} before it drives the next operator note." + else: + action = "confirm_clear" + label = "Confirmed Clear" + prompt = f"{kind} has no SLA recovery action pending." + return { + "kind": kind, + "action": action, + "label": label, + "priority": priority, + "freshness_debt_trend": trend, + "freshness_debt_score": int(freshness_debt_score or 0), + "prompt": prompt, + } + + +def _build_digest_history_sla_recovery_recommendations(groups: list[dict]) -> list[dict]: + recommendations = [ + dict(group.get("sla_recovery_recommendation") or {}) + for group in groups + if dict(group.get("sla_recovery_recommendation") or {}).get("action") != "confirm_clear" + ] + recommendations.sort( + key=lambda row: ( + {"high": 1, "medium": 2, "low": 3, "none": 4}.get(str(row.get("priority") or "none"), 4), + { + "debt_expanding": 1, + "timestamp_gap": 2, + "debt_watch": 3, + "debt_clear": 4, + }.get(str(row.get("freshness_debt_trend") or "debt_clear"), 4), + -int(row.get("freshness_debt_score") or 0), + str(row.get("kind") or ""), + ) + ) + return recommendations + + def _digest_history_repeated_stale_proof_suppression( *, item_count: int, @@ -3320,6 +3538,18 @@ def _build_decision_assistant_scan_handoff_summary(cards: list[dict]) -> dict: ), "resume_backlog_group": _scan_handoff_resume_backlog_group(str(item.get("handoff_status") or "")), "resume_backlog_priority": _scan_handoff_resume_backlog_priority(str(item.get("handoff_status") or "")), + "cleanup_execution_log": _scan_handoff_cleanup_execution_log( + title=str(item.get("title") or item.get("target_section") or "handoff"), + handoff_status=str(item.get("handoff_status") or ""), + persistence_key=str(item.get("handoff_persistence_key") or item.get("handoff_marker") or ""), + ), + "cleanup_execution_log_count": len( + _scan_handoff_cleanup_execution_log( + title=str(item.get("title") or item.get("target_section") or "handoff"), + handoff_status=str(item.get("handoff_status") or ""), + persistence_key=str(item.get("handoff_persistence_key") or item.get("handoff_marker") or ""), + ) + ), } for item in cards if item.get("handoff_marker") @@ -3353,6 +3583,11 @@ def _build_decision_assistant_scan_handoff_summary(cards: list[dict]) -> dict: if item.get("cleanup_action_readiness") in {"ready_now", "ready_after_refresh", "ready_after_blocker", "monitor"} ] next_cleanup_action = dict(cleanup_action_queue[0] or {}) if cleanup_action_queue else {} + cleanup_execution_log = [ + row + for item in cleanup_action_queue + for row in list(item.get("cleanup_execution_log") or []) + ] next_unresolved_marker = next( ( item @@ -3416,6 +3651,10 @@ def _build_decision_assistant_scan_handoff_summary(cards: list[dict]) -> dict: "cleanup_action_ready_count": len(cleanup_action_queue), "next_cleanup_action_label": next_cleanup_action.get("cleanup_action_label"), "next_cleanup_action_prompt": next_cleanup_action.get("cleanup_action_readiness_reason"), + "cleanup_execution_log": cleanup_execution_log[:8], + "cleanup_execution_log_count": len(cleanup_execution_log), + "grouped_resume_handoff_summary": _build_scan_handoff_grouped_resume_handoff_summary(resume_backlog_groups), + "next_cleanup_execution_prompt": _scan_handoff_next_cleanup_execution_prompt(next_cleanup_action), "handoff_stale_policy": "Persisted handoffs older than their stale window should be reopened and refreshed before reuse.", "handoff_cleanup_recommendation": _scan_handoff_cleanup_recommendation(markers), "snooze_options": [ @@ -3702,6 +3941,94 @@ def _scan_handoff_resume_backlog_group_label(group: str) -> str: return "Carry Forward" +def _scan_handoff_cleanup_execution_action_label(handoff_status: str) -> str: + if handoff_status == "blocked_handoff": + return "Hold Blocker" + if handoff_status == "refresh_handoff": + return "Refresh Then Clear" + if handoff_status == "ready_handoff": + return "Clear Resume State" + if handoff_status == "next_handoff": + return "Monitor Window" + return "Carry Forward" + + +def _scan_handoff_cleanup_execution_log(*, title: str, handoff_status: str, persistence_key: str) -> list[dict]: + key = str(persistence_key or title or "handoff").strip() or "handoff" + readiness = _scan_handoff_cleanup_action_readiness(handoff_status) + backlog_group = _scan_handoff_resume_backlog_group(handoff_status) + log = [ + { + "event": "cleanup_readiness_checked", + "title": title, + "status": readiness, + "persistence_key": key, + "resume_backlog_group": backlog_group, + }, + { + "event": "cleanup_action_selected", + "title": title, + "status": _scan_handoff_stale_persistence_cleanup_status(handoff_status), + "persistence_key": key, + "action_label": _scan_handoff_cleanup_execution_action_label(handoff_status), + }, + { + "event": "grouped_resume_handoff", + "title": title, + "status": backlog_group, + "persistence_key": key, + "resume_backlog_priority": _scan_handoff_resume_backlog_priority(handoff_status), + }, + ] + if readiness in {"ready_now", "ready_after_refresh", "ready_after_blocker", "monitor"}: + log.append( + { + "event": "operator_cleanup_handoff_ready", + "title": title, + "status": readiness, + "persistence_key": key, + "prompt": _scan_handoff_cleanup_action_readiness_reason( + title=title, + handoff_status=handoff_status, + ), + } + ) + return log + + +def _build_scan_handoff_grouped_resume_handoff_summary(resume_backlog_groups: list[dict]) -> list[dict]: + summary = [] + for group in resume_backlog_groups: + label = str(group.get("label") or group.get("resume_backlog_group") or "handoff") + count = int(group.get("count") or 0) + cleanup_ready_count = int(group.get("cleanup_ready_count") or 0) + summary.append( + { + "resume_backlog_group": group.get("resume_backlog_group"), + "label": label, + "count": count, + "cleanup_ready_count": cleanup_ready_count, + "next_marker": group.get("next_marker"), + "next_cleanup_action": group.get("next_cleanup_action"), + "summary": f"{label} has {count} lane(s), {cleanup_ready_count} cleanup-ready.", + } + ) + return summary + + +def _scan_handoff_next_cleanup_execution_prompt(next_cleanup_action: dict) -> str: + if not next_cleanup_action: + return "No cleanup execution is queued." + title = str(next_cleanup_action.get("title") or next_cleanup_action.get("target_section") or "handoff") + readiness_reason = str(next_cleanup_action.get("cleanup_action_readiness_reason") or "") + action_label = str(next_cleanup_action.get("cleanup_action_label") or "") + if readiness_reason: + return readiness_reason + if action_label: + return f"{title}: execute {action_label} when the operator resumes this handoff." + return f"{title}: review cleanup execution before carrying the handoff forward." + + def _build_scan_handoff_stale_persistence_cleanup_queue(markers: list[dict]) -> list[dict]: cleanup_status_rank = { "cleanup_required": 1, @@ -4141,6 +4468,28 @@ def _build_setup_replay_broader_baseline_summary( "promotion_backlog_trend_handoff": ( f"Keep {setup_family} in analysis-only collection until replay evidence reaches the sample threshold." ), + "promotion_evidence_aging_status": "collecting_samples", + "promotion_evidence_aging_prompt": ( + f"{setup_family} promotion evidence is still collecting; add replay samples before review." + ), + "promotion_evidence_aging_queue": [ + { + "evidence_type": "sample_gap", + "aging_status": "collecting_samples", + "aging_rank": 1, + "summary": f"{setup_family} needs replay samples before promotion evidence can age into review.", + } + ], + "promotion_backlog_comparison_status": "collecting_samples", + "promotion_backlog_comparison_summary": { + "status": "collecting_samples", + "priority": "low", + "rank": 5, + "reason": "insufficient_samples", + "sample_gap": 5, + "evidence_queue_count": 1, + "summary": f"{setup_family} backlog remains in sample collection before comparison.", + }, "demotion_history": [], "demotion_history_count": 0, "focus_recommendation": f"Collect samples before changing focus for {setup_family}.", @@ -4327,12 +4676,35 @@ def _build_setup_family_trend_delta( baseline_rows = peer_sessions or fallback_rows if sample_count <= 0 or not baseline_rows: sample_threshold = 5 + sample_gap = max(0, sample_threshold - sample_count) + promotion_evidence_queue = [ + { + "evidence_type": "sample_gap", + "status": "collecting", + "value": sample_gap, + "summary": f"{setup_family} needs more replay samples before promotion evidence can be reviewed.", + } + ] + promotion_evidence_aging_status = _setup_family_promotion_evidence_aging_status( + sample_gap=sample_gap, + review_lane_sla_status="collecting_samples", + promotion_backlog_trend_status="stable_collecting", + ) + promotion_backlog_comparison_summary = _setup_family_promotion_backlog_comparison_summary( + setup_family=setup_family, + promotion_backlog_priority="low", + promotion_backlog_rank=5, + promotion_backlog_reason="insufficient_samples", + promotion_backlog_trend_status="stable_collecting", + sample_gap=sample_gap, + promotion_evidence_queue_count=len(promotion_evidence_queue), + ) return { "status": "idle", "trend_status": "insufficient_samples", "sample_count": sample_count, "sample_threshold": sample_threshold, - "sample_gap": max(0, sample_threshold - sample_count), + "sample_gap": sample_gap, "baseline_count": len(baseline_rows), "framework_delta": 0.0, "avg_r_delta": 0.0, @@ -4374,19 +4746,24 @@ def _build_setup_family_trend_delta( "reason": "insufficient_samples", "action": "Collect replay samples before promotion backlog review.", }, - "promotion_evidence_queue": [ - { - "evidence_type": "sample_gap", - "status": "collecting", - "value": max(0, sample_threshold - sample_count), - "summary": f"{setup_family} needs more replay samples before promotion evidence can be reviewed.", - } - ], - "promotion_evidence_queue_count": 1, + "promotion_evidence_queue": promotion_evidence_queue, + "promotion_evidence_queue_count": len(promotion_evidence_queue), "promotion_backlog_trend_status": "stable_collecting", "promotion_backlog_trend_handoff": ( f"Keep {setup_family} in analysis-only collection until replay evidence reaches the sample threshold." ), + "promotion_evidence_aging_status": promotion_evidence_aging_status, + "promotion_evidence_aging_prompt": _setup_family_promotion_evidence_aging_prompt( + setup_family=setup_family, + promotion_evidence_aging_status=promotion_evidence_aging_status, + ), + "promotion_evidence_aging_queue": _setup_family_promotion_evidence_aging_queue( + setup_family=setup_family, + promotion_evidence_queue=promotion_evidence_queue, + promotion_evidence_aging_status=promotion_evidence_aging_status, + ), + "promotion_backlog_comparison_status": promotion_backlog_comparison_summary.get("status"), + "promotion_backlog_comparison_summary": promotion_backlog_comparison_summary, "demotion_history": [], "demotion_history_count": 0, "focus_recommendation": f"Collect samples before changing focus for {setup_family}.", @@ -4540,6 +4917,25 @@ def _build_setup_family_trend_delta( review_lane_sla_status=review_lane_sla_status, promotion_backlog_priority=promotion_backlog_priority, ) + promotion_evidence_aging_status = _setup_family_promotion_evidence_aging_status( + sample_gap=sample_gap, + review_lane_sla_status=review_lane_sla_status, + promotion_backlog_trend_status=promotion_backlog_trend_status, + ) + promotion_evidence_aging_queue = _setup_family_promotion_evidence_aging_queue( + setup_family=setup_family, + promotion_evidence_queue=promotion_evidence_queue, + promotion_evidence_aging_status=promotion_evidence_aging_status, + ) + promotion_backlog_comparison_summary = _setup_family_promotion_backlog_comparison_summary( + setup_family=setup_family, + promotion_backlog_priority=promotion_backlog_priority, + promotion_backlog_rank=promotion_backlog_rank, + promotion_backlog_reason=promotion_backlog_reason, + promotion_backlog_trend_status=promotion_backlog_trend_status, + sample_gap=sample_gap, + promotion_evidence_queue_count=len(promotion_evidence_queue), + ) return { "status": "active", "trend_status": trend_status, @@ -4598,6 +4994,14 @@ def _build_setup_family_trend_delta( promotion_backlog_trend_status=promotion_backlog_trend_status, promotion_backlog_reason=promotion_backlog_reason, ), + "promotion_evidence_aging_status": promotion_evidence_aging_status, + "promotion_evidence_aging_prompt": _setup_family_promotion_evidence_aging_prompt( + setup_family=setup_family, + promotion_evidence_aging_status=promotion_evidence_aging_status, + ), + "promotion_evidence_aging_queue": promotion_evidence_aging_queue[:6], + "promotion_backlog_comparison_status": promotion_backlog_comparison_summary.get("status"), + "promotion_backlog_comparison_summary": promotion_backlog_comparison_summary, "demotion_history": demotion_history[:5], "demotion_history_count": len(demotion_history), "focus_recommendation": focus_recommendation, @@ -4974,6 +5378,109 @@ def _setup_family_promotion_backlog_trend_handoff( return f"Keep {setup_family} in evidence watch until promotion backlog signals change." +def _setup_family_promotion_evidence_aging_status( + *, + sample_gap: int, + review_lane_sla_status: str, + promotion_backlog_trend_status: str, +) -> str: + sla_status = str(review_lane_sla_status or "within_sla") + trend_status = str(promotion_backlog_trend_status or "evidence_watch") + if sla_status == "breached" or trend_status == "backlog_breached": + return "aging_overdue" + if sla_status in {"due_now", "due_soon"} or trend_status == "backlog_due": + return "aging_due" + if int(sample_gap or 0) > 0: + return "collecting_samples" + if trend_status == "ready_for_review": + return "ready_now" + return "watching_evidence" + + +def _setup_family_promotion_evidence_aging_prompt(*, setup_family: str, promotion_evidence_aging_status: str) -> str: + status = str(promotion_evidence_aging_status or "watching_evidence") + if status == "aging_overdue": + return f"{setup_family} promotion evidence is overdue; review the backlog before carrying the lane forward." + if status == "aging_due": + return f"{setup_family} promotion evidence is due soon; compare backlog signals in the next review pass." + if status == "collecting_samples": + return f"{setup_family} promotion evidence is still collecting; add replay samples before review." + if status == "ready_now": + return f"{setup_family} promotion evidence is ready for analysis-lane review; execution permissions stay unchanged." + return f"{setup_family} promotion evidence remains on watch until backlog signals change." + + +def _setup_family_promotion_evidence_aging_queue( + *, + setup_family: str, + promotion_evidence_queue: list[dict], + promotion_evidence_aging_status: str, +) -> list[dict]: + queue = [] + for index, item in enumerate(promotion_evidence_queue[:6], start=1): + evidence_type = str(item.get("evidence_type") or "promotion_evidence") + queue.append( + { + "evidence_type": evidence_type, + "status": item.get("status"), + "aging_status": promotion_evidence_aging_status, + "aging_rank": index, + "summary": item.get("summary") + or f"{setup_family} {evidence_type} is part of the promotion evidence aging queue.", + } + ) + return queue + + +def _setup_family_promotion_backlog_comparison_summary( + *, + setup_family: str, + promotion_backlog_priority: str, + promotion_backlog_rank: int, + promotion_backlog_reason: str, + promotion_backlog_trend_status: str, + sample_gap: int, + promotion_evidence_queue_count: int, +) -> dict: + status = _setup_family_promotion_backlog_comparison_status( + promotion_backlog_priority=promotion_backlog_priority, + promotion_backlog_trend_status=promotion_backlog_trend_status, + sample_gap=sample_gap, + ) + return { + "status": status, + "priority": promotion_backlog_priority, + "rank": int(promotion_backlog_rank or 0), + "reason": promotion_backlog_reason, + "trend_status": promotion_backlog_trend_status, + "sample_gap": int(sample_gap or 0), + "evidence_queue_count": int(promotion_evidence_queue_count or 0), + "summary": ( + f"{setup_family} backlog comparison is {status}: priority {promotion_backlog_priority}, " + f"reason {promotion_backlog_reason}, sample gap {int(sample_gap or 0)}." + ), + } + + +def _setup_family_promotion_backlog_comparison_status( + *, + promotion_backlog_priority: str, + promotion_backlog_trend_status: str, + sample_gap: int, +) -> str: + priority = str(promotion_backlog_priority or "low") + trend_status = str(promotion_backlog_trend_status or "evidence_watch") + if priority == "urgent" or trend_status == "backlog_breached": + return "urgent_gap" + if priority in {"high", "medium"} or trend_status == "backlog_due": + return "due_gap" + if int(sample_gap or 0) > 0: + return "collecting_samples" + if trend_status == "ready_for_review": + return "ready_for_review" + return "watching" + + def _rank_session_composite(rows: list[dict], *, session_code: str) -> int | None: if not session_code: return None diff --git a/src/web/app.js b/src/web/app.js index 423132d..4eb9d71 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -588,9 +588,13 @@ function renderDecisionAssistant(summary) { ['next persistence cleanup', scanHandoffSummary.next_stale_persistence_cleanup_key || 'n/a'], ['persistence cleanup prompt', scanHandoffSummary.next_stale_persistence_cleanup_prompt || 'n/a'], ['resume backlog groups', formatList((scanHandoffSummary.resume_backlog_groups || []).map((item) => `${item.label || item.resume_backlog_group}: ${item.count ?? 0}`))], + ['grouped resume summary', formatList((scanHandoffSummary.grouped_resume_handoff_summary || []).map((item) => item.summary || `${item.label}: ${item.count ?? 0}`))], ['cleanup action ready', String(scanHandoffSummary.cleanup_action_ready_count ?? 0)], ['next cleanup action', scanHandoffSummary.next_cleanup_action_label || 'n/a'], ['next cleanup action prompt', scanHandoffSummary.next_cleanup_action_prompt || 'n/a'], + ['cleanup execution log', String(scanHandoffSummary.cleanup_execution_log_count ?? 0)], + ['cleanup execution events', formatList((scanHandoffSummary.cleanup_execution_log || []).map((item) => `${item.event}: ${item.status}`))], + ['next cleanup execution', scanHandoffSummary.next_cleanup_execution_prompt || 'n/a'], ['next resume action', scanHandoffSummary.next_resume_action_label || 'n/a'], ['next resume prompt', scanHandoffSummary.next_resume_prompt || 'n/a'], ['handoff cleanup recommendation', scanHandoffSummary.handoff_cleanup_recommendation || 'n/a'], @@ -738,6 +742,10 @@ function renderDecisionAssistant(summary) { ['freshness debt', digestSurface.history_summary?.freshness_debt_summary?.status || 'n/a'], ['freshness debt score', String(digestSurface.history_summary?.freshness_debt_summary?.debt_score_total ?? 0)], ['next freshness debt', digestSurface.history_summary?.next_freshness_debt_prompt || 'n/a'], + ['freshness debt trends', formatJsonInline(digestSurface.history_summary?.freshness_debt_trend_counts || {})], + ['SLA recovery recs', String(digestSurface.history_summary?.sla_recovery_recommendation_count ?? 0)], + ['next SLA recovery', digestSurface.history_summary?.next_sla_recovery_recommendation?.label || 'n/a'], + ['next SLA recovery prompt', digestSurface.history_summary?.next_sla_recovery_prompt || 'n/a'], ['next proof follow-up', digestSurface.history_summary?.next_proof_follow_up_prompt || 'n/a'], ['confirmation trail items', String(digestSurface.history_summary?.refresh_confirmation_trail_count ?? 0)], ['confirmation trail', formatList((digestSurface.history_summary?.refresh_confirmation_trail || []).map((item) => `${item.kind}:${item.proof_label}`))], @@ -838,6 +846,9 @@ function renderDecisionAssistant(summary) { ['SLA backlog route', item.proof_sla_backlog_route || 'n/a'], ['SLA backlog priority', item.proof_sla_backlog_priority || 'n/a'], ['freshness debt', String(item.freshness_debt_score ?? 0)], + ['freshness debt trend', item.freshness_debt_trend || 'n/a'], + ['SLA recovery', item.sla_recovery_recommendation?.label || 'n/a'], + ['SLA recovery prompt', item.sla_recovery_recommendation?.prompt || 'n/a'], ['stale suppression', item.repeated_stale_proof_suppression || 'n/a'], ['latest proof trail', item.latest_confirmation_trail_item?.proof_label || 'n/a'], ['proof trail items', formatList((item.refresh_confirmation_trail || []).map((row) => `${row.title || row.kind}: ${row.proof_label}`))], @@ -909,6 +920,10 @@ function renderDecisionAssistant(summary) { ['audit lane groups', formatList((reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.template_audit_lane_groups || []).map((item) => `${item.label || item.audit_lane_group}: ${item.count ?? 0}`))], ['recovery shortcuts', String(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.recovery_action_shortcut_count ?? 0)], ['next recovery shortcut', reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.next_recovery_action_shortcut?.label || 'n/a'], + ['audit lane recommendations', String(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.template_audit_lane_recommendation_count ?? 0)], + ['next audit lane prompt', reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.next_shortcut_outcome_prompt || 'n/a'], + ['shortcut telemetry', String(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.shortcut_outcome_telemetry_count ?? 0)], + ['shortcut telemetry states', formatJsonInline(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.shortcut_outcome_status_counts || {})], ['preferred template decay', reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.preferred_template_decay_status || 'n/a'], ['preferred decay templates', formatList(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.preferred_template_decay_template_ids || [])], ['max preference decay', formatPercent(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.preferred_template_decay_max)], @@ -976,7 +991,10 @@ function renderDecisionAssistant(summary) { ['recovery recommendation', item.preference_recovery_recommendation?.action || 'n/a'], ['audit lane', item.audit_lane_group || 'n/a'], ['audit lane rank', String(item.audit_lane_rank ?? 'n/a')], + ['audit lane recommendation', item.audit_lane_recommendation?.action || 'n/a'], + ['audit lane prompt', item.audit_lane_recommendation?.prompt || 'n/a'], ['recovery shortcuts', formatList((item.recovery_action_shortcuts || []).map((row) => row.label || row.action_id))], + ['shortcut telemetry', formatList((item.shortcut_outcome_telemetry || []).map((row) => `${row.label || row.action_id}: ${row.telemetry_status}`))], ['audit filter', item.guardrail_audit_filter || 'n/a'], ['audit filter reason', item.guardrail_audit_filter_reason || 'n/a'], ['guardrail audit trail', formatList((item.guardrail_audit_trail || []).map((row) => `${row.label || row.signal}: ${row.resolution_status}`))], @@ -1297,6 +1315,11 @@ function renderDecisionAssistant(summary) { ['promotion evidence', formatList((setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_evidence_queue || []).map((item) => `${item.evidence_type}: ${item.status}`))], ['backlog trend', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_backlog_trend_status || 'n/a'], ['backlog handoff', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_backlog_trend_handoff || 'n/a'], + ['evidence aging', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_evidence_aging_status || 'n/a'], + ['evidence aging prompt', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_evidence_aging_prompt || 'n/a'], + ['evidence aging queue', formatList((setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_evidence_aging_queue || []).map((item) => `${item.evidence_type}: ${item.aging_status}`))], + ['backlog comparison', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_backlog_comparison_status || 'n/a'], + ['backlog comparison summary', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_backlog_comparison_summary?.summary || 'n/a'], ['demotion history', String(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.demotion_history_count ?? 0)], ['demotion reasons', formatList((setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.demotion_history || []).map((item) => item.reason || item.event))], ['family sample threshold', String(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.sample_threshold ?? 'n/a')], diff --git a/tests/unit/test_operator_workspace_service.py b/tests/unit/test_operator_workspace_service.py index b1f0256..19a158c 100644 --- a/tests/unit/test_operator_workspace_service.py +++ b/tests/unit/test_operator_workspace_service.py @@ -218,6 +218,12 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("recovery_action_shortcuts", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"]) self.assertIn("recovery_action_shortcut_count", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"]) self.assertIn("next_recovery_action_shortcut", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"]) + self.assertIn("template_audit_lane_recommendations", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"]) + self.assertIn("template_audit_lane_recommendation_count", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"]) + self.assertIn("shortcut_outcome_telemetry", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"]) + self.assertIn("shortcut_outcome_telemetry_count", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"]) + self.assertIn("shortcut_outcome_status_counts", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"]) + self.assertIn("next_shortcut_outcome_prompt", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"]) self.assertEqual(summary["review_entrypoints"]["note_templates"][0]["reuse_count"], 1) self.assertEqual(summary["review_entrypoints"]["note_templates"][0]["reuse_status"], "saved_reuse") self.assertEqual(summary["review_entrypoints"]["note_templates"][0]["outcome_status"], "useful_signal") @@ -239,6 +245,9 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("recovery_action_shortcuts", summary["review_entrypoints"]["note_templates"][0]) self.assertIn("audit_lane_group", summary["review_entrypoints"]["note_templates"][0]) self.assertIn("audit_lane_rank", summary["review_entrypoints"]["note_templates"][0]) + self.assertIn("audit_lane_recommendation", summary["review_entrypoints"]["note_templates"][0]) + self.assertIn("shortcut_outcome_telemetry", summary["review_entrypoints"]["note_templates"][0]) + self.assertIn("shortcut_outcome_telemetry_count", summary["review_entrypoints"]["note_templates"][0]) self.assertIn("guardrail_audit_filter", summary["review_entrypoints"]["note_templates"][0]) self.assertIn("guardrail_audit_filter_reason", summary["review_entrypoints"]["note_templates"][0]) self.assertIn("guardrail_audit_filtered_trail", summary["review_entrypoints"]["note_templates"][0]) @@ -284,6 +293,11 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("proof_sla_backlog_count", summary["digest_surface"]["history_summary"]) self.assertIn("freshness_debt_summary", summary["digest_surface"]["history_summary"]) self.assertIn("next_freshness_debt_prompt", summary["digest_surface"]["history_summary"]) + self.assertIn("freshness_debt_trend_counts", summary["digest_surface"]["history_summary"]) + self.assertIn("sla_recovery_recommendations", summary["digest_surface"]["history_summary"]) + self.assertIn("sla_recovery_recommendation_count", summary["digest_surface"]["history_summary"]) + self.assertIn("next_sla_recovery_recommendation", summary["digest_surface"]["history_summary"]) + self.assertIn("next_sla_recovery_prompt", summary["digest_surface"]["history_summary"]) self.assertIn("refresh_prompts", summary["digest_surface"]["history_summary"]) self.assertIn("digest_history_groups", summary) self.assertIn("group_status", summary["digest_history_groups"][0]) @@ -315,6 +329,8 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("proof_sla_backlog_route", summary["digest_history_groups"][0]) self.assertIn("proof_sla_backlog_priority", summary["digest_history_groups"][0]) self.assertIn("freshness_debt_score", summary["digest_history_groups"][0]) + self.assertIn("freshness_debt_trend", summary["digest_history_groups"][0]) + self.assertIn("sla_recovery_recommendation", summary["digest_history_groups"][0]) self.assertIn("repeated_stale_proof_suppression", summary["digest_history_groups"][0]) self.assertIn("refresh_confirmation_trail", summary["digest_history_groups"][0]) self.assertIn("latest_confirmation_trail_item", summary["digest_history_groups"][0]) @@ -374,6 +390,11 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("promotion_evidence_queue_count", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) self.assertIn("promotion_backlog_trend_status", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) self.assertIn("promotion_backlog_trend_handoff", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) + self.assertIn("promotion_evidence_aging_status", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) + self.assertIn("promotion_evidence_aging_prompt", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) + self.assertIn("promotion_evidence_aging_queue", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) + self.assertIn("promotion_backlog_comparison_status", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) + self.assertIn("promotion_backlog_comparison_summary", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) self.assertIn("demotion_history", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) self.assertIn("demotion_history_count", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) self.assertIn("sample_threshold", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) @@ -430,6 +451,10 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("cleanup_action_ready_count", summary["scan_handoff_summary"]) self.assertIn("next_cleanup_action_label", summary["scan_handoff_summary"]) self.assertIn("next_cleanup_action_prompt", summary["scan_handoff_summary"]) + self.assertIn("cleanup_execution_log", summary["scan_handoff_summary"]) + self.assertIn("cleanup_execution_log_count", summary["scan_handoff_summary"]) + self.assertIn("grouped_resume_handoff_summary", summary["scan_handoff_summary"]) + self.assertIn("next_cleanup_execution_prompt", summary["scan_handoff_summary"]) self.assertIn("handoff_cleanup_recommendation", summary["scan_handoff_summary"]) self.assertTrue(summary["scan_handoff_summary"]["snooze_options"]) self.assertIn("snooze_seconds", summary["scan_handoff_summary"]["handoff_markers"][0]) @@ -451,6 +476,8 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("cleanup_action_readiness_reason", summary["scan_handoff_summary"]["handoff_markers"][0]) self.assertIn("resume_backlog_group", summary["scan_handoff_summary"]["handoff_markers"][0]) self.assertIn("resume_backlog_priority", summary["scan_handoff_summary"]["handoff_markers"][0]) + self.assertIn("cleanup_execution_log", summary["scan_handoff_summary"]["handoff_markers"][0]) + self.assertIn("cleanup_execution_log_count", summary["scan_handoff_summary"]["handoff_markers"][0]) self.assertTrue(summary["external_context_summary"]["operator_questions"]) self.assertTrue(summary["external_context_summary"]["source_cards"]) self.assertEqual(summary["external_context_summary"]["exchange_status"]["status"], "active")