diff --git a/SESSION.md b/SESSION.md index 06dc707..dad1f9a 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 H17/F18/G17/D19 by adding handoff resume history / stale-persistence cleanup, digest proof-resolution SLA / repeated-stale suppression, template recovery recommendations / audit filtering, and setup-family review-lane SLA / promotion backlog prioritization. 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 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. ## Current Branch @@ -96,6 +96,10 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. - latest_decision_assistant_digest_proof_sla_slice: proof_resolution_sla_repeated_stale_suppression - latest_decision_assistant_template_recovery_recommendation_slice: recovery_recommendations_audit_filtering - latest_decision_assistant_replay_backlog_slice: review_lane_sla_promotion_backlog_priority +- latest_decision_assistant_handoff_backlog_slice: cleanup_action_readiness_resume_backlog_grouping +- 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_summary_status: implemented - latest_decision_assistant_change_summary_status: implemented - latest_decision_assistant_evidence_status: implemented @@ -176,10 +180,14 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. - latest_decision_assistant_digest_proof_sla_status: implemented - latest_decision_assistant_template_recovery_recommendation_status: implemented - latest_decision_assistant_replay_backlog_status: implemented +- latest_decision_assistant_handoff_backlog_status: implemented +- 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_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: resume_history_proof_sla_template_filter_review_backlog_visible +- latest_remote_decision_assistant_review_packet_status: cleanup_backlog_proof_debt_template_lanes_promotion_evidence_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 @@ -258,6 +266,12 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. ## Completed In This Run +- 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. +- Added setup-family promotion evidence queue / backlog trend handoff fields (`promotion_evidence_queue`, `promotion_evidence_queue_count`, `promotion_backlog_trend_status`, and `promotion_backlog_trend_handoff`) 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 backlog, proof debt, template audit lanes, and promotion evidence fields. - Added handoff resume-history / stale-persistence cleanup fields (`resume_history`, `resume_history_count`, `stale_persistence_cleanup_queue`, `stale_persistence_cleanup_count`, and `next_stale_persistence_cleanup_prompt`) plus cockpit visibility. - Added digest proof-resolution SLA / repeated-stale suppression fields (`proof_resolution_sla_status`, `proof_resolution_sla_seconds`, `proof_resolution_sla_queue`, `proof_resolution_sla_breach_count`, and `repeated_stale_proof_suppression_count`). - Added template recovery recommendation / audit-filter fields (`preference_recovery_recommendation`, `preference_recovery_recommendations`, `guardrail_audit_filter`, `guardrail_audit_filter_counts`, and `guardrail_audit_filtered_trail`). @@ -560,12 +574,13 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. - P24 remote ops smoke passed on `http://192.168.3.90:8000` with 9 passed checks, warnings=[], blockers=[]. - PXX final validation passed: 709 unit tests, 73 script static checks, `node --check src/web/app.js`, P24 remote ops smoke passed, and closure gate passed. - 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`. - 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: H18 handoff cleanup action readiness / resume backlog grouping, F19 digest proof SLA backlog routing / freshness-debt summary, G18 template recovery action shortcuts / audit lane grouping, and D20 setup-family promotion evidence queue / backlog trend handoff. 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 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. ## Validation Commands Used Recently diff --git a/TASKS.md b/TASKS.md index 0801da2..d96db55 100644 --- a/TASKS.md +++ b/TASKS.md @@ -96,6 +96,10 @@ - [x] Decision Assistant F18 digest proof resolution SLA / repeated-stale proof suppression slice complete - [x] Decision Assistant G17 template recovery recommendations / audit filtering slice complete - [x] Decision Assistant D19 setup-family review-lane SLA / promotion backlog prioritization slice complete +- [x] Decision Assistant H18 handoff cleanup action readiness / resume backlog grouping slice complete +- [x] Decision Assistant F19 digest proof SLA backlog routing / freshness-debt summary slice complete +- [x] Decision Assistant G18 template recovery action shortcuts / audit lane grouping slice complete +- [x] Decision Assistant D20 setup-family promotion evidence queue / backlog trend handoff slice complete ## Next Up @@ -123,10 +127,14 @@ - [x] F18 deepen digest recall with proof resolution SLA / repeated-stale proof suppression - [x] G17 continue review template ergonomics with template recovery recommendations / audit filtering - [x] D19 extend replay comparison with setup-family review-lane SLA / promotion backlog prioritization -- [ ] H18 continue compact operator workspace polish with handoff cleanup action readiness / resume backlog grouping -- [ ] F19 deepen digest recall with proof SLA backlog routing / freshness-debt summary -- [ ] G18 continue review template ergonomics with template recovery action shortcuts / audit lane grouping -- [ ] D20 extend replay comparison with setup-family promotion evidence queue / backlog trend handoff +- [x] H18 continue compact operator workspace polish with handoff cleanup action readiness / resume backlog grouping +- [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] P8 local closure complete - [x] P9 local closure complete - [x] P10 local closure complete @@ -230,6 +238,10 @@ - [x] latest_decision_assistant_digest_proof_sla_slice: proof_resolution_sla_repeated_stale_suppression - [x] latest_decision_assistant_template_recovery_recommendation_slice: recovery_recommendations_audit_filtering - [x] latest_decision_assistant_replay_backlog_slice: review_lane_sla_promotion_backlog_priority +- [x] latest_decision_assistant_handoff_backlog_slice: cleanup_action_readiness_resume_backlog_grouping +- [x] latest_decision_assistant_digest_backlog_slice: proof_sla_backlog_freshness_debt_summary +- [x] latest_decision_assistant_template_audit_lane_slice: recovery_shortcuts_audit_lane_grouping +- [x] latest_decision_assistant_replay_evidence_backlog_slice: promotion_evidence_queue_backlog_trend_handoff - [x] latest_decision_assistant_summary_status: implemented - [x] latest_decision_assistant_change_summary_status: implemented - [x] latest_decision_assistant_evidence_status: implemented @@ -307,10 +319,14 @@ - [x] latest_decision_assistant_digest_proof_sla_status: implemented - [x] latest_decision_assistant_template_recovery_recommendation_status: implemented - [x] latest_decision_assistant_replay_backlog_status: implemented +- [x] latest_decision_assistant_handoff_backlog_status: implemented +- [x] latest_decision_assistant_digest_backlog_status: implemented +- [x] latest_decision_assistant_template_audit_lane_status: implemented +- [x] latest_decision_assistant_replay_evidence_backlog_status: implemented - [x] latest_decision_assistant_workbench_status: visible_local_smoke - [x] latest_remote_decision_assistant_deploy_status: deployed_current_head - [x] latest_remote_decision_assistant_smoke_status: passed -- [x] latest_remote_decision_assistant_review_packet_status: resume_history_proof_sla_template_filter_review_backlog_visible +- [x] latest_remote_decision_assistant_review_packet_status: cleanup_backlog_proof_debt_template_lanes_promotion_evidence_visible - [x] latest_p7_selector_choice: continue_periodic_analysis_decision_monitoring_with_maturity_rollup - [x] latest_p14_slice: webhook_transport_retry - [x] latest_p15_pxx_tasklist_status: ready @@ -388,6 +404,11 @@ ## Newly Done +- [x] Added handoff cleanup-action readiness / resume backlog grouping fields and cockpit visibility +- [x] Added digest proof-SLA backlog routing / freshness-debt summary fields and cockpit visibility +- [x] Added template recovery action shortcuts / audit lane grouping fields and cockpit visibility +- [x] Added setup-family promotion evidence queue / backlog trend handoff fields and cockpit visibility +- [x] Passed focused validation, full unit discovery, script static checks, JS syntax check, deploy smoke, remote ops smoke, and remote field smoke for H18/F19/G18/D20 - [x] Added handoff lifecycle / operator resume queue fields and cockpit visibility - [x] Added digest proof follow-up queue, resolved proof state counts, and group follow-up actions - [x] Added template guardrail resolution and preferred-template decay fields diff --git a/src/services/operator/operator_workspace_service.py b/src/services/operator/operator_workspace_service.py index 71df7ab..c7c667f 100644 --- a/src/services/operator/operator_workspace_service.py +++ b/src/services/operator/operator_workspace_service.py @@ -1095,6 +1095,9 @@ def _build_decision_assistant_review_entrypoints( item["guardrail_audit_trail"] = outcome.get("guardrail_audit_trail") or [] item["guardrail_audit_trail_count"] = outcome.get("guardrail_audit_trail_count") or 0 item["preference_recovery_recommendation"] = outcome.get("preference_recovery_recommendation") + 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["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 [] @@ -1429,6 +1432,18 @@ def _score_template_outcome(*, template_id: str, usage: dict, notes: list[dict]) guardrail_audit_trail=guardrail_audit_trail, guardrail_audit_filter=guardrail_audit_filter, ) + preference_recovery_recommendation = _template_preference_recovery_recommendation( + template_id=template_id, + preference_recovery_status=preference_recovery_status, + negative_count=negative_count, + positive_count=positive_count, + actionable_count=action_count, + ) + audit_lane_group = _template_audit_lane_group( + guardrail_audit_filter=guardrail_audit_filter, + preference_recovery_status=preference_recovery_status, + outcome_status=outcome_status, + ) return { "template_id": template_id, "reuse_count": reuse_count, @@ -1470,13 +1485,14 @@ 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": _template_preference_recovery_recommendation( + "preference_recovery_recommendation": preference_recovery_recommendation, + "recovery_action_shortcuts": _template_recovery_action_shortcuts( template_id=template_id, - preference_recovery_status=preference_recovery_status, - negative_count=negative_count, - positive_count=positive_count, - actionable_count=action_count, + recommendation=preference_recovery_recommendation, + audit_lane_group=audit_lane_group, ), + "audit_lane_group": audit_lane_group, + "audit_lane_rank": _template_audit_lane_rank(audit_lane_group), "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], @@ -1650,6 +1666,77 @@ def _template_guardrail_audit_filtered_trail( return guardrail_audit_trail[:2] +def _template_audit_lane_group( + *, + guardrail_audit_filter: str, + preference_recovery_status: str, + outcome_status: str, +) -> str: + if guardrail_audit_filter == "negative_guardrail_only" or preference_recovery_status == "recovery_required": + return "guardrail" + if preference_recovery_status == "recovery_in_progress": + return "recovery" + if outcome_status in {"needs_outcome_feedback", "unscored"} or preference_recovery_status == "awaiting_feedback": + return "capture" + return "stable" + + +def _template_audit_lane_rank(audit_lane_group: str) -> int: + return { + "guardrail": 1, + "recovery": 2, + "capture": 3, + "stable": 4, + }.get(str(audit_lane_group or "stable"), 4) + + +def _template_recovery_action_shortcuts(*, template_id: str, recommendation: dict, audit_lane_group: str) -> list[dict]: + action = str(recommendation.get("action") or "capture_first_outcome") + prompt = str(recommendation.get("prompt") or "") + priority = str(recommendation.get("priority") or "low") + template_label = template_id.replace("_", " ") or "template" + shortcuts = [ + { + "action_id": f"{template_id}:{action}", + "label": _template_recovery_shortcut_label(action), + "priority": priority, + "audit_lane_group": audit_lane_group, + "prompt": prompt or f"{template_label}: capture recovery evidence.", + } + ] + if audit_lane_group in {"guardrail", "recovery"}: + shortcuts.append( + { + "action_id": f"{template_id}:open_audit_lane", + "label": "Open Audit Lane", + "priority": "medium" if audit_lane_group == "recovery" else "high", + "audit_lane_group": audit_lane_group, + "prompt": f"Review {template_label} audit evidence before restoring preference.", + } + ) + if audit_lane_group == "capture": + shortcuts.append( + { + "action_id": f"{template_id}:capture_outcome", + "label": "Capture Outcome", + "priority": "low", + "audit_lane_group": audit_lane_group, + "prompt": f"Save one outcome note for {template_label} before ranking it again.", + } + ) + return shortcuts[:3] + + +def _template_recovery_shortcut_label(action: str) -> str: + if action == "collect_recovery_note": + return "Collect Recovery" + if action == "review_recovery_evidence": + return "Review Recovery" + if action == "capture_first_outcome": + return "Capture First Outcome" + return "Keep Stable" + + 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": @@ -1757,10 +1844,23 @@ def _build_template_feedback_aggregation( for item in template_outcomes.values() if dict(item.get("preference_recovery_recommendation") or {}).get("action") != "keep_preferred_candidate" ] + recovery_action_shortcuts = [ + dict(shortcut) + for item in template_outcomes.values() + for shortcut in list(item.get("recovery_action_shortcuts") or []) + ] + recovery_action_shortcuts.sort( + key=lambda row: ( + _template_audit_lane_rank(str(row.get("audit_lane_group") or "stable")), + {"high": 1, "medium": 2, "low": 3}.get(str(row.get("priority") or "low"), 3), + str(row.get("label") or ""), + ) + ) guardrail_audit_filter_counts = {} for item in template_outcomes.values(): audit_filter = str(item.get("guardrail_audit_filter") or "unknown") guardrail_audit_filter_counts[audit_filter] = int(guardrail_audit_filter_counts.get(audit_filter) or 0) + 1 + template_audit_lane_groups = _build_template_audit_lane_groups(template_outcomes) rated_template_count = sum( 1 for item in template_outcomes.values() @@ -1835,9 +1935,47 @@ def _build_template_feedback_aggregation( "guardrail_audit_filter_counts": guardrail_audit_filter_counts, "guardrail_audit_filtered_trail": guardrail_audit_filtered_trail[:5], "guardrail_audit_filtered_count": len(guardrail_audit_filtered_trail), + "template_audit_lane_groups": template_audit_lane_groups, + "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, } +def _build_template_audit_lane_groups(template_outcomes: dict[str, dict]) -> list[dict]: + lanes: dict[str, list[dict]] = {} + for item in template_outcomes.values(): + lane = str(item.get("audit_lane_group") or "stable") + lanes.setdefault(lane, []).append(dict(item)) + groups = [] + for lane in ("guardrail", "recovery", "capture", "stable"): + items = lanes.get(lane) or [] + if not items: + continue + ranked = sorted(items, key=lambda row: (int(row.get("audit_lane_rank") or 9), str(row.get("template_id") or ""))) + groups.append( + { + "audit_lane_group": lane, + "label": _template_audit_lane_label(lane), + "count": len(items), + "template_ids": [str(item.get("template_id") or "") for item in ranked[:5]], + "next_template_id": str((ranked[0] or {}).get("template_id") or ""), + "next_prompt": (ranked[0] or {}).get("preference_recovery_prompt"), + } + ) + return groups + + +def _template_audit_lane_label(lane: str) -> str: + if lane == "guardrail": + return "Guardrail" + if lane == "recovery": + return "Recovery" + if lane == "capture": + return "Capture" + return "Stable" + + def _pick_best_template_outcome(template_outcomes: dict[str, dict]) -> str | None: if not template_outcomes: return None @@ -2115,10 +2253,38 @@ def _build_decision_assistant_digest_history_summary(history: list[dict]) -> dic "prompt": group.get("proof_resolution_sla_prompt"), "age_seconds": group.get("proof_follow_up_age_seconds"), "suppression": group.get("repeated_stale_proof_suppression"), + "backlog_route": group.get("proof_sla_backlog_route"), + "backlog_priority": group.get("proof_sla_backlog_priority"), + "freshness_debt_score": group.get("freshness_debt_score"), } for group in groups if group.get("proof_resolution_sla_status") in {"breached", "due_soon", "missing_timestamp_sla"} ] + proof_sla_backlog_routes = {} + for group in groups: + route = str(group.get("proof_sla_backlog_route") or "unknown") + proof_sla_backlog_routes[route] = int(proof_sla_backlog_routes.get(route) or 0) + 1 + proof_sla_backlog_queue = [ + { + "kind": str(group.get("kind") or "history"), + "route": group.get("proof_sla_backlog_route"), + "priority": group.get("proof_sla_backlog_priority"), + "freshness_debt_score": group.get("freshness_debt_score"), + "sla_status": group.get("proof_resolution_sla_status"), + "prompt": group.get("proof_resolution_sla_prompt"), + "evidence_anchor": group.get("refresh_evidence_anchor"), + } + for group in groups + if group.get("proof_sla_backlog_route") not in {"resolved"} + ] + proof_sla_backlog_queue.sort( + key=lambda row: ( + {"high": 1, "medium": 2, "low": 3, "none": 4}.get(str(row.get("priority") or "none"), 4), + -int(row.get("freshness_debt_score") or 0), + str(row.get("kind") or ""), + ) + ) + freshness_debt_summary = _build_digest_history_freshness_debt_summary(groups) repeated_stale_suppression_count = sum( 1 for group in groups @@ -2195,6 +2361,11 @@ def _build_decision_assistant_digest_history_summary(history: list[dict]) -> dic "repeated_stale_proof_suppression_count": repeated_stale_suppression_count, "next_proof_resolution_sla_kind": proof_resolution_sla_queue[0].get("kind") if proof_resolution_sla_queue else None, "next_proof_resolution_sla_prompt": proof_resolution_sla_queue[0].get("prompt") if proof_resolution_sla_queue else None, + "proof_sla_backlog_routes": proof_sla_backlog_routes, + "proof_sla_backlog_queue": proof_sla_backlog_queue[:5], + "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"), } @@ -2255,6 +2426,21 @@ def _build_decision_assistant_digest_history_groups(history: list[dict], *, reco proof_follow_up_state=proof_follow_up_state, proof_resolution_sla_seconds=proof_resolution_sla_seconds, ) + proof_sla_backlog_route = _digest_history_proof_sla_backlog_route( + proof_resolution_sla_status=proof_resolution_sla_status, + proof_follow_up_state=proof_follow_up_state, + freshness_proof_status=freshness_proof_status, + ) + freshness_debt_score = _digest_history_freshness_debt_score( + age=age, + proof_resolution_sla_status=proof_resolution_sla_status, + proof_follow_up_state=proof_follow_up_state, + ) + proof_sla_backlog_priority = _digest_history_proof_sla_backlog_priority( + route=proof_sla_backlog_route, + proof_resolution_sla_status=proof_resolution_sla_status, + 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, @@ -2317,6 +2503,9 @@ def _build_decision_assistant_digest_history_groups(history: list[dict], *, reco proof_resolution_sla_status=proof_resolution_sla_status, proof_resolution_sla_seconds=proof_resolution_sla_seconds, ), + "proof_sla_backlog_route": proof_sla_backlog_route, + "proof_sla_backlog_priority": proof_sla_backlog_priority, + "freshness_debt_score": freshness_debt_score, "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, @@ -2648,6 +2837,124 @@ def _digest_history_proof_resolution_sla_prompt( return f"{kind} proof is resolved; no proof SLA action is pending." +def _digest_history_proof_sla_backlog_route( + *, + proof_resolution_sla_status: str, + proof_follow_up_state: str, + freshness_proof_status: str, +) -> str: + if proof_resolution_sla_status == "breached": + return "resolve_now" + if proof_resolution_sla_status == "missing_timestamp_sla" or freshness_proof_status == "missing_timestamp_proof": + return "timestamp_proof" + if proof_resolution_sla_status in {"due_soon", "within_sla"} or proof_follow_up_state == "watch": + return "watch_queue" + return "resolved" + + +def _digest_history_proof_sla_backlog_priority( + *, + route: str, + proof_resolution_sla_status: str, + freshness_debt_score: int, +) -> str: + if route == "resolve_now" or proof_resolution_sla_status == "breached": + return "high" + if route == "timestamp_proof": + return "high" if int(freshness_debt_score or 0) >= 60 else "medium" + if route == "watch_queue": + return "medium" if int(freshness_debt_score or 0) >= 45 else "low" + return "none" + + +def _digest_history_freshness_debt_score( + *, + age: dict, + proof_resolution_sla_status: str, + proof_follow_up_state: str, +) -> int: + age_band = str(age.get("age_band") or "unknown_age") + base = { + "fresh_now": 0, + "monitor_window": 10, + "same_session": 35, + "stale": 75, + "untimed": 60, + "unknown_age": 60, + }.get(age_band, 40) + if proof_resolution_sla_status == "breached": + base += 25 + elif proof_resolution_sla_status == "due_soon": + base += 15 + elif proof_resolution_sla_status == "missing_timestamp_sla": + base += 20 + elif proof_resolution_sla_status == "within_sla": + base += 10 + if proof_follow_up_state == "follow_up_required": + base += 10 + return max(0, min(100, int(base))) + + +def _build_digest_history_freshness_debt_summary(groups: list[dict]) -> dict: + if not groups: + return { + "status": "idle", + "debt_score_total": 0, + "high_debt_count": 0, + "route_counts": {}, + "next_kind": None, + "next_prompt": None, + } + debt_score_total = sum(int(group.get("freshness_debt_score") or 0) for group in groups) + high_debt_groups = [ + group + for group in groups + if int(group.get("freshness_debt_score") or 0) >= 70 + ] + route_counts = {} + for group in groups: + route = str(group.get("proof_sla_backlog_route") or "unknown") + route_counts[route] = int(route_counts.get(route) or 0) + 1 + next_group = dict( + sorted( + groups, + key=lambda row: ( + int(row.get("freshness_debt_score") or 0), + str(row.get("kind") or ""), + ), + reverse=True, + )[0] + ) + if high_debt_groups: + status = "high_debt" + elif any(group.get("proof_sla_backlog_route") in {"timestamp_proof", "watch_queue"} for group in groups): + status = "watch_debt" + else: + status = "clear" + return { + "status": status, + "debt_score_total": debt_score_total, + "average_debt_score": debt_score_total / len(groups) if groups else 0.0, + "high_debt_count": len(high_debt_groups), + "route_counts": route_counts, + "next_kind": next_group.get("kind"), + "next_debt_score": next_group.get("freshness_debt_score"), + "next_prompt": _digest_history_freshness_debt_prompt(next_group), + } + + +def _digest_history_freshness_debt_prompt(group: dict) -> str: + kind = str(group.get("kind") or "history") + route = str(group.get("proof_sla_backlog_route") or "resolved") + if route == "resolve_now": + return f"Resolve {kind} freshness debt now before reusing this digest group." + if route == "timestamp_proof": + return f"Attach timestamp proof for {kind} so the digest backlog can leave the debt queue." + if route == "watch_queue": + return f"Keep {kind} in the watch queue and refresh it before it drives the next note." + return f"{kind} freshness debt is clear." + + def _digest_history_repeated_stale_proof_suppression( *, item_count: int, @@ -3006,6 +3313,13 @@ def _build_decision_assistant_scan_handoff_summary(cards: list[dict]) -> dict: title=str(item.get("title") or item.get("target_section") or "handoff"), handoff_status=str(item.get("handoff_status") or ""), ), + "cleanup_action_readiness": _scan_handoff_cleanup_action_readiness(str(item.get("handoff_status") or "")), + "cleanup_action_readiness_reason": _scan_handoff_cleanup_action_readiness_reason( + title=str(item.get("title") or item.get("target_section") or "handoff"), + handoff_status=str(item.get("handoff_status") or ""), + ), + "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 "")), } for item in cards if item.get("handoff_marker") @@ -3025,6 +3339,20 @@ def _build_decision_assistant_scan_handoff_summary(cards: list[dict]) -> dict: for row in list(item.get("resume_history") or []) ] stale_persistence_cleanup_queue = _build_scan_handoff_stale_persistence_cleanup_queue(markers) + resume_backlog_groups = _build_scan_handoff_resume_backlog_groups(markers) + cleanup_action_queue = [ + item + for item in sorted( + markers, + key=lambda row: ( + int(row.get("resume_backlog_priority") or 99), + int(row.get("cleanup_priority") or 0) * -1, + str(row.get("title") or ""), + ), + ) + 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 {} next_unresolved_marker = next( ( item @@ -3084,6 +3412,10 @@ def _build_decision_assistant_scan_handoff_summary(cards: list[dict]) -> dict: "next_stale_persistence_cleanup_prompt": ( stale_persistence_cleanup_queue[0].get("stale_persistence_cleanup_prompt") if stale_persistence_cleanup_queue else None ), + "resume_backlog_groups": resume_backlog_groups, + "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"), "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": [ @@ -3273,6 +3605,103 @@ def _scan_handoff_stale_persistence_cleanup_prompt(*, title: str, handoff_status return f"{title} does not need stale persistence cleanup." +def _scan_handoff_cleanup_action_readiness(handoff_status: str) -> str: + if handoff_status == "ready_handoff": + return "ready_now" + if handoff_status == "refresh_handoff": + return "ready_after_refresh" + if handoff_status == "blocked_handoff": + return "ready_after_blocker" + if handoff_status == "next_handoff": + return "monitor" + return "not_needed" + + +def _scan_handoff_cleanup_action_readiness_reason(*, title: str, handoff_status: str) -> str: + readiness = _scan_handoff_cleanup_action_readiness(handoff_status) + if readiness == "ready_now": + return f"{title} can be reopened and cleared from the persisted resume backlog." + if readiness == "ready_after_refresh": + return f"Refresh {title}, then clear the stale persisted handoff." + if readiness == "ready_after_blocker": + return f"Keep {title} in the cleanup backlog until its blocker is resolved." + if readiness == "monitor": + return f"{title} is the next handoff; monitor the stale window before cleanup." + return f"{title} does not need a cleanup action." + + +def _scan_handoff_resume_backlog_group(handoff_status: str) -> str: + if handoff_status == "next_handoff": + return "active_resume" + if handoff_status == "blocked_handoff": + return "blocked_cleanup" + if handoff_status == "refresh_handoff": + return "refresh_cleanup" + if handoff_status == "ready_handoff": + return "ready_cleanup" + return "carry_forward" + + +def _scan_handoff_resume_backlog_priority(handoff_status: str) -> int: + return { + "blocked_handoff": 1, + "next_handoff": 2, + "refresh_handoff": 3, + "ready_handoff": 4, + "context_ready": 5, + }.get(str(handoff_status or "context_ready"), 5) + + +def _build_scan_handoff_resume_backlog_groups(markers: list[dict]) -> list[dict]: + groups_by_name: dict[str, list[dict]] = {} + for marker in markers: + group = str(marker.get("resume_backlog_group") or "carry_forward") + groups_by_name.setdefault(group, []).append(marker) + ranked_groups = ["blocked_cleanup", "active_resume", "refresh_cleanup", "ready_cleanup", "carry_forward"] + groups = [] + for group in ranked_groups: + items = groups_by_name.get(group) or [] + if not items: + continue + ranked_items = sorted( + items, + key=lambda row: ( + int(row.get("resume_backlog_priority") or 99), + str(row.get("title") or ""), + ), + ) + cleanup_ready_items = [ + item + for item in ranked_items + if item.get("cleanup_action_readiness") in {"ready_now", "ready_after_refresh", "ready_after_blocker", "monitor"} + ] + groups.append( + { + "resume_backlog_group": group, + "label": _scan_handoff_resume_backlog_group_label(group), + "count": len(ranked_items), + "cleanup_ready_count": len(cleanup_ready_items), + "titles": [str(item.get("title") or "handoff") for item in ranked_items[:4]], + "next_marker": (ranked_items[0] or {}).get("handoff_marker"), + "next_cleanup_action": (cleanup_ready_items[0] or {}).get("cleanup_action_label") if cleanup_ready_items else None, + "next_prompt": (ranked_items[0] or {}).get("cleanup_action_readiness_reason"), + } + ) + return groups + + +def _scan_handoff_resume_backlog_group_label(group: str) -> str: + if group == "blocked_cleanup": + return "Blocked Cleanup" + if group == "active_resume": + return "Active Resume" + if group == "refresh_cleanup": + return "Refresh Cleanup" + if group == "ready_cleanup": + return "Ready Cleanup" + return "Carry Forward" + + def _build_scan_handoff_stale_persistence_cleanup_queue(markers: list[dict]) -> list[dict]: cleanup_status_rank = { "cleanup_required": 1, @@ -3287,6 +3716,10 @@ def _build_scan_handoff_stale_persistence_cleanup_queue(markers: list[dict]) -> "stale_persistence_cleanup_status": item.get("stale_persistence_cleanup_status"), "stale_persistence_cleanup_prompt": item.get("stale_persistence_cleanup_prompt"), "resume_queue_rank": item.get("resume_queue_rank"), + "cleanup_action_readiness": item.get("cleanup_action_readiness"), + "cleanup_action_readiness_reason": item.get("cleanup_action_readiness_reason"), + "resume_backlog_group": item.get("resume_backlog_group"), + "resume_backlog_priority": item.get("resume_backlog_priority"), } for item in markers if item.get("stale_persistence_cleanup_status") in cleanup_status_rank @@ -3321,6 +3754,10 @@ def _build_scan_handoff_resume_queue(markers: list[dict]) -> list[dict]: "resume_history_count": item.get("resume_history_count") or 0, "stale_persistence_cleanup_status": item.get("stale_persistence_cleanup_status"), "stale_persistence_cleanup_prompt": item.get("stale_persistence_cleanup_prompt"), + "cleanup_action_readiness": item.get("cleanup_action_readiness"), + "cleanup_action_readiness_reason": item.get("cleanup_action_readiness_reason"), + "resume_backlog_group": item.get("resume_backlog_group"), + "resume_backlog_priority": item.get("resume_backlog_priority"), } for item in markers if item.get("lifecycle_state") != "carry_forward" @@ -3691,6 +4128,19 @@ def _build_setup_replay_broader_baseline_summary( "reason": "insufficient_samples", "action": "Collect replay samples before promotion backlog review.", }, + "promotion_evidence_queue": [ + { + "evidence_type": "sample_gap", + "status": "collecting", + "value": 5, + "summary": f"{setup_family} needs replay samples before promotion evidence can be reviewed.", + } + ], + "promotion_evidence_queue_count": 1, + "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." + ), "demotion_history": [], "demotion_history_count": 0, "focus_recommendation": f"Collect samples before changing focus for {setup_family}.", @@ -3924,6 +4374,19 @@ 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_backlog_trend_status": "stable_collecting", + "promotion_backlog_trend_handoff": ( + f"Keep {setup_family} in analysis-only collection until replay evidence reaches the sample threshold." + ), "demotion_history": [], "demotion_history_count": 0, "focus_recommendation": f"Collect samples before changing focus for {setup_family}.", @@ -4059,6 +4522,24 @@ def _build_setup_family_trend_delta( promotion_backlog_rank = _setup_family_promotion_backlog_rank( promotion_backlog_priority=promotion_backlog_priority, ) + promotion_evidence_queue = _setup_family_promotion_evidence_queue( + setup_family=setup_family, + trend_status=trend_status, + sample_count=sample_count, + sample_gap=sample_gap, + framework_delta=framework_delta, + avg_r_delta=avg_r_delta, + risk_delta=risk_delta, + promotion_blockers=promotion_blockers, + review_lane_sla_status=review_lane_sla_status, + promotion_backlog_priority=promotion_backlog_priority, + ) + promotion_backlog_trend_status = _setup_family_promotion_backlog_trend_status( + promotion_ready=promotion_ready, + sample_gap=sample_gap, + review_lane_sla_status=review_lane_sla_status, + promotion_backlog_priority=promotion_backlog_priority, + ) return { "status": "active", "trend_status": trend_status, @@ -4109,6 +4590,14 @@ def _build_setup_family_trend_delta( promotion_backlog_reason=promotion_backlog_reason, promotion_next_action=promotion_next_action, ), + "promotion_evidence_queue": promotion_evidence_queue[:6], + "promotion_evidence_queue_count": len(promotion_evidence_queue), + "promotion_backlog_trend_status": promotion_backlog_trend_status, + "promotion_backlog_trend_handoff": _setup_family_promotion_backlog_trend_handoff( + setup_family=setup_family, + promotion_backlog_trend_status=promotion_backlog_trend_status, + promotion_backlog_reason=promotion_backlog_reason, + ), "demotion_history": demotion_history[:5], "demotion_history_count": len(demotion_history), "focus_recommendation": focus_recommendation, @@ -4402,6 +4891,89 @@ def _setup_family_promotion_backlog_item( } +def _setup_family_promotion_evidence_queue( + *, + setup_family: str, + trend_status: str, + sample_count: int, + sample_gap: int, + framework_delta: float, + avg_r_delta: float, + risk_delta: float, + promotion_blockers: list[str], + review_lane_sla_status: str, + promotion_backlog_priority: str, +) -> list[dict]: + queue = [ + { + "evidence_type": "sample_window", + "status": "ready" if sample_gap <= 0 else "collecting", + "value": sample_count, + "summary": f"{setup_family} has {sample_count} replay sample(s); sample gap is {sample_gap}.", + }, + { + "evidence_type": "family_delta", + "status": trend_status, + "value": round(float(framework_delta or 0.0), 4), + "summary": ( + f"Framework delta {framework_delta:.2f}, avg-R delta {avg_r_delta:.2f}, " + f"risk delta {risk_delta:.2f}." + ), + }, + { + "evidence_type": "review_lane_sla", + "status": review_lane_sla_status, + "value": promotion_backlog_priority, + "summary": f"Review lane SLA is {review_lane_sla_status}; backlog priority is {promotion_backlog_priority}.", + }, + ] + for blocker in promotion_blockers[:3]: + queue.append( + { + "evidence_type": "promotion_blocker", + "status": "blocking", + "value": blocker, + "summary": f"{setup_family} promotion evidence is blocked by {blocker}.", + } + ) + return queue + + +def _setup_family_promotion_backlog_trend_status( + *, + promotion_ready: bool, + sample_gap: int, + review_lane_sla_status: str, + promotion_backlog_priority: str, +) -> str: + if promotion_ready: + return "ready_for_review" + if review_lane_sla_status == "breached" or promotion_backlog_priority == "urgent": + return "backlog_breached" + if review_lane_sla_status in {"due_now", "due_soon"} or promotion_backlog_priority in {"high", "medium"}: + return "backlog_due" + if sample_gap > 0: + return "stable_collecting" + return "evidence_watch" + + +def _setup_family_promotion_backlog_trend_handoff( + *, + setup_family: str, + promotion_backlog_trend_status: str, + promotion_backlog_reason: str, +) -> str: + if promotion_backlog_trend_status == "ready_for_review": + return f"Hand off {setup_family} for analysis-lane promotion review; keep execution permissions unchanged." + if promotion_backlog_trend_status == "backlog_breached": + return f"Review {setup_family} backlog immediately because {promotion_backlog_reason} is breached." + if promotion_backlog_trend_status == "backlog_due": + return f"Queue {setup_family} for the next operator review because {promotion_backlog_reason} is due." + if promotion_backlog_trend_status == "stable_collecting": + return f"Keep collecting {setup_family} replay evidence before promotion handoff." + return f"Keep {setup_family} in evidence watch until promotion backlog signals change." + + 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 40c44a4..423132d 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -587,6 +587,10 @@ function renderDecisionAssistant(summary) { ['stale persistence cleanup', String(scanHandoffSummary.stale_persistence_cleanup_count ?? 0)], ['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}`))], + ['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'], ['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'], @@ -729,6 +733,11 @@ function renderDecisionAssistant(summary) { ['stale proof suppressions', String(digestSurface.history_summary?.repeated_stale_proof_suppression_count ?? 0)], ['next proof SLA kind', digestSurface.history_summary?.next_proof_resolution_sla_kind || 'n/a'], ['next proof SLA prompt', digestSurface.history_summary?.next_proof_resolution_sla_prompt || 'n/a'], + ['proof SLA backlog', String(digestSurface.history_summary?.proof_sla_backlog_count ?? 0)], + ['proof SLA routes', formatJsonInline(digestSurface.history_summary?.proof_sla_backlog_routes || {})], + ['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'], ['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}`))], @@ -826,6 +835,9 @@ function renderDecisionAssistant(summary) { ['proof SLA status', item.proof_resolution_sla_status || 'n/a'], ['proof SLA', formatDurationSeconds(item.proof_resolution_sla_seconds)], ['proof SLA prompt', item.proof_resolution_sla_prompt || 'n/a'], + ['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)], ['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}`))], @@ -894,6 +906,9 @@ function renderDecisionAssistant(summary) { ['next recovery prompt', reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.next_preference_recovery_prompt || 'n/a'], ['audit filters', formatJsonInline(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.guardrail_audit_filter_counts || {})], ['filtered audit trail', String(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.guardrail_audit_filtered_count ?? 0)], + ['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'], ['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)], @@ -959,6 +974,9 @@ function renderDecisionAssistant(summary) { ['preference recovery', item.preference_recovery_status || 'n/a'], ['recovery prompt', item.preference_recovery_prompt || 'n/a'], ['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')], + ['recovery shortcuts', formatList((item.recovery_action_shortcuts || []).map((row) => row.label || row.action_id))], ['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}`))], @@ -1275,6 +1293,10 @@ function renderDecisionAssistant(summary) { ['promotion backlog priority', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_backlog_priority || 'n/a'], ['promotion backlog rank', String(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_backlog_rank ?? 'n/a')], ['promotion backlog reason', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_backlog_reason || 'n/a'], + ['promotion evidence queue', String(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_evidence_queue_count ?? 0)], + ['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'], ['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 83a4ce1..b1f0256 100644 --- a/tests/unit/test_operator_workspace_service.py +++ b/tests/unit/test_operator_workspace_service.py @@ -214,6 +214,10 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("guardrail_audit_filter_counts", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"]) self.assertIn("guardrail_audit_filtered_trail", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"]) self.assertIn("guardrail_audit_filtered_count", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"]) + self.assertIn("template_audit_lane_groups", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"]) + 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.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") @@ -232,6 +236,9 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("guardrail_audit_trail", summary["review_entrypoints"]["note_templates"][0]) self.assertIn("guardrail_audit_trail_count", summary["review_entrypoints"]["note_templates"][0]) self.assertIn("preference_recovery_recommendation", summary["review_entrypoints"]["note_templates"][0]) + 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("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]) @@ -272,6 +279,11 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("proof_resolution_sla_queue_count", summary["digest_surface"]["history_summary"]) self.assertIn("repeated_stale_proof_suppression_count", summary["digest_surface"]["history_summary"]) self.assertIn("next_proof_resolution_sla_prompt", summary["digest_surface"]["history_summary"]) + self.assertIn("proof_sla_backlog_routes", summary["digest_surface"]["history_summary"]) + self.assertIn("proof_sla_backlog_queue", summary["digest_surface"]["history_summary"]) + 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("refresh_prompts", summary["digest_surface"]["history_summary"]) self.assertIn("digest_history_groups", summary) self.assertIn("group_status", summary["digest_history_groups"][0]) @@ -300,6 +312,9 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("proof_resolution_sla_seconds", summary["digest_history_groups"][0]) self.assertIn("proof_resolution_sla_status", summary["digest_history_groups"][0]) self.assertIn("proof_resolution_sla_prompt", summary["digest_history_groups"][0]) + 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("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]) @@ -355,6 +370,10 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("promotion_backlog_rank", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) self.assertIn("promotion_backlog_reason", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) self.assertIn("promotion_backlog_item", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) + self.assertIn("promotion_evidence_queue", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"]) + 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("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"]) @@ -407,6 +426,10 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("stale_persistence_cleanup_count", summary["scan_handoff_summary"]) self.assertIn("next_stale_persistence_cleanup_key", summary["scan_handoff_summary"]) self.assertIn("next_stale_persistence_cleanup_prompt", summary["scan_handoff_summary"]) + self.assertIn("resume_backlog_groups", summary["scan_handoff_summary"]) + 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("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]) @@ -424,6 +447,10 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertIn("resume_history_count", summary["scan_handoff_summary"]["handoff_markers"][0]) self.assertIn("stale_persistence_cleanup_status", summary["scan_handoff_summary"]["handoff_markers"][0]) self.assertIn("stale_persistence_cleanup_prompt", summary["scan_handoff_summary"]["handoff_markers"][0]) + self.assertIn("cleanup_action_readiness", summary["scan_handoff_summary"]["handoff_markers"][0]) + 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.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")