From 19d9d14cc6686fc9b26a80c2ebd904b48dab30e4 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 13 May 2026 22:12:06 +0800 Subject: [PATCH] Add watchlist and review continuity batch --- SESSION.md | 13 +- TASKS.md | 13 +- .../operator/operator_workspace_service.py | 171 +++++++++++++++++- src/web/app.js | 35 ++++ tests/unit/test_operator_workspace_service.py | 6 + 5 files changed, 226 insertions(+), 12 deletions(-) diff --git a/SESSION.md b/SESSION.md index ddbda0e..1dc829e 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, read-only market context, and a continuous operator cockpit. This run closed W4/D25/F25/H25 as one internal batch: workflow phase artifacts now summarize pre/intraday/post handoff state, digest carries a prioritized action queue, replay learning exposes a session/market-state focus matrix, and workspace continuity now shows ready/watch/loop handoff context. 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, read-only market context, and a continuous operator cockpit. This run closed C3/G20/H26 as one internal batch: watchlist now exposes a focus matrix and next-focus prompt, review entry now exposes a prioritized review-action queue and handoff summary, and the main `assistant_loop_queue` now carries watchlist focus alongside review-entry actions. Local validation passed, the current working tree was deployed, and remote smoke confirmed the latest Decision Assistant fields on the live host. ## Current Branch @@ -126,6 +126,9 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. - latest_decision_assistant_digest_action_slice: digest_action_queue - latest_decision_assistant_learning_matrix_slice: learning_focus_matrix - latest_decision_assistant_workspace_handoff_slice: workspace_continuity_handoff +- latest_decision_assistant_watchlist_focus_slice: watchlist_focus_matrix +- latest_decision_assistant_review_action_slice: review_entry_action_queue +- latest_decision_assistant_review_handoff_slice: review_entry_handoff_summary - latest_decision_assistant_summary_status: implemented - latest_decision_assistant_change_summary_status: implemented - latest_decision_assistant_evidence_status: implemented @@ -236,10 +239,13 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. - latest_decision_assistant_digest_action_status: implemented - latest_decision_assistant_learning_matrix_status: implemented - latest_decision_assistant_workspace_handoff_status: implemented +- latest_decision_assistant_watchlist_focus_status: implemented +- latest_decision_assistant_review_action_status: implemented +- latest_decision_assistant_review_handoff_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: workflow_digest_learning_workspace_batch_visible +- latest_remote_decision_assistant_review_packet_status: watchlist_review_loop_batch_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 @@ -654,11 +660,12 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. - Latest D24/H24 validation passed: focused operator workspace tests, 68 related unit/API/static tests, 713 full 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 `learning_focus_queue`, `next_learning_focus_prompt`, and replay-learning presence inside `assistant_loop_queue`. - Latest E7 validation passed: focused operator workspace tests, 68 related unit/API/static tests, 713 full 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 `chart_adapter_mode=optional_reference`, `chart_primary_mode=internal_review_packet_html`, and a non-blocking external chart adapter even when other supporting context sources are stale. - Latest W4/D25/F25/H25 validation passed: focused operator workspace tests, 68 related unit/API/static tests, 713 full 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 `workflow_artifact_summary`, `digest_action_queue`, `learning_focus_matrix`, and `workspace_continuity_handoff` plus matching UI strings. +- Latest C3/G20/H26 validation passed: focused operator workspace tests, 68 related unit/API/static tests, 713 full 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 `watchlist focus matrix`, `review_entry_action_queue`, `review_entry_handoff_summary`, and watchlist-focus presence inside `assistant_loop_queue`. - Real-live order submission remains intentionally disabled because the OKX order adapter is still not a real-live submitter. ## Exact Next Step -W4/D25/F25/H25 batch is closed. External chart URLs remain optional reference enhancements and are not required for the current analysis-only loop. Internal non-trading backlog still exists, but PXX remains closed; do not create P25/P26 automatically, and do not enable real trading. +C3/G20/H26 batch is closed. External chart URLs remain optional reference enhancements and are not required for the current analysis-only loop. Internal non-trading backlog still exists, but 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 08e5b5e..c71c3c2 100644 --- a/TASKS.md +++ b/TASKS.md @@ -157,6 +157,9 @@ - [x] D25 learning focus matrix slice - [x] F25 digest action queue slice - [x] H25 workspace continuity handoff slice +- [x] C3 watchlist focus matrix / next-focus prompt slice +- [x] G20 review entry action queue slice +- [x] H26 review/watchlist continuity into loop queue slice - [x] P8 local closure complete - [x] P9 local closure complete - [x] P10 local closure complete @@ -276,6 +279,9 @@ - [x] latest_decision_assistant_digest_action_slice: digest_action_queue - [x] latest_decision_assistant_learning_matrix_slice: learning_focus_matrix - [x] latest_decision_assistant_workspace_handoff_slice: workspace_continuity_handoff +- [x] latest_decision_assistant_watchlist_focus_slice: watchlist_focus_matrix +- [x] latest_decision_assistant_review_action_slice: review_entry_action_queue +- [x] latest_decision_assistant_review_handoff_slice: review_entry_handoff_summary - [x] latest_decision_assistant_summary_status: implemented - [x] latest_decision_assistant_change_summary_status: implemented - [x] latest_decision_assistant_evidence_status: implemented @@ -383,10 +389,13 @@ - [x] latest_decision_assistant_digest_action_status: implemented - [x] latest_decision_assistant_learning_matrix_status: implemented - [x] latest_decision_assistant_workspace_handoff_status: implemented +- [x] latest_decision_assistant_watchlist_focus_status: implemented +- [x] latest_decision_assistant_review_action_status: implemented +- [x] latest_decision_assistant_review_handoff_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: workflow_digest_learning_workspace_batch_visible +- [x] latest_remote_decision_assistant_review_packet_status: watchlist_review_loop_batch_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 @@ -696,6 +705,8 @@ - [x] Deployed E7 current tree and passed remote smoke: 11 deploy endpoints, 9 remote ops checks, and review-packet field smoke for `chart_adapter_mode=optional_reference`, `chart_primary_mode=internal_review_packet_html`, and a non-blocking external chart adapter even when supporting context sources are stale - [x] Completed W4/D25/F25/H25 focused validation: operator workspace tests, 68 related unit/API/static tests, 713 full unit tests, 73 script static checks, and dashboard JS syntax - [x] Deployed W4/D25/F25/H25 current tree and passed remote smoke: 11 deploy endpoints, 9 remote ops checks, and review-packet/static field smoke for `workflow_artifact_summary`, `digest_action_queue`, `learning_focus_matrix`, and `workspace_continuity_handoff` +- [x] Completed C3/G20/H26 focused validation: operator workspace tests, 68 related unit/API/static tests, 713 full unit tests, 73 script static checks, and dashboard JS syntax +- [x] Deployed C3/G20/H26 current tree and passed remote smoke: 11 deploy endpoints, 9 remote ops checks, and review-packet/static field smoke for `watchlist focus matrix`, `review_entry_action_queue`, `review_entry_handoff_summary`, and watchlist-focus presence inside `assistant_loop_queue` ## Blocked diff --git a/src/services/operator/operator_workspace_service.py b/src/services/operator/operator_workspace_service.py index dcb244d..6676618 100644 --- a/src/services/operator/operator_workspace_service.py +++ b/src/services/operator/operator_workspace_service.py @@ -467,6 +467,7 @@ def build_decision_assistant_summary( digest_history_browser=digest_history_browser, review_entrypoints=review_entrypoints, setup_learning_rollup=setup_learning_rollup, + candidate_watchlist_summary=candidate_watchlist_summary, ) assistant_loop_summary = _build_decision_assistant_loop_summary(assistant_loop_queue) workspace_continuity_handoff = _build_decision_assistant_workspace_continuity_handoff( @@ -697,6 +698,7 @@ def _build_decision_assistant_loop_queue( digest_history_browser: dict, review_entrypoints: dict, setup_learning_rollup: dict, + candidate_watchlist_summary: dict, ) -> list[dict]: queue = [] if workflow_session_completion_summary.get("next_task_label"): @@ -725,6 +727,20 @@ def _build_decision_assistant_loop_queue( "priority_rank": 2, } ) + next_watchlist_focus = dict(candidate_watchlist_summary.get("next_watchlist_focus") or {}) + if next_watchlist_focus.get("title"): + queue.append( + { + "queue_id": "watchlist_focus", + "lane": "watchlist", + "title": str(next_watchlist_focus.get("title") or "Watchlist"), + "status": str(candidate_watchlist_summary.get("focus_matrix", {}).get("status") or "active"), + "target_section": "watchlist-review-now", + "action_label": "Open Watchlist", + "prompt": str(candidate_watchlist_summary.get("next_watchlist_focus_prompt") or "Continue watchlist review."), + "priority_rank": 3, + } + ) if continuous_workspace_summary.get("next_section"): queue.append( { @@ -735,7 +751,7 @@ def _build_decision_assistant_loop_queue( "target_section": str(continuous_workspace_summary.get("next_target_section") or "workspace-rail"), "action_label": str(continuous_workspace_summary.get("next_action") or "Open Workspace"), "prompt": str(continuous_workspace_summary.get("next_handoff_prompt") or "Continue the workspace rail."), - "priority_rank": 3, + "priority_rank": 4, } ) next_browser_action = dict(digest_history_browser.get("next_action") or {}) @@ -749,7 +765,7 @@ def _build_decision_assistant_loop_queue( "target_section": "digest", "action_label": "Open Digest", "prompt": str(next_browser_action.get("prompt") or "Continue digest review."), - "priority_rank": 4, + "priority_rank": 5, } ) next_learning_focus = dict(setup_learning_rollup.get("next_learning_focus") or {}) @@ -763,20 +779,21 @@ def _build_decision_assistant_loop_queue( "target_section": "setup-replay", "action_label": "Open Replay", "prompt": str(setup_learning_rollup.get("next_learning_focus_prompt") or next_learning_focus.get("prompt") or "Continue replay learning."), - "priority_rank": 5, + "priority_rank": 6, } ) - if review_entrypoints.get("next_review_action"): + next_review_entry_action = dict(review_entrypoints.get("next_review_entry_action") or {}) + if next_review_entry_action.get("label") or review_entrypoints.get("next_review_action"): queue.append( { "queue_id": "review_entry", "lane": "review", - "title": str(review_entrypoints.get("template_summary", {}).get("recommended_label") or "Review Entry"), + "title": str(next_review_entry_action.get("label") or review_entrypoints.get("template_summary", {}).get("recommended_label") or "Review Entry"), "status": str(review_entrypoints.get("status") or "active"), "target_section": "review-entry", "action_label": "Open Review", - "prompt": str(review_entrypoints.get("next_review_action") or "Open review entry."), - "priority_rank": 6, + "prompt": str(next_review_entry_action.get("prompt") or review_entrypoints.get("next_review_action") or "Open review entry."), + "priority_rank": 7, } ) if scan_handoff_summary.get("next_resume_prompt"): @@ -789,7 +806,7 @@ def _build_decision_assistant_loop_queue( "target_section": str(scan_handoff_summary.get("next_target_section") or "scan"), "action_label": "Open Handoff", "prompt": str(scan_handoff_summary.get("next_resume_prompt") or "Resume persisted handoff."), - "priority_rank": 7, + "priority_rank": 8, } ) queue.sort( @@ -1034,12 +1051,63 @@ def _build_candidate_watchlist_tiers(*, candidates: list[dict], session_focus: d def _build_candidate_watchlist_summary(tiers: dict) -> dict: + focus_matrix = _build_candidate_watchlist_focus_matrix(tiers) return { "review_now_count": len(list(tiers.get("review_now") or [])), "strong_watch_count": len(list(tiers.get("strong_watch") or [])), "soft_watch_count": len(list(tiers.get("soft_watch") or [])), "archive_only_count": len(list(tiers.get("archive_only") or [])), "top_review_candidate_id": (tiers.get("review_now") or [{}])[0].get("candidate_id") if list(tiers.get("review_now") or []) else None, + "focus_matrix": focus_matrix, + "next_watchlist_focus": focus_matrix.get("next_focus"), + "next_watchlist_focus_prompt": focus_matrix.get("next_focus_prompt"), + } + + +def _build_candidate_watchlist_focus_matrix(tiers: dict) -> dict: + tier_order = ["review_now", "strong_watch", "soft_watch", "archive_only"] + rows = [] + for tier in tier_order: + for item in list(tiers.get(tier) or []): + rows.append(dict(item)) + if not rows: + return { + "status": "idle", + "tier_counts": {}, + "session_counts": {}, + "recommended_tier": None, + "recommended_session_code": None, + "next_focus": None, + "next_focus_prompt": None, + } + tier_counts: dict[str, int] = {} + session_counts: dict[str, int] = {} + for item in rows: + tier = str(item.get("tier") or "archive_only") + session_code = str(item.get("session_code") or "OFF_HOURS") + tier_counts[tier] = int(tier_counts.get(tier) or 0) + 1 + session_counts[session_code] = int(session_counts.get(session_code) or 0) + 1 + next_focus = dict( + sorted( + rows, + key=lambda row: ( + {"review_now": 1, "strong_watch": 2, "soft_watch": 3, "archive_only": 4}.get(str(row.get("tier") or "archive_only"), 5), + -float(row.get("priority_score") or 0.0), + str(row.get("session_code") or ""), + ), + )[0] + ) + return { + "status": "active", + "tier_counts": tier_counts, + "session_counts": session_counts, + "recommended_tier": next_focus.get("tier"), + "recommended_session_code": next_focus.get("session_code"), + "next_focus": next_focus, + "next_focus_prompt": ( + f"Start with {next_focus.get('title') or 'candidate'} in {next_focus.get('session_code') or 'session'} " + f"because it leads the {next_focus.get('tier') or 'watchlist'} tier." + ), } @@ -1737,6 +1805,16 @@ def _build_decision_assistant_review_entrypoints( item["guardrail_audit_filter_reason"] = outcome.get("guardrail_audit_filter_reason") item["guardrail_audit_filtered_trail"] = outcome.get("guardrail_audit_filtered_trail") or [] item["guardrail_audit_filtered_count"] = outcome.get("guardrail_audit_filtered_count") or 0 + review_entry_action_queue = _build_review_entry_action_queue( + note_templates=note_templates, + template_summary=_build_review_entry_template_summary(note_templates, recommended_template_id=recommended_template_id), + template_reuse_metrics=template_reuse_metrics, + next_actions=next_actions, + ) + review_entry_handoff_summary = _build_review_entry_handoff_summary( + note_templates=note_templates, + review_entry_action_queue=review_entry_action_queue, + ) return { "status": "active", "note_count": len(recent_notes), @@ -1749,6 +1827,10 @@ def _build_decision_assistant_review_entrypoints( "recommended_template_id": recommended_template_id, "template_summary": _build_review_entry_template_summary(note_templates, recommended_template_id=recommended_template_id), "template_reuse_metrics": template_reuse_metrics, + "review_entry_action_queue": review_entry_action_queue[:6], + "review_entry_action_queue_count": len(review_entry_action_queue), + "next_review_entry_action": review_entry_action_queue[0] if review_entry_action_queue else None, + "review_entry_handoff_summary": review_entry_handoff_summary, "actions": [ {"action_id": "open_operator_notes", "label": "Open Notes"}, {"action_id": "open_post_trade_review", "label": "Open Review"}, @@ -2753,6 +2835,79 @@ def _build_review_entry_template_summary(note_templates: list[dict], *, recommen } +def _build_review_entry_action_queue( + *, + note_templates: list[dict], + template_summary: dict, + template_reuse_metrics: dict, + next_actions: list[str], +) -> list[dict]: + queue = [] + if template_summary.get("recommended_label"): + queue.append( + { + "action_id": "use_recommended_template", + "label": str(template_summary.get("recommended_label") or "Use recommended template"), + "priority": str(template_summary.get("recommended_priority") or "medium"), + "prompt": str(template_summary.get("recommended_reuse_hint") or template_summary.get("recommended_reason") or "Use the recommended template."), + "source": "template_summary", + } + ) + feedback_loop = dict(template_reuse_metrics.get("feedback_loop_summary") or {}) + if feedback_loop.get("next_feedback_prompt"): + queue.append( + { + "action_id": "capture_feedback", + "label": "Capture Feedback", + "priority": "medium", + "prompt": str(feedback_loop.get("next_feedback_prompt") or "Capture the next template feedback signal."), + "source": "feedback_loop", + } + ) + aggregation = dict(template_reuse_metrics.get("template_feedback_aggregation") or {}) + if aggregation.get("next_preference_recovery_prompt"): + queue.append( + { + "action_id": "preference_recovery", + "label": "Preference Recovery", + "priority": "medium", + "prompt": str(aggregation.get("next_preference_recovery_prompt") or "Review preference recovery evidence."), + "source": "preference_recovery", + } + ) + if next_actions: + queue.append( + { + "action_id": "weekly_follow_up", + "label": "Weekly Follow-up", + "priority": "low", + "prompt": str(next_actions[0] or "Continue the weekly follow-up action."), + "source": "weekly_review", + } + ) + queue.sort( + key=lambda row: ( + {"high": 1, "medium": 2, "low": 3}.get(str(row.get("priority") or "low"), 4), + str(row.get("source") or ""), + ) + ) + return queue + + +def _build_review_entry_handoff_summary(*, note_templates: list[dict], review_entry_action_queue: list[dict]) -> dict: + lane_counts: dict[str, int] = {} + for item in note_templates: + lane = str(item.get("audit_lane_group") or "stable") + lane_counts[lane] = int(lane_counts.get(lane) or 0) + 1 + return { + "status": "active" if note_templates else "idle", + "lane_counts": lane_counts, + "template_count": len(note_templates), + "action_queue_count": len(review_entry_action_queue), + "next_handoff_prompt": (review_entry_action_queue[0] or {}).get("prompt") if review_entry_action_queue else None, + } + + def _build_external_context_headline( *, news_window: dict, diff --git a/src/web/app.js b/src/web/app.js index 2960684..c41d9f4 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -824,6 +824,8 @@ function renderDecisionAssistant(summary) { ['launch mode', evidence.launch_mode || 'unknown'], ['top opportunity', opportunities[0]?.title || 'n/a'], ['watchlist tiers', `review ${watchlistSummary.review_now_count ?? 0} | strong ${watchlistSummary.strong_watch_count ?? 0} | soft ${watchlistSummary.soft_watch_count ?? 0}`], + ['next watchlist focus', watchlistSummary.focus_matrix?.next_focus?.title || 'n/a'], + ['watchlist focus prompt', watchlistSummary.next_watchlist_focus_prompt || 'n/a'], ['section nav', assistantSections.map((item) => item.title).join(' | ') || 'n/a'], ['no-trade reasons', formatList(summary.no_trade_reasons || [])], ['next actions', formatList(summary.next_actions || [])], @@ -1094,6 +1096,9 @@ function renderDecisionAssistant(summary) { ['journal entries', String(reviewEntrypoints.journal_entry_count ?? 0)], ['review queue', String(reviewEntrypoints.review_queue_count ?? 0)], ['next review action', reviewEntrypoints.next_review_action || 'n/a'], + ['review action queue', String(reviewEntrypoints.review_entry_action_queue_count ?? 0)], + ['next review queue action', reviewEntrypoints.next_review_entry_action?.label || 'n/a'], + ['next review queue prompt', reviewEntrypoints.next_review_entry_action?.prompt || 'n/a'], ['recommended template', reviewEntrypoints.recommended_template_id || 'n/a'], ['recommended label', reviewEntrypoints.template_summary?.recommended_label || 'n/a'], ['recommended priority', reviewEntrypoints.template_summary?.recommended_priority || 'n/a'], @@ -1143,6 +1148,8 @@ function renderDecisionAssistant(summary) { ['next feedback template', reviewEntrypoints.template_reuse_metrics?.feedback_loop_summary?.next_feedback_template_id || 'n/a'], ['next feedback prompt', reviewEntrypoints.template_reuse_metrics?.feedback_loop_summary?.next_feedback_prompt || 'n/a'], ['template priorities', formatJsonInline(reviewEntrypoints.template_summary?.priorities || {})], + ['handoff lanes', formatJsonInline(reviewEntrypoints.review_entry_handoff_summary?.lane_counts || {})], + ['review handoff prompt', reviewEntrypoints.review_entry_handoff_summary?.next_handoff_prompt || 'n/a'], ]) ); decisionAssistantPanel.appendChild( @@ -1161,6 +1168,22 @@ function renderDecisionAssistant(summary) { }, ]) ); + if (reviewEntrypoints.review_entry_action_queue?.length) { + const reviewActionGrid = createElement('div', 'cards assistant-tight-grid'); + (reviewEntrypoints.review_entry_action_queue || []).forEach((item) => { + reviewActionGrid.appendChild( + buildItemCard( + item.label || 'review action', + item.priority || item.source || 'n/a', + [ + ['source', item.source || 'n/a'], + ['prompt', item.prompt || 'n/a'], + ] + ) + ); + }); + decisionAssistantPanel.appendChild(reviewActionGrid); + } const noteTemplates = reviewEntrypoints.note_templates || []; if (noteTemplates.length) { const recommendedTemplate = noteTemplates.find((item) => item.template_id === reviewEntrypoints.recommended_template_id) || noteTemplates[0]; @@ -1378,6 +1401,18 @@ function renderDecisionAssistant(summary) { const items = watchlistTiers[key] || []; if (!items.length) return; appendSectionTitle(decisionAssistantPanel, `Watchlist: ${label}`); + if (key === 'review_now' && watchlistSummary.focus_matrix) { + decisionAssistantPanel.appendChild( + buildInfoGrid([ + ['watchlist focus', watchlistSummary.focus_matrix.status || 'idle'], + ['recommended tier', watchlistSummary.focus_matrix.recommended_tier || 'n/a'], + ['recommended session', watchlistSummary.focus_matrix.recommended_session_code || 'n/a'], + ['watchlist next prompt', watchlistSummary.next_watchlist_focus_prompt || 'n/a'], + ['tier counts', formatJsonInline(watchlistSummary.focus_matrix.tier_counts || {})], + ['session counts', formatJsonInline(watchlistSummary.focus_matrix.session_counts || {})], + ]) + ); + } const tierGrid = createElement('div', 'cards assistant-tight-grid'); items.forEach((item) => { tierGrid.appendChild( diff --git a/tests/unit/test_operator_workspace_service.py b/tests/unit/test_operator_workspace_service.py index cdba3f9..6b8ef13 100644 --- a/tests/unit/test_operator_workspace_service.py +++ b/tests/unit/test_operator_workspace_service.py @@ -165,6 +165,9 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertEqual(summary["candidate_watchlist_tiers"]["soft_watch"][0]["candidate_id"], 22) self.assertEqual(summary["candidate_watchlist_tiers"]["review_now"][0]["action_method"], "POST") self.assertIn("/trading/candidates/18/confirm-check", summary["candidate_watchlist_tiers"]["review_now"][0]["action_path"]) + self.assertIn("focus_matrix", summary["candidate_watchlist_summary"]) + self.assertIn("next_watchlist_focus", summary["candidate_watchlist_summary"]) + self.assertIn("next_watchlist_focus_prompt", summary["candidate_watchlist_summary"]) self.assertEqual(summary["workflow_digest"][0]["phase"], "pre_market") self.assertEqual(summary["workflow_digest"][1]["phase"], "intraday") self.assertEqual(summary["workflow_digest"][2]["phase"], "post_trade") @@ -213,6 +216,9 @@ class OperatorWorkspaceServiceTests(unittest.TestCase): self.assertEqual(summary["review_entrypoints"]["status"], "active") self.assertEqual(summary["review_entrypoints"]["actions"][0]["action_id"], "open_operator_notes") self.assertTrue(summary["review_entrypoints"]["note_templates"]) + self.assertIn("review_entry_action_queue", summary["review_entrypoints"]) + self.assertIn("review_entry_handoff_summary", summary["review_entrypoints"]) + self.assertTrue(summary["review_entrypoints"]["review_entry_action_queue"]) self.assertEqual(summary["review_entrypoints"]["note_templates"][0]["note_scope"], "workbench") self.assertTrue(summary["review_entrypoints"]["note_templates"][0]["tags"]) self.assertEqual(summary["review_entrypoints"]["recommended_template_id"], "top_opportunity")