Close decision assistant H19 F20 G19 D21

This commit is contained in:
Codex
2026-05-13 12:07:01 +08:00
parent 3c5ccbdf3e
commit b19d3bcb78
5 changed files with 598 additions and 22 deletions
+18 -3
View File
@@ -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
+8 -4
View File
@@ -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
@@ -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
+23
View File
@@ -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')],
@@ -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")